如果您的应用程序在多个服务器上运行,则你可能会想限制计划任务仅在单个服务器上执行。 例如,您有一个计划任务,每个星期五晚上都会生成一个新报告。 如果在三台服务器上运行,则将在所有三台服务器上运行并生成三次报告。 这样不好!

为了让计划任务只在一台服务器上运行,在定义计划任务时可使用 onOneServer 方法。原理是获取到该任务的第一台服务器将对该任务加上原子锁,以防止其他服务器同时运行相同的任务。

这是laravel-china文档上的说明。

onOneServer()这个方法,在5.6才有,但是在我们的服务分布式部署情况下,却是十分需要的。

原理

非常简单,第一个执行的节点对该任务加上原子锁。

实现

如何能够达到原子锁,那么最好使用redis。

在Laravel中的核心命令是: Redis::add()

转换为Redis核心命令为:setnx

该命令意义为:如果不存在,那么就SET。

该命令是原子性的,也就是说当第一个任务SET了值之后,后边其他相同的任务都会无法执行。

代码

  1. 对Event类添加onOneServer方法及参数。

    vendor/laravel/framework/src/Illuminate/Console/Scheduling/Event.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Event
    {
    // ...
    // 判断参数
    public $onOneServer = false;

    // 设置函数
    public function onOneServer()
    {
    $this->onOneServer = true;

    return $this;
    }
    }

  2. 运行时执行真正操作

    vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    /**
    * Execute the console command.
    *
    * @return void
    */
    public function fire()
    {
    // ...
    foreach ($events as $event) {
    if (! $event->filtersPass($this->laravel)) {
    continue;
    }

    if ($event->onOneServer) { // 是否只能运行在单个节点上
    $this->runSingleServerEvent($event);
    } else {
    $this->runEvent($event);
    }

    ++$eventsRan;
    }
    // ...
    }

    /**
    * 将事件进行上锁处理
    *
    * @param Event $event
    * @return void
    * @author vogan <voganwong@gmail.com>
    */
    protected function runSingleServerEvent($event)
    {
    if ($this->schedule->serverShouldRun($event)) {
    $this->runEvent($event);
    } else {
    $this->line('<info>Skipping command (has already run on another server):</info> '.$event->getSummaryForDisplay());
    }
    }

    vendor/laravel/framework/src/Illuminate/Console/Scheduling/Schedule.php

    1
    2
    3
    4
    5
    public function serverShouldRun($event)
    {
    // 创建一个锁,返回是否成功
    return $this->schedulingMutex->create($event);
    }

    vendor/laravel/framework/src/Illuminate/Console/Scheduling/SchedulingMutex.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 对事件创建任务锁
    *
    * @param Event $event
    * @return bool
    * @author vogan <voganwong@gmail.com>
    */
    public function create($event)
    {
    // 最终调用的即是cache->add()方法,
    // key名为锁的名称,value为true,
    // 另外设置一个超时时间,防止任务执行失败,造成了一个死锁不释放。
    return $this->cache->add(
    $event->mutexName(), true, $event->expiresAt
    );
    }