數據準備:
create table t(c1 int primary key, c2 int, c3 int, c4 int, unique index i_c2(c2), index i_c3(c3));
insert into t values (10, 11, 12, 13), (20, 21, 22, 23), (30, 31, 32, 33), (40, 41, 42, 43);
表名t,c1列為主鍵,c2列為唯一索引,c3列為普通索引
數據庫隔離級別:RR
數據庫版本:mysql 5.7.21
鎖阻塞示意圖(后續分析的時候,用得到):
image.png
死鎖場景
場景說明:批量插入場景,多個會話同時插入,每一個會話插入多條數據。
場景描述:在唯一索引c2的間隙(31,41)插入3條不同記錄。會話1先插入1條c2=36,會話2接著插入1條c2=35,最后會話1插入1條c2=34。這3次操作插入的值都不一樣
會話1:
start transaction;
insert into t values(50,36,52,53) on duplicate key update c2=36;
會話2:
start transaction;
insert into t values(60,35,62,63) on duplicate key update c2=35;
這個時候會話2阻塞了,我們可以查看一下鎖信息
另外開啟一個會話3
show engine innodb status;
image.png
暫不分析這個鎖信息,后面一起分析
接著,會話1:
insert into t values(70,34,72,73) on duplicate key update c2=34;
這個時候某一個會話會顯示如下信息:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
我們切換到會話3
show engine innodb status;
image.png
可以看到最新的1條死鎖信息,就是我們剛剛產生的死鎖
分析:
說在前面:第一張圖命名為圖1,第二張圖命名為圖2
一. 查看圖1,我們知道insert會上3把鎖
1.1 表鎖IX,這個沒有任何問題,插入期間,表結構不能變
1.2 間隙鎖(lock_model X locks gap before rec),這個比較難理解。我看了半天原因也沒看明白想。(如果insert語句命中了記錄,比如已經存在c2=36的記錄,這里會是next-key lock)如果你想細細研究,參考如下文章https://juejin.im/entry/5adca48df265da0b9c1037c8
總之,因為mysql5.6有一個bug,mysql5.7為了修復這個bug,引入了這把鎖
1.3 插入意向鎖(lock_mode X locks gap before rec insert intention waiting),插入的時候,一般都有這把鎖。目的是,告訴別人我要在某某區間插入數據了,避免幻讀(RC隔離級別不會加,這里我80%的把握,如果RC隔離級別會加,還望大神指正)。對意向鎖感興趣,可以參考如下文章:http://yeshaoting.cn/article/database/mysql%20insert%E9%94%81%E6%9C%BA%E5%88%B6/
1.4 還有最后一個紅框,是lock_mode X locks rec but not gap 這個是插入記錄之后的記錄鎖。
這里捋一下:
會話1插入記錄的時候,申請表鎖IX,gap鎖,插入意向鎖。插入成功后,釋放了插入意向鎖,增加了插入記錄的記錄鎖(rec lock)(理論上鎖在事務未提交,不應該釋放,但是看截圖是釋放了的,估計記錄鎖也能夠保證插入意向鎖的功能)
會話2插入記錄的時候,申請表鎖IX,gap鎖。然后申請意向鎖發生了等待,因為會話1持有了gap鎖。
二. 查看圖2,我們可以看到死鎖原因
會話2總共3把鎖,等待插入意向鎖,另外持有的2把鎖沒有顯示,經過圖1的分析,可以猜到是表鎖IX和間隙鎖gap lock。
會話1持有3把鎖,等待1把鎖。持有表鎖IX,gap鎖,第一條插入記錄的record lock鎖(第一條記錄申請的插入意向鎖已經釋放)
會話1插入第三條記錄的時候,先申請gap鎖,發現已經持有,成功(我猜的)。申請插入意向鎖發生了等待,因為會話2持有gap鎖,阻塞了插入意向鎖。
總之,死鎖產生原因是,會話2的插入意向鎖等待會話1釋放gap鎖,會話1插入意向鎖也在等待會話2釋放gap鎖。
三. 如何規避
- 隔離級別調整為RC,讀提交,在這種場景不會有gap鎖(并不是說RC隔離級別不會有gap鎖)。前提是binlog同步機制是基于row,而不是基于statement。這一塊不熟悉的,可以參考
http://www.lxweimin.com/p/c16686b35807
這里多說一句,建議大家都把隔離級別調整為RC,然后binlog基于row。這樣能夠減少很多死鎖的發生(因為死鎖一般是因為gap鎖,而RR隔離級別很多場景都會有gap鎖。而RC隔離級別只有很少場景存在gap鎖) - 改寫為先select 如果存在update,不存在insert