01. MySQL 事務(wù)死鎖現(xiàn)象及原因初步判斷
做IT的幾乎每天都接觸 MySql,但是 Mysql 事務(wù)死鎖卻并不常見,前段時間就讓我遇到了。異常日志如下

從日志看是發(fā)生了 Lock wait timeout exceeded 異常。
Lock wait timeout exceeded:后提交的事務(wù)等待前面處理的事務(wù)釋放鎖,但是在等待的時候超過了mysql的鎖等待時間,就會引發(fā)這個異常。
PreparedStatementCallback; SQL [
UPDATE sf_wx_keyword_rule
SET status = ?,
last_update_time = last_update_time
WHERE id = ?];
Lock wait timeout exceeded;
try restarting transaction;
發(fā)生異常的代碼主要邏輯如下


分析后其實是因為一個處理流程里開了兩個事務(wù),并更新的同一條數(shù)據(jù),導致的事務(wù)間死鎖。
外層方法通過@Transactional 開啟了事務(wù)1(@t1),對 sf_wx_keyword_rule 一條數(shù)據(jù)做更新,內(nèi)層方法通過 REQUIRES_NEW 又開啟了一個新事務(wù)2(@t2),并對sf_wx_keyword_rule 的同一條數(shù)據(jù)做更新。
begin @t1;
UPDATE table SET status = ? WHERE id = 1
begin @t2;
UPDATE table SET status = ? WHERE id = 1
commit @t2;
commit @t1;
結(jié)論:由于 @t1 和 @t2 更新的是同一條數(shù)據(jù),所以 @t2 的執(zhí)行需要依賴 @t1 的提交,而@t1 的提交又需要 @t2 執(zhí)行完。所以兩個事務(wù)互相等待對方提交導致死鎖。
02. 復現(xiàn)及深層原因追蹤
2.1 復現(xiàn)
為了搞清楚事務(wù)死鎖,及死鎖期間 MySql 的數(shù)據(jù)狀態(tài),新建 test1 表重復上述操作


過了大概 30s @t2 返回鎖超時,與異常日志一致。
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
2.2 原因追蹤
2.2.1 事務(wù)狀態(tài)
Mysql 事務(wù)操作會涉及到三張表
//當前正在執(zhí)行的每個事務(wù)的信息
information_schema.innodb_trx
//當前事務(wù)持有的鎖記錄
information_schema.innodb_locks
// 當前被阻塞的事務(wù)鎖記錄
information_schema.innodb_lock_waits
查詢 innodb_trx 表

主要字段的含義

當前有兩個未提交的事務(wù),trx_id=21245712 狀態(tài)為 LOCK WAIT,這條事務(wù)產(chǎn)生了一個 id為 21245712:565:3:2 (innodb_locks 表的id) 的鎖,也就是該事務(wù)的 LOCK因為被阻塞而導致事務(wù)超時。
trx_id = 21245684 是執(zhí)行完 SQL 還未提交的事務(wù)。
2.2.2 MySql 鎖
- innodb_locks InnoDB 鎖記錄

主要字段含義

鎖在 MySql 事務(wù)里是非常主要的,上面的事務(wù)就是通過 Primary (主鍵) 在 Record (行) 上加的X (寫) 鎖,先加的 X 鎖會成功,后加的 X 鎖就會被阻塞。下面詳細了解一下幾個主要的鎖。
基本鎖
InnoDB 行級鎖,分為共享鎖(S)和獨占鎖(X)
- 共享鎖(Sharaed Locks: S鎖),或叫讀鎖
- mysql允許拿到S鎖的事務(wù)讀一行
- 加了S鎖記錄,允許其他事務(wù)再加S鎖,不允許其他事務(wù)再加X鎖
- 語法:select ... lock in share mode;
- 獨占鎖(Exclusive Locks:X鎖)或叫寫鎖
- mysql允許拿到X鎖的事務(wù)更新或刪除一行
- 加了X鎖的記錄,不允許其他事務(wù)再加X鎖或S鎖
- 語法:select … for update;
所以出現(xiàn)上述事務(wù)死鎖超時的原因是 UPDATE 會在記錄上加 X 鎖,阻塞了另一個事務(wù)對同一數(shù)據(jù)加的 X 鎖。
延伸一下,有 X 鎖之后,我們還能正常的讀數(shù)據(jù)嗎?答案是可以的。
select * from test1;
普通的 SELECT 語句上沒有加鎖,只有 select ... lock in share mode; 才會加 S 鎖。



下面是 MySql 的其他鎖
意向鎖
InnoDB為了支持多粒度(表鎖和行鎖)的鎖并存,引入意向鎖。意向鎖是表級鎖,分為IS鎖和IX鎖。
- 意向共享鎖(IS)事務(wù)在請求S鎖前,需要先獲得對應(yīng)的IS鎖
- 意向排他鎖 (IX)事務(wù)在請求X鎖前,需要先獲得對應(yīng)的IX鎖
鎖兼容矩陣

自增鎖 auto-inc lock
AUTO-INC鎖是事務(wù)中的一種特殊的表級鎖,通過AUTO_INCREMENT的列來實現(xiàn),這種鎖是作用于語句的而不是事務(wù)。
記錄鎖 record Lock
即行鎖。單條索引記錄上加鎖,record lock鎖住的永遠是索引,而非記錄本身。
間隙鎖 gap lock
區(qū)間鎖, 僅僅鎖住一個索引區(qū)間(開區(qū)間)。在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄之前或者之后加鎖,并不包括該索引記錄本身。GAP鎖的目的是為了防止同一事務(wù)的兩次當前讀,出現(xiàn)幻讀的情況。
臨鍵鎖 next key lock
行鎖和間隙鎖組合起來就叫Next-Key-Lock,左開右閉區(qū)間。默認情況下,innodb使用next-key locks來鎖定記錄。但當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優(yōu)化,將其降級為Record Lock,即僅鎖住索引本身,不是范圍。
插入意向鎖 insert intention lock
Gap Lock中存在一種插入意向鎖(Insert Intention Lock),在insert操作時產(chǎn)生。在多事務(wù)同時寫入不同數(shù)據(jù)至同一索引間隙的時候,并不需要等待其他事務(wù)完成,不會發(fā)生鎖等待。 假設(shè)有一個記錄索引包含鍵值4和7,不同的事務(wù)分別插入5和6,每個事務(wù)都會產(chǎn)生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因為數(shù)據(jù)行并不沖突。
注:插入意向鎖并非意向鎖,而是一種特殊的間隙鎖。
如果插入前,該間隙已經(jīng)有g(shù)ap鎖,那么insert 會申請插入意向鎖。因為了避免幻讀,當其他事務(wù)持有該間隙的間隔鎖,插入意向鎖就會被阻塞(不用直接用gap鎖,是因為gap鎖不互斥)。
- innodb_lock_waits 被阻塞的鎖記錄
這張表里有記錄就說明有事務(wù)被阻塞里。

主要字段含義

03. 解決方案及總結(jié)
線上遇到死鎖怎么解決?最快的方式當然是 kill 事務(wù),重啟服務(wù),根本原因還是需要看這三張表,以后再遇到數(shù)據(jù)庫死鎖、事務(wù)死鎖,查這三張表就差不多知道原因了。
我們該如何避免死鎖呢?常規(guī)的回答都是以固定的順序訪問數(shù)據(jù)。但本案例是因為使用了 REQUIRES_NEW 導致。
使用 REQUIRES_NEW 的原因以下場景,內(nèi)層事務(wù)是一個批量更新,但是又不希望因為某一條失敗而影響其他的更新。
begin @t1
aMApper.update()
for pojo in pojos:
begin @t2
bMapper.update(pojo)
rpc.update()
commit
commit
所以一定要避免內(nèi)外雙層事務(wù)修改同一條數(shù)據(jù)的情況,對于 Spring 事務(wù)傳播機制也要熟知其作用。
要保證數(shù)據(jù)的最終一致性,應(yīng)該寫成一個Job,更新失敗后不斷的去補償。
公眾號:看起來很美(kanqilaihenmei_)