分享遇到過的一種間隙鎖導致的死鎖案例。文后有總結知識供參考
日志出現:Deadlock found when trying to get lock; try restarting transaction
導致原因:並發導致的數據庫間隙鎖死鎖(MySql數據庫默認RR級別)
業務主要操作提煉:首先進來將t1表原來的記錄狀態更新掉,然后插入新的記錄。
線程1 |
線程2 |
...... |
update t1 set status =0 where rule=1 insert t1 () values (),(),(); |
update t1 set status =0 where rule=2 insert t1 () values (),(),(); |
...... |
復現問題:
測試數據准備
-- 准備測試表 CREATE TABLE IF NOT EXISTS `test` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `col` int(11) NULL COMMENT '普通字段', `idx_col` int(11) NULL COMMENT '索引字段', `uni_col` tinyint(4) NULL COMMENT '唯一鍵字段', PRIMARY KEY (`id`), KEY `idx_1` (`idx_col`) USING BTREE, unique KEY `uni_idx_1` (`uni_col`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=0 ; |
-- 插入測試數據 INSERT INTO `test` ( `col`,`idx_col`,`uni_col`) VALUES (3,3,3), (10,10,10), (11,11,11), (20,20,20), (25,25,25), (26,26,26), (50,50,50) ; |
-- 清空數據表 DELETE FROM test ; DROP table test;
-- 查詢測試表數據 SELECT * FROM test; |
驗證間隙鎖的存在:
事務1 |
事務2 |
-- 開始事務1 BEGIN; |
|
|
-- 開始事務2 BEGIN; |
-- 更新test,使用索引字段 UPDATE test set col=99 WHERE idx_col=20; |
|
|
-- 插入數據,因為有間隙鎖,[11-25)這個區間全部被鎖上了,插入被阻塞 INSERT INTO `test` ( `idx_col`) VALUES (11); INSERT INTO `test` ( `idx_col`) VALUES (12); INSERT INTO `test` ( `idx_col`) VALUES (20); INSERT INTO `test` ( `idx_col`) VALUES (24);
在間隙之外的可以順利插入 INSERT INTO `test` ( `idx_col`) VALUES (10); |
由於間隙鎖導致的死鎖案例:(本次報錯復現)
事務1 |
事務2 |
-- 開始事務1 BEGIN; |
|
|
-- 開始事務2 BEGIN; |
-- 更新test,使用索引字段,鎖間隙[11,25) UPDATE test set col=99 WHERE idx_col=20; |
|
|
-- 更新test,使用索引字段,鎖間隙[20,26) UPDATE test set col=99 WHERE idx_col=25; |
-- 使用了事務2的間隙鎖,所以阻塞 |
|
|
-- 使用了事務1的間隙鎖,阻塞,互相需要對方的鎖,導致死鎖 INSERT INTO `test` ( `idx_col`) VALUES (12);
-- 該事務被回滾,事務1提交成功。 |
where條件如果換成唯一鍵或者主鍵,沒有間隙鎖
事務1 |
事務2 |
-- 開始事務1 BEGIN; |
|
|
-- 開始事務2 BEGIN; |
-- 更新test,使用唯一鍵或主鍵無間隙鎖 UPDATE test set col=99 WHERE uni_col=20; |
|
|
-- 更新test,使用唯一鍵或主鍵無間隙鎖 UPDATE test set col=99 WHERE uni_col=25; 或 |
-- 無間隙鎖,順利執行 或 |
|
|
-- 無間隙鎖,順利執行 INSERT INTO `test` ( `uni_col`) VALUES (12); 或 |
如果沒有索引,導致全表鎖
事務1 |
事務2 |
-- 開始事務1 BEGIN; |
|
|
-- 開始事務2 BEGIN; |
-- 更新test UPDATE test set col=99 WHERE col=20; |
|
|
-- 以下語句全部阻塞 或 |
結論:
對於update ,insert組合的這種業務操作,建議update操作使用主鍵或者唯一鍵作為where條件可以有效避免並發時候間隙鎖的危害。
如果該字段不是主鍵或者唯一鍵,建議先查詢,使用主鍵或唯一鍵進行更新。
或者修改數據庫隔離級別為RC級別。
線程1 |
線程2 |
...... |
xx = select id from t1 where rule=1 insert t1 () values (),(),(); |
xx = select id from t1 where rule=1 insert t1 () values (),(),(); |
...... |
注意:以下寫法無效
update t1 set status=0
WHERE id IN (
select id FROM
(SELECT id FROM t1 where rule=2) t
);
本文來自博客園,作者:wanglifeng,轉載請注明原文鏈接:https://www.cnblogs.com/wanglifeng717/p/15993400.html
知識示例與參考如下:
在InnoDB中,主鍵可以被理解為聚簇索引,聚簇索引中的葉子結點就是相應的數據行,具有聚簇索引的表也被稱為聚簇索引表,數據在存儲的時候,是按照主鍵進行排序存儲的。
我們都知道,數據庫在select的時候,會選擇索引列進行查找,索引列都是按照B+樹(多叉搜索樹)數據結構進行存儲,找到主鍵之后,再回到聚簇索引表中進行查詢,這叫回表查詢。
id列是主鍵,RC或RR隔離級別 只有id=10記錄上有行鎖 |
![]()
|
id列是二級唯一索引,RC或RR隔離級別
|
![]()
|
id列是二級非唯一索引RC級別 |
![]()
|
id列是二級非唯一索引RR級別 在RR隔離級別下,為了防止幻讀的發生,會使用Gap鎖。 這里,你可以把Gap鎖理解為,不允許在數據記錄前面插入數據。首先,通過id索引定位到第一條滿足查詢條件的記錄,加記錄上的X鎖,加GAP上的GAP鎖,然后加主鍵聚簇索引上的記錄X鎖,然后返回;然后讀取下一條,重復進行。 直至進行到第一條不滿足條件的記錄[11,f],此時,不需要加記錄X鎖,但是仍舊需要加GAP鎖, |
![]()
|
id上沒有索引,RC級別
若id列上沒有索引,SQL會走聚簇索引的全掃描進行過濾,由於過濾是由MySQL Server層面進行的。因此每條記錄,無論是否滿足條件,都會被加上X鎖。 但是,為了效率考量,MySQL做了優化,對於不滿足條件的記錄,會在判斷后放鎖,最終持有的,是滿足條件的記錄上的鎖,但是不滿足條件的記錄上的加鎖/放鎖動作不會省略。 同時,優化也違背了2PL的約束(同時加鎖同時放鎖) |
![]()
|
id上沒有索引,RR隔離級別 聚簇索引上的所有記錄,都被加上了X鎖。其次,聚簇索引每條記錄間的間隙(GAP),也同時被加上了GAP鎖。 MySQL是做了相關的優化的,就是所謂的semi-consistent read。semi-consistent read開啟的情況下,對於不滿足查詢條件的記錄,MySQL會提前放鎖,同時也不會添加Gap鎖。 |
![]() |
實例:
在RR隔離級別下,針對一個復雜的SQL,首先需要提取其where條件。 Index Key確定的范圍,需要加上GAP鎖;Index Filter過濾條件,視MySQL版本是否支持ICP,若支持ICP(index condition pushdown ,mysql 5.6),則不滿足Index Filter的記錄,不加X鎖,否則需要X鎖; Table Filter過濾條件,無論是否滿足,都需要加X鎖。 |
|
本文來自博客園,作者:wanglifeng,轉載請注明原文鏈接:https://www.cnblogs.com/wanglifeng717/p/15993400.html