線上某服務時不時報出以下異常(大約一天二十屢次):「Deadlock found when trying to get lock;」。mysql
Oh, My God! 是死鎖問題。儘管報錯很少,對性能目前看來也無太大影響,但仍是須要解決,保不齊哪天成爲性能瓶頸。
爲了更系統的分析問題,本文將從死鎖檢測、索引隔離級別與鎖的關係、死鎖成因、問題定位這五個方面來展開討論。算法
圖1 應用日誌sql
左圖那兩輛車形成死鎖了嗎?不是!右圖四輛車形成死鎖了嗎?是!數據庫
圖2 死鎖描述併發
咱們mysql用的存儲引擎是innodb,從日誌來看,innodb主動探知到死鎖,並回滾了某一苦苦等待的事務。問題來了,innodb是怎麼探知死鎖的?性能
直觀方法是在兩個事務相互等待時,當一個等待時間超過設置的某一閥值時,對其中一個事務進行回滾,另外一個事務就能繼續執行。這種方法簡單有效,在innodb中,參數innodb_lock_wait_timeout用來設置超時時間。spa
僅用上述方法來檢測死鎖太過被動,innodb還提供了wait-for graph算法來主動進行死鎖檢測,每當加鎖請求沒法當即知足須要並進入等待時,wait-for graph算法都會被觸發。3d
咱們怎麼知道上圖中四輛車是死鎖的?他們相互等待對方的資源,並且造成環路!咱們將每輛車看爲一個節點,當節點1須要等待節點2的資源時,就生成一條有向邊指向節點2,最後造成一個有向圖。咱們只要檢測這個有向圖是否出現環路便可,出現環路就是死鎖!這就是wait-for graph算法。
圖3 wait for graph日誌
innodb將各個事務看爲一個個節點,資源就是各個事務佔用的鎖,當事務1須要等待事務2的鎖時,就生成一條有向邊從1指向2,最後行成一個有向圖。code
死鎖檢測是死鎖發生時innodb給咱們的救命稻草,咱們須要它,但咱們更須要的是避免死鎖發生的能力,如何儘量避免?這須要瞭解innodb中的鎖。
假設咱們有一張消息表(msg),裏面有3個字段。假設id是主鍵,token是非惟一索引,message沒有索引。
id: bigint |
token: varchar(30) |
message: varchar(4096) |
innodb對於主鍵使用了聚簇索引,這是一種數據存儲方式,表數據是和主鍵一塊兒存儲,主鍵索引的葉結點存儲行數據。對於普通索引,其葉子節點存儲的是主鍵值。
圖4 聚簇索引和二級索引
下面分析下索引和鎖的關係。
1)delete from msg where id=2;
因爲id是主鍵,所以直接鎖住整行記錄便可。
圖5
2)delete from msg where token=’ cvs’;
因爲token是二級索引,所以首先鎖住二級索引(兩行),接着會鎖住相應主鍵所對應的記錄;
圖6
3)delete from msg where message=訂單號是多少’;
message沒有索引,因此走的是全表掃描過濾。這時表上的各個記錄都將添加上X鎖。
圖7
大學數據庫原理都學過,爲了保證併發操做數據的正確性,數據庫都會有事務隔離級別的概念: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級別下儘管加了行鎖,但仍是避免不了幻讀。
圖8
innodb的RR隔離級別能夠避免幻讀發生,怎麼實現?固然須要藉助於鎖了!
爲了解決幻讀問題,innodb引入了gap鎖。
在事務A執行:update msg set message=‘訂單’ where token=‘asd’;
innodb首先會和RC級別同樣,給索引上的記錄添加上X鎖,此外,還在非惟一索引’asd’與相鄰兩個索引的區間加上鎖。
這樣,當事務B在執行insert into msg values (null,‘asd',’hello’); commit;時,會首先檢查這個區間是否被鎖上,若是被鎖上,則不能當即執行,須要等待該gap鎖被釋放。這樣就能避免幻讀問題。
圖9
推薦一篇好文,能夠深刻理解鎖的原理:http://hedengcheng.com/?p=771#_Toc374698322
瞭解了innodb鎖的基本原理後,下面分析下死鎖的成因。如前面所說,死鎖通常是事務相互等待對方資源,最後造成環路形成的。下面簡單講下形成相互等待最後造成環路的例子。
這種狀況很好理解,事務A和事務B操做兩張表,但出現循環等待鎖狀況。
圖10
這種狀況比較常見,以前遇到兩個job在執行數據批量更新時,jobA處理的的id列表爲[1,2,3,4],而job處理的id列表爲[8,9,10,4,2],這樣就形成了死鎖。
圖11
這種狀況比較隱晦,事務A在執行時,除了在二級索引加鎖外,還會在聚簇索引上加鎖,在聚簇索引上加鎖的順序是[1,4,2,3,5],而事務B執行時,只在聚簇索引上加鎖,加鎖順序是[1,2,3,4,5],這樣就形成了死鎖的可能性。
圖12
innodb在RR級別下,以下的狀況也會產生死鎖,比較隱晦。不清楚的同窗能夠自行根據上節的gap鎖原理分析下。
圖13
1)以固定的順序訪問表和行。好比對第2節兩個job批量更新的情形,簡單方法是對id列表先排序,後執行,這樣就避免了交叉等待鎖的情形;又好比對於3.1節的情形,將兩個事務的sql順序調整爲一致,也能避免死鎖。
2)大事務拆小。大事務更傾向於死鎖,若是業務容許,將大事務拆小。
3)在同一個事務中,儘量作到一次鎖定所須要的全部資源,減小死鎖機率。
4)下降隔離級別。若是業務容許,將隔離級別調低也是較好的選擇,好比將隔離級別從RR調整爲RC,能夠避免掉不少由於gap鎖形成的死鎖。
5)爲表添加合理的索引。能夠看到若是不走索引將會爲表的每一行記錄添加上鎖,死鎖的機率大大增大。
下面以本文開頭的死鎖案例爲例,講下如何排查死鎖成因。
1)經過應用業務日誌定位到問題代碼,找到相應的事務對應的sql;
由於死鎖被檢測到後會回滾,這些信息都會以異常反應在應用的業務日誌中,經過這些日誌咱們能夠定位到相應的代碼,並把事務的sql給梳理出來。
1 2 3 4 5 |
|
此外,咱們根據日誌回滾的信息發如今檢測出死鎖時這個事務被回滾。
2)肯定數據庫隔離級別。
執行select @@global.tx_isolation,能夠肯定數據庫的隔離級別,咱們數據庫的隔離級別是RC,這樣能夠很大機率排除gap鎖形成死鎖的嫌疑;
3)找DBA執行下show InnoDB STATUS看看最近死鎖的日誌。
這個步驟很是關鍵。經過DBA的幫忙,咱們能夠有更爲詳細的死鎖信息。經過此詳細日誌一看就能發現,與以前事務相沖突的事務結構以下:
1 2 3 4 5 |
|
這不就是圖10描述的死鎖嘛!