天天看點

mysql 死鎖

線上某服務時不時報出如下異常(大約一天二十多次):“Deadlock found when trying to get lock;”。

      Oh, My God! 是死鎖問題。盡管報錯不多,對性能目前看來也無太大影響,但還是需要解決,保不齊哪天成為性能瓶頸。

     為了更系統的分析問題,本文将從死鎖檢測、索引隔離級别與鎖的關系、死鎖成因、問題定位這五個方面來展開讨論。

mysql 死鎖

1 死鎖是怎麼被發現的?

1.1 死鎖成因&&檢測方法

     左圖那兩輛車造成死鎖了嗎?不是!右圖四輛車造成死鎖了嗎?是!

mysql 死鎖

  我們mysql用的存儲引擎是innodb,從日志來看,innodb主動探知到死鎖,并復原了某一苦苦等待的事務。問題來了,innodb是怎麼探知死鎖的?

     直覺方法是在兩個事務互相等待時,當一個等待時間超過設定的某一閥值時,對其中一個事務進行復原,另一個事務就能繼續執行。這種方法簡單有效,在innodb中,參數innodb_lock_wait_timeout用來設定逾時時間。

     僅用上述方法來檢測死鎖太過被動,innodb還提供了wait-for graph算法來主動進行死鎖檢測,每當加鎖請求無法立即滿足需要并進入等待時,wait-for graph算法都會被觸發。

1.2 wait-for graph原理

     我們怎麼知道上圖中四輛車是死鎖的?他們互相等待對方的資源,而且形成環路!我們将每輛車看為一個節點,當節點1需要等待節點2的資源時,就生成一條有向邊指向節點2,最後形成一個有向圖。我們隻要檢測這個有向圖是否出現環路即可,出現環路就是死鎖!這就是wait-for graph算法。

mysql 死鎖

     innodb将各個事務看為一個個節點,資源就是各個事務占用的鎖,當事務1需要等待事務2的鎖時,就生成一條有向邊從1指向2,最後行成一個有向圖。

1.2 innodb隔離級别、索引與鎖 

      死鎖檢測是死鎖發生時innodb給我們的救命稻草,我們需要它,但我們更需要的是避免死鎖發生的能力,如何盡可能避免?這需要了解innodb中的鎖。

1.2.1 鎖與索引的關系

       假設我們有一張消息表(msg),裡面有3個字段。假設id是主鍵,token是非唯一索引,message沒有索引。

id: bigint token: varchar(30) message: varchar(4096)

     innodb對于主鍵使用了聚簇索引,這是一種資料存儲方式,表資料是和主鍵一起存儲,主鍵索引的葉結點存儲行資料。對于普通索引,其葉子節點存儲的是主鍵值。

mysql 死鎖

   下面分析下索引和鎖的關系。

1)delete from msg where id=2;

     由于id是主鍵,是以直接鎖住整行記錄即可。

mysql 死鎖

2)delete from msg where token=’ cvs’;

    由于token是二級索引,是以首先鎖住二級索引(兩行),接着會鎖住相應主鍵所對應的記錄;

mysql 死鎖

3)delete from msg where message=訂單号是多少’;

     message沒有索引,是以走的是全表掃描過濾。這時表上的各個記錄都将添加上X鎖。

mysql 死鎖

1.2.2 鎖與隔離級别的關系

     大學資料庫原理都學過,為了保證并發操作資料的正确性,資料庫都會有事務隔離級别的概念:

1)未送出讀(Read uncommitted);

2)已送出讀(Read committed(RC));

3)可重複讀(Repeatable read(RR));

4)可串行化(Serializable)。

我們較常使用的是RC和RR。

     送出讀(RC):隻能讀取到已經送出的資料。

     可重複讀(RR):在同一個事務内的查詢都是事務開始時刻一緻的,InnoDB預設級别。

     我們在1.2.1節談論的其實是RC隔離級别下的鎖,它可以防止不同僚務版本的資料修改送出時造成資料沖突的情況,但當别的事務插入資料時可能會出現問題。

       如下圖所示,事務A在第一次查詢時得到1條記錄,在第二次執行相同查詢時卻得到兩條記錄。從事務A角度上看是見鬼了!這就是幻讀,RC級别下盡管加了行鎖,但還是避免不了幻讀。

mysql 死鎖

  innodb的RR隔離級别可以避免幻讀發生,怎麼實作?當然需要借助于鎖了!

     為了解決幻讀問題,innodb引入了gap鎖(間隙鎖)。

      在事務A執行:insert into msg values (null,‘asd',’hello’);  

      innodb首先會和RC級别一樣,給索引上的記錄添加上X鎖,此外,還在非唯一索引’asd’與相鄰兩個索引的區間加上鎖。

       這樣,當事務B在執行update msg set message=‘訂單’ where token=‘asd’ commit;時,會首先檢查這個區間是否被鎖上,如果被鎖上,則不能立即執行,需要等待該gap鎖被釋放。這樣就能避免幻讀問題。

mysql 死鎖

3 死鎖成因

     了解了innodb鎖的基本原理後,下面分析下死鎖的成因。如前面所說,死鎖一般是事務互相等待對方資源,最後形成環路造成的。下面簡單講下造成互相等待最後形成環路的例子。

3.1不同表相同記錄行鎖沖突

     這種情況很好了解,事務A和事務B操作兩張表,但出現循環等待鎖情況。

mysql 死鎖

3.2相同表記錄行鎖沖突

     這種情況比較常見,之前遇到兩個job在執行資料批量更新時,jobA處理的的id清單為[1,2,3,4],而 jobB處理的id清單為[8,9,10,4,2],這樣就造成了死鎖。

mysql 死鎖

3.3不同索引鎖沖突

     這種情況比較隐晦,事務A在執行時,除了在二級索引加鎖外,還會在聚簇索引上加鎖,在聚簇索引上加鎖的順序是[1,4,2,3,5],而事務B執行時,隻在聚簇索引上加鎖,加鎖順序是[1,2,3,4,5],這樣就造成了死鎖的可能性。

mysql 死鎖

3.4 gap鎖沖突

     innodb在RR級别下,如下的情況也會産生死鎖,比較隐晦。不清楚的同學可以自行根據上節的gap鎖原理分析下。

mysql 死鎖

4 如何盡可能避免死鎖

1)以固定的順序通路表和行。比如對第2節兩個job批量更新的情形,簡單方法是對id清單先排序,後執行,這樣就避免了交叉等待鎖的情形;又比如對于3.1節的情形,将兩個事務的sql順序調整為一緻,也能避免死鎖。

2)大事務拆小。大事務更傾向于死鎖,如果業務允許,将大事務拆小。

3)在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖機率。

4)降低隔離級别。如果業務允許,将隔離級别調低也是較好的選擇,比如将隔離級别從RR調整為RC,可以避免掉很多因為gap鎖造成的死鎖。

5)為表添加合理的索引。可以看到如果不走索引将會為表的每一行記錄添加上鎖,死鎖的機率大大增大。

5 如何定位死鎖成因

     下面以本文開頭的死鎖案例為例,講下如何排查死鎖成因。

1)通過應用業務日志定位到問題代碼,找到相應的事務對應的sql;

      因為死鎖被檢測到後會復原,這些資訊都會以異常反應在應用的業務日志中,通過這些日志我們可以定位到相應的代碼,并把事務的sql給梳理出來。

start tran
1 deleteHeartCheckDOByToken
2 updateSessionUser
...
commit      

      此外,我們根據日志復原的資訊發現在檢測出死鎖時這個事務被復原。

繼續閱讀