最近遇到一個mysql在RR級別下的死鎖問題,感受有點意思,研究了一下,作個記錄。
涉及知識點:共享鎖、排他鎖、意向鎖、間隙鎖、插入意向鎖、鎖等待隊列html
有幫助的話就點個贊,關注專欄數據庫,不跑路吧~~
不按期更新數據庫的小知識和實用經驗,讓你不用再須要擔憂跑路mysql
隔離級別:Repeatable-Read
表結構以下算法
create table t ( id int not null primary key AUTO_INCREMENT, a int not null default 0, b varchar(10) not null default '', c varchar(10) not null default '', unique key uniq_a_b(a,b), unique key uniq_c(c) );
初始化數據sql
insert into t(a,b,c) values(1,'1','1');
有A/B兩個session,按以下順序執行兩個事務
數據庫
結果是segmentfault
show engine innodb status
中能夠看到死鎖信息,這裏先不貼,先解釋幾種鎖的概念,再來理解死鎖過程session
這兩種實際上是鎖的模式能夠和行鎖、間隙鎖混搭,多個事務能夠同時持有S鎖,可是隻有一個事務能持有X鎖優化
一種表鎖(也是一種鎖模式),代表有事務即將給對應表的記錄加S或者X鎖。SELECT ... LOCK IN SHARE MODE
會在給記錄加S鎖以前先給表加IS鎖,SELECT ... FOR UPDATE
會在給記錄加X鎖以前給表加IX鎖。
這是一種mysql的鎖優化策略,並非很清楚意向鎖的優化點在哪裏,求大佬指教spa
兩種鎖的兼容狀況以下
設計
很簡單,給對應行加鎖。好比update
、select for update
、delete
等都會給涉及到的行加上行鎖,防止其餘事務的操做
在RR隔離級別下,爲了防止幻讀現象,除了給記錄自己,還須要爲記錄兩邊的間隙加上間隙鎖。
好比列a上有一個普通索引,已經有了一、五、10三條記錄,select * from t where a=5 for update
除了會給5這條記錄加行鎖,還會給間隙(1,5)和(5,10)加上間隙鎖,防止其餘事務插入值爲5的數據形成幻讀。
當a上的普通索引變成惟一索引時,不須要間隙鎖,由於值惟一,select * from t where a=5 for update
不可能讀出兩條記錄來。
間隙鎖相互兼容,由於若是互斥,事務A持有左半段(1,5),事務B持有右半段(1,10),那麼當前面那個例子中a=5的記錄被刪除時,理論上左右兩個間隙鎖得合併成一個新鎖(1,10),那麼這個新的大範圍鎖屬於誰呢?因此間隙鎖相互兼容,不論是S間隙鎖仍是X間隙鎖
插入意向鎖實際上是一種特殊的間隙鎖,從前面對間隙鎖的描述中能夠得知,兩個事務在真正insert以前能夠同時持有一段間隙的間隙鎖,鎖不住真正insert的這個動做。真正insert以前,mysql還會嘗試獲取對應記錄的插入意向鎖,代表有在間隙中插入一個值的意向。
插入意向鎖和間隙鎖互斥,好比事務1鎖了(1,5)這個間隙,事務2就不能獲取到a=3的插入意向鎖,因此須要鎖等待。
接下來就能夠來分析前面那個例子中的死鎖過程了,先看show engine innodb status
*** (1) TRANSACTION: TRANSACTION 5967, ACTIVE 8 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 MySQL thread id 9, OS thread handle 140528848688896, query id 537 192.168.128.1 root update insert into t(a,b) values(0,'0') *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5967 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 1; hex 31; asc 1;; 2: len 4; hex 80000001; asc ;; *** (2) TRANSACTION: TRANSACTION 5968, ACTIVE 7 sec inserting mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 MySQL thread id 8, OS thread handle 140528848484096, query id 538 192.168.128.1 root update insert into t(a,b) values(0,'0') *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5968 lock_mode X locks gap before rec Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 1; hex 31; asc 1;; 2: len 4; hex 80000001; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5968 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 1; hex 31; asc 1;; 2: len 4; hex 80000001; asc ;; *** WE ROLL BACK TRANSACTION (2)
session A(即TRANSACTION 5967)正在等待記錄(a=1,b='1')以前的插入意向鎖,session B(即TRANSACTION 5968)持有記錄(a=1,b='1')以前的間隙鎖,卻也在等待那個插入意向鎖。這說的什麼玩意兒,是否是很詭異?
從頭開始分析過程
select * from t where a=0 and b='0' for update;
,先加了IX鎖,而後本來意圖爲給(0, '0')這條記錄加排他行鎖,可是記錄不存在,因此變成了排他間隙鎖(-∞,1)select * from t where a=0 and b='0' for update;
,也是先加了IX鎖,由於記錄不存在,因此加上了排他間隙鎖(-∞,1),可是因爲間隙鎖相互兼容,因此沒有blockinsert into t(a,b) values(0,'0');
,這時候,要開始真正insert了,A須要得到(0,'0')上的插入意向鎖,因爲和B持有的(-∞,1)排他間隙鎖衝突,因此鎖等待,進入記錄(0,'0')的鎖等待隊列(雖然記錄並不存在)insert into t(a,b) values(0,'0');
,要獲取插入意向鎖,發現雖然B本身是持有(-∞,1)的排他間隙鎖,可是A也有,因此進入等待隊列,等待A釋放死鎖信息解讀
事務1(TRANSACTION 5967),等待得到鎖index uniq_a_b of table t2
.t
trx id 5967 lock_mode X locks gap before rec insert intention waiting,即在惟一索引uniq_a_b
上的插入意向鎖(lock_mode X locks gap before rec insert intention)
鎖的邊界爲
0: len 4; hex 80000001; asc ;; 1: len 1; hex 31; asc 1;; 2: len 4; hex 80000001; asc ;;
代表兩行記錄
至於int值按位或上的0x80000000就不是很清楚爲何了,須要大佬解讀
事務2(TRANSACTION 5968),持有間隙鎖index uniq_a_b of table t2
.t
trx id 5968 lock_mode X locks gap before rec,等待插入意向鎖index uniq_a_b of table t2
.t
trx id 5968 lock_mode X locks gap before rec insert intention,因此死鎖發生。
原則上是innodb引擎判斷哪一個事務回滾代價小就回滾哪一個事務,可是具體評判標準不是很清楚(再一次須要大佬),這裏innodb選擇了回滾事務2。至此,死鎖過程分析完畢
還沒完。。。有個神奇的現象是,若是表結構變成
create table t ( id int not null primary key AUTO_INCREMENT, a int not null default 0, b varchar(10) not null default '', c varchar(10) not null default '', unique key uniq_c(c), unique key uniq_a_b(a,b) ); insert into t(a,b,c) values(1,1,1);
只是把c上的惟一索引uniq_c
放到了uniq_a_b
前面,那麼最後的死鎖信息就變了!
*** (1) TRANSACTION: TRANSACTION 5801, ACTIVE 5 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1 MySQL thread id 5, OS thread handle 140528848688896, query id 380 192.168.128.1 root update insert into t2(a,b) values(0,'0') *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 56 page no 5 n bits 72 index uniq_a_b of table `t2`.`t2` trx id 5801 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 1; hex 31; asc 1;; 2: len 4; hex 80000001; asc ;; *** (2) TRANSACTION: TRANSACTION 5802, ACTIVE 4 sec inserting mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 MySQL thread id 6, OS thread handle 140528848484096, query id 381 192.168.128.1 root update insert into t2(a,b) values(0,'0') *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 56 page no 5 n bits 72 index uniq_a_b of table `t2`.`t2` trx id 5802 lock_mode X locks gap before rec Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 1; hex 31; asc 1;; 2: len 4; hex 80000001; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 56 page no 4 n bits 72 index uniq_c of table `t2`.`t2` trx id 5802 lock mode S waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 0; hex ; asc ;; 1: len 4; hex 80000002; asc ;; *** WE ROLL BACK TRANSACTION (2)
事務2等待的鎖由前面的插入意向鎖變成了共享鎖。什麼鬼?
因爲沒看過源碼,只能根據現象倒推:由於表結構上c的惟一索引在(a,b)前面,而插入的時候沒指定c的值,用的默認值0,innodb須要先去查一下有沒有0這條記錄,有的話就要報惟一鍵衝突了,因此先要加S鎖,可是在(0,'0')這條記錄上已經有了IX鎖,看一下前面的兼容性矩陣,S鎖和IX鎖互斥,因此也只能鎖等待
看似一句簡單的select和insert,底下設計很是複雜的鎖機制,理解這些鎖機制有利於寫出高效的SQL(至少是正確的😂)
遺留問題:
有幫助的話就點個贊,關注專欄數據庫,不跑路吧~~
不按期更新數據庫的小知識和實用經驗,讓你不用再須要擔憂跑路