基本介紹
有時候一個定時任務執行需要的時間可能會比我們想象的要長,這就會引起一個問題 —— **當前任務還沒有執行完畢的時候另一個相同的任務也會執行,從而導致任務重複。**例如想象一下我們執行每分鐘生成一次報告的任務,在經過一段時間後,數據量變得很大導致執行時間多於 1 分鐘,這樣就會導致在上一個任務還沒結束的時候另一個相同的任務開始執行。
解決方法
大部分情況下是沒有什麼問題的,但是有時我們需要避免這種情況來保證獲得正確的數據。在 Laravel 中我們可以通過withoutOverlapping
方法來進行處理:
$schedule->command('mail:send')->withoutOverlapping();
Laravel 會檢查 Console\Scheduling\Event::withoutOverlapping
屬性,如果該值爲 true 那麼將會針對這個任務創建一個互斥鎖 (mutex),並且只有在可以創建互斥鎖的情況下才會執行此任務。
什麼是互斥鎖?
這是我在網上找到的最有趣的解釋:
當我們在開會進行激烈的討論時,我會從我桌子裏拿出來一個尖叫雞。只有手裏拿着尖叫雞的人才能說話,如果你沒有拿着尖叫雞你是不能說話的。你只能向會議主持人請示,只有在你拿到尖叫雞的時候你才能說話否則只能等待。當你講話完畢的時候,將尖叫雞還給會議主持人,主持人會將尖叫雞給到下一個人來讓其說話。這樣會確保人們不會互相交談,同時他們也會有自己的時間來進行講話。
將尖叫雞換成互斥鎖,人換成線程。你基本上就有了一個互斥鎖的基本概念。
– https://stackoverflow.com/questions/34524/what-is-a-mutex/34558#34558
原理分析
Laravel 在第一次執行任務的時候會創建一個互斥鎖,然後在每次執行任務時會檢查互斥鎖是否存在,只有互斥鎖不存在的時候任務纔會執行。下面是 withoutOverlapping
方法:
public function withoutOverlapping()
{
$this->withoutOverlapping = true;
return $this->then(function () {
$this->mutex->forget($this);
})->skip(function () {
return $this->mutex->exists($this);
});
}
Laravel 創建了一個過濾回調方法來告訴計劃管理器忽略互斥鎖仍然存在的任務,同時也創建了一個在完成任務實例後清除互斥鎖的回調。同時,在執行任務之前,Laravel 會在 Console\Scheduling\Event::run() 方法中依次執行下面一系列的檢查:
if ($this->withoutOverlapping && ! $this->mutex->create($this)) {
return;
}
那麼互斥鎖的屬性是從哪裏來的呢?
當 Console\Scheduling\Schedule
被實例化的時候,Laravel 會檢查 Console\Scheduling\Mutex
是否綁定到了容器,如果是那麼就會實例化它,否則會使用 Console\Scheduling\CacheMutex
$this->mutex = $container->bound(Mutex::class)
? $container->make(Mutex::class)
: $container->make(CacheMutex::class);
現在當任務管理器在註冊事件的時候會將互斥鎖的實例一併傳進去:
$this->events[] = new Event($this->mutex, $command);
Laravel 默認使用了緩存實現的互斥鎖,但是你可以自己實現並替換它。
緩存版的互斥鎖
public function create(Event $event)
{
return $this->cache->add($event->mutexName(), true, 1440);
}
public function exists(Event $event)
{
return $this->cache->has($event->mutexName());
}
public function forget(Event $event)
{
$this->cache->forget($event->mutexName());
}
就像我們之前看過的,管理器註冊了一個執行後回調來保證任務執行完畢的時候移除互斥鎖,對於一個系統裏的命令來說也許已經可以確保移除了。但是對於一個回調方法的任務來說腳本可能在執行回調的時候結束,因此爲了避免這種情況在 Console\Scheduling\CallbackEvent::run()
方法中加入了下面的代碼確保互斥鎖在任務意外關閉的時候能夠正常移除:
register_shutdown_function(function () {
$this->removeMutex();
});