數據庫死鎖分析(行鎖、間隙鎖)


 

分享遇到過的一種間隙鎖導致的死鎖案例。文后有總結知識供參考

 

日志出現: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);
INSERT INTO `test` ( `idx_col`) VALUES (25);
INSERT INTO `test` ( `idx_col`) VALUES (26);

 

由於間隙鎖導致的死鎖案例:(本次報錯復現)

事務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的間隙鎖,所以阻塞
INSERT INTO `test` ( `idx_col`) VALUES (21);

 

 

-- 使用了事務1的間隙鎖,阻塞,互相需要對方的鎖,導致死鎖
-- Deadlock found when trying to get lock; try restarting transaction

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;

UPDATE test set col=99 WHERE uni_col=20 AND col=20;

 

 

-- 更新test,使用唯一鍵或主鍵無間隙鎖

UPDATE test set col=99 WHERE uni_col=25;


UPDATE test set col=99 WHERE uni_col=25 AND col=25;

-- 無間隙鎖,順利執行
INSERT INTO `test` ( `uni_col`) VALUES (21);


INSERT INTO `test` ( `uni_col`,`col`) VALUES (21,21);

 

 

-- 無間隙鎖,順利執行

INSERT INTO `test` ( `uni_col`) VALUES (12);


INSERT INTO `test` ( `uni_col`,`col`) VALUES (12,12);

 

 

如果沒有索引,導致全表鎖

事務1

事務2

-- 開始事務1

BEGIN;

 

 

-- 開始事務2

BEGIN;

-- 更新test

UPDATE test set col=99 WHERE  col=20;

 

 

-- 以下語句全部阻塞
UPDATE test set col=99 WHERE  col=25;


INSERT INTO `test` ( `uni_col`,`col`) VALUES (100,100);

 

 

 

 

結論:

對於update ,insert組合的這種業務操作,建議update操作使用主鍵或者唯一鍵作為where條件可以有效避免並發時候間隙鎖的危害。

如果該字段不是主鍵或者唯一鍵,建議先查詢,使用主鍵或唯一鍵進行更新。

或者修改數據庫隔離級別為RC級別。

線程1

線程2

......

xx = select id from t1 where rule=1
update t1 set status =0 where id in(xx)

insert  t1 () values (),(),();

xx = select id from t1 where rule=1
update t1 set status =0 where id in(xx)

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鎖。

 

實例:
delete from t1 where pubtime>1 and pubtime<20 and userid='hdc' and comment is not null

 

在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

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM