數據準備(與其他場景一樣):
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列為普通索引
數據庫隔離級別:RC(注意,隔離級別變成RC了)
數據庫版本:mysql 5.7.21
鎖阻塞示意圖(后續分析的時候,用得到):
image.png
1.死鎖場景描述
1.1場景說明
重復提交數據,比如前端提交按鈕重復點擊了多次,后端也沒有控制
1.2場景描述(與場景二一樣,只是sql沒有了on duplicate key)
在唯一索引c2的間隙(31,41)插入3條相同記錄。會話1插入1條c2=36的記錄,會話2插入相同數據,會話3插入相同數據,會話1回滾,會話2,3形成了死鎖。(如果會話1提交,不會形成死鎖)
會話1:
start transaction;
insert into t values(50,36,52,53);
會話2:
start transaction;
insert into t values(51,36,62,63);
會話3:
start transaction;
insert into t values(52,36,62,63);
這個時候會話2,會話3都阻塞了,我們可以查看一下鎖信息
另外開啟一個會話4
show engine innodb status;
image.png
暫不分析,后面一起分析
會話1:
rollback;
這個時候可以看到會話2,3形成了死鎖
會話4查看死鎖信息:
show engine innodb status;
image.png
1.3死鎖分析
1.3.1 圖1分析
會話2,3被會話1阻塞的時候,會話2,3都顯示lock mode S waiting。這個意思是等待共享的next-key鎖。因為會話1持有了新插入記錄的鎖,會話2,3等待在這條記錄上加共享的next-key鎖。
1.3.2 圖2分析
死鎖信息表明,會話2,3都在等待插入意向鎖,會話3顯示持有了lock mode S locks gap before rec(共享的間隙鎖)。會話2,3都持有了4把鎖,會話2也應該持有了相同的間隙鎖。死鎖跟場景二一樣了,插入意向鎖被gap鎖阻塞了(相互阻塞)。與場景二唯一的區別是,等待的是共享的next-key鎖(場景二排他的),會話1回滾后,拿到的是共享的gap鎖(場景二排他的)。
1.3.3 隔離級別改為REPEATABLE-READ
大家可以嘗試一下,結果是一模一樣的
1.3.4 會話1修改為刪除一條記錄
這個場景可以修改一下,會話1刪除1條已經存在的記錄,會話2,3分別插入相同的記錄。比如:
會話1: delete from t where c2=31;
會話2:insert into t values(32,31,322,332);
會話3:insert into t values(33,31,323,333);
會話1:提交
也會有概率地出現死鎖。原因差不太多,刪除的時候,會鎖住c2=31的記錄,阻塞會話2,3獲取next-key鎖。一旦會話1提交,會話2,3都會拿到next-key鎖,發現記錄刪除了,重新掃描,變成獲取gap鎖了(這里有一定的概率,因為有可能一個會話執行比較快,直接拿完gap鎖和插入意向鎖了,另外一個會話還在拿gap鎖的路上)。都去獲取插入意向鎖的時候,互相等待對方釋放gap鎖
隔離級別為RC或者RR都會有概率出現死鎖
1.3.5 會話2,3sql加上on duplicate key
會話2:insert into t values(32,31,322,332) on duplicate key update c3=322;
會話3:insert into t values(33,31,323,333) on duplicate key update c3=323;
這樣不會有死鎖,查看鎖等待信息,可以看到也是等待獲取next-key鎖,會話1提交后,也會發生鎖遷移,但是還是去獲取next-key鎖,這樣會話2,3就只有一個能夠進入。這里比較奇怪,為什么沒有變成去獲取gap鎖,沒想通
2.如何規避
跟場景二一樣