最近項目線上環境,隊列服務器上一直頻繁地大量出現數據庫死鎖問題,這個問題最先能夠追溯到年前,19年的時候就出現了,當時一直頻於開發業務功能,因此一直未去處理這個問題,此次正好來探究一下死鎖的緣由和問題所在。php
首先,目前項目中使用的隊列驅動選用的是database,由於簡單、高效、無需擴展其餘第三方應用,就一直採用了mysql數據庫來做爲隊列驅動,線上隊列環境運行的是:Ubuntu 16.04 + Mysql5.7 + Laravel5.6,這樣的一個配置,目前總體使用supervisor在上面託管了16個隊列進程。
mysql
上圖顯示了17個,由於有一個匹配符,因此須要-1,就是16個。laravel
這個死鎖問題,接近觸發了44萬次事件,幾乎每分每秒都有概率觸發死鎖,看了下隊列源碼發現是X鎖形成的,而後下面就嘗試模擬一下多進程隊列消費,是否會形成死鎖出現。sql
經過artisan命令來生成,而後咱們爲了模擬處理過程,每一個隊列暫停了500毫秒。數據庫
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class TestJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** * Create a new job instance. * * @return void */ public function __construct() { // } /** * Execute the job. * * @return void */ public function handle() { usleep(500000); } }
for($i=0;$i<10000;$i++){ dispatch(new TestJob())->onQueue('test'); }
咱們這裏使用supervisor來託管咱們的8個處理進程,使用配置以下:segmentfault
[program:laravel-worker-queue-test] process_name=%(program_name)s_%(process_num)02d command=php /data/sites/test/artisan queue:work --queue=test autostart=true autorestart=true numprocs=8 user=root redirect_stderr=true stdout_logfile=/data/sites/test/storage/logs/worker.log
而後開始啓動8個進程,進行測試。
服務器
而後發現,消費到1400+任務的時候,就產生了456次死鎖。
併發
下面咱們就來分析一下死鎖過程和嘗試解決一些方案svg
咱們運行了8個進程,就至關於8名工做人員,他們都會進行 "求職操做",來得到下一個Job進行工做,在Laravel的源碼中實現是這樣的:測試
public function pop($queue = null) { $queue = $this->getQueue($queue); return $this->database->transaction(function () use ($queue) { if ($job = $this->getNextAvailableJob($queue)) { return $this->marshalJob($queue, $job); } return null; }); }
轉換成SQL語句就是以下操做:
BEGIN TRANSACTION; SELECT * FROM `jobs` WHERE `queue` = ? AND ((`reserved_at` IS NULL and `available_at` <= NOW()) OR (`reserved_at` <= ?)) ORDER BY `id` ASC limit 1 FOR UPDATE; UPDATE `jobs` SET `reserved_at` = NOW(), `attempts` = `attempts` + 1 WHERE `id` = ?; COMMIT;
第一個select查詢,主要在進行得到下一個可用的job,若是available_at < now
,這表示該做業可用,而後選擇了for update
增長了排它鎖,禁止其餘工做人員(worker進程),進行處理貨貨更新。
第二個update更新,工做人員(worker進程)將會更新reserved_at
時間,進行保留,讓其餘工做進程沒法再查詢到,同時reserved_at
字段將會保障,每一個job在刪除以前,至少將被執行一次(除了attempts太大,知足刪除條件)。
當執行完第二個update操做後,工做人員(worker進程)將會開始處理隊列做業,處理完成後,中途沒有異常後,工做人員就會開始刪除掉該做業。
laravel代碼以下:
public function deleteReserved($queue, $id) { $this->database->transaction(function () use ($id) { if ($this->database->table($this->table)->lockForUpdate()->find($id)) { $this->database->table($this->table)->where('id', $id)->delete(); } }); }
轉換成對應的SQL操做:
BEGIN TRANSACTION; SELECT * from `jobs` WHERE `id` = ? FOR UPDATE; DELETE FROM `jobs` WHERE `id` = ?; COMMIT;
首先仍是會嘗試去使用X鎖,鎖住該記錄,而後進行刪除,再提交整個事務。
這樣問題就開始來了,經過以上結構,單個進程進行該操做應該沒有太大問題,可是多個進程同時操做執行2組SQL的時候,可能就會出現死鎖了。
當同時8個進程進行該操做時,同時線上又在頻繁的操做該表,這邊又在頻繁的刪改查,能夠算得上併發式的瘋狗操做。
當工做進程(1)正在查詢下一個可用工做進程時,他將會經過for update嘗試鎖住主鍵索引(id_index)
工廠進程(2)也剛處理完一個做業,而且正在嘗試執行刪除查詢,以便從該表中刪除做業,當能夠執行刪除時,已經拿到了主鍵鎖(index lock),可是刪除操做又會影響到queue_index,所以查詢就會請求該鎖。
這樣將會可能產生全局死鎖,每一個事務都在等待另外一個事務持有的鎖。
下面是用腳本模擬整個隊列的操做流程,依然產生了大量的死鎖:
根據以上的問題,想到了一些解決方案,仍然能夠有效處理掉死鎖:
1.切換到隊列系統到Redis或Beanstalkd,減小Mysql層面的事務開銷,利用內存達到更快的處理速度。
2.刪除掉queue_index索引,爲了不死鎖,咱們能夠刪除這個條件,可是刪除後,處理速度會大大下降。
3.添加軟刪除:deleted_at,將數據變成更新操做,而不是刪除操做,因爲是更新,因此不會致使死鎖(無需鎖定該記錄)
4.嘗試使用第三方擴展包laravel-queue-database-ph4,使用S鎖實現的數據庫隊列,增長了version字段,消除掉了死鎖的問題。