本文翻譯自How Laravel prevents your scheduled jobs from overlapping - Diving Laravel
基本介紹
有時候一個定時任務執行需要的時間可能會比我們想象的要長,這就會引起一個問題——當前任務還沒有執行完畢的時候另一個相同的任務也會執行,從而導致任務重複。例如想象一下我們執行每分鐘生成一次報告的任務,在經過一段時間後,資料量變得很大導致執行時間多於1分鐘,這樣就會導致在上一個任務還沒結束的時候另一個相同的任務開始執行。
解決方法
大部分情況下是沒有什麼問題的,但是有時我們需要避免這種情況來保證獲得正確的資料。在Laravel中我們可以通過withoutOverlapping
方法來進行處理:
$schedule->command('mail:send')->withoutOverlapping();
Laravel會檢查Console\Scheduling\Event::withoutOverlapping
屬性,如果該值為true那麼將會針對這個任務建立一個互斥鎖(mutex),並且只有在可以建立互斥鎖的情況下才會執行此任務。
什麼是互斥鎖?
這是我在網上找到的最有趣的解釋:
當我們在開會進行激烈的討論時,我會從我桌子裡拿出來一個尖叫雞。只有手裡拿著尖叫雞的人才能說話,如果你沒有拿著尖叫雞你是不能說話的。你只能向會議主持人請示,只有在你拿到尖叫雞的時候你才能說話否則只能等待。當你講話完畢的時候,將尖叫雞還給會議主持人,主持人會將尖叫雞給到下一個人來讓其說話。這樣會確保人們不會互相交談,同時他們也會有自己的時間來進行講話。
將尖叫雞換成互斥鎖,人換成執行緒。你基本上就有了一個互斥鎖的基本概念。
-- https://stackoverflow.com/questions/34524/...
原理分析
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預設使用了快取實現的互斥鎖,但是你可以自己實現並替換它。
快取版的互斥鎖
CacheMutex類只有3個簡單的方法,它使用了事件互斥鎖的名字作為快取的鍵值:
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();
});