首頁>技術>

01. Mysql 事務死鎖現象及原因初步判斷

做IT的幾乎每天都接觸 MySql,但是 Mysql 事務死鎖卻並不常見,前段時間就讓我遇到了。異常日誌如下

從日誌看是發生了 Lock wait timeout exceeded 異常。

Lock wait timeout exceeded:後提交的事務等待前面處理的事務釋放鎖,但是在等待的時候超過了mysql的鎖等待時間,就會引發這個異常。

PreparedStatementCallback; SQL [UPDATE sf_wx_keyword_ruleSET status = ?,last_update_time = last_update_timeWHERE id = ?];Lock wait timeout exceeded;try restarting transaction;

發生異常的程式碼主要邏輯如下

分析後其實是因為一個處理流程裡開了兩個事務,並更新的同一條資料,導致的事務間死鎖。

外層方法透過@Transactional 開啟了事務1(@t1),對 sf_wx_keyword_rule 一條資料做更新,內層方法透過 REQUIRES_NEW 又開啟了一個新事務2(@t2),並對sf_wx_keyword_rule 的同一條資料做更新。

begin @t1;UPDATE table SET status = ? WHERE id = 1begin @t2;UPDATE table SET status = ? WHERE id = 1commit @t2;commit @t1;

結論:由於 @t1 和 @t2 更新的是同一條資料,所以 @t2 的執行需要依賴 @t1 的提交,而@t1 的提交又需要 @t2 執行完。所以兩個事務互相等待對方提交導致死鎖。

02. 復現及深層原因追蹤

2.1 復現

為了搞清楚事務死鎖,及死鎖期間 MySql 的資料狀態,新建 test1 表重複上述操作

過了大概 30s @t2 返回鎖超時,與異常日誌一致。

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
2.2 原因追蹤2.2.1 事務狀態

Mysql 事務操作會涉及到三張表

//當前正在執行的每個事務的資訊information_schema.innodb_trx//當前事務持有的鎖記錄information_schema.innodb_locks// 當前被阻塞的事務鎖記錄information_schema.innodb_lock_waits 

查詢 innodb_trx 表

主要欄位的含義

當前有兩個未提交的事務,trx_id=21245712 狀態為 LOCK WAIT,這條事務產生了一個 id為 21245712:565:3:2 (innodb_locks 表的id) 的鎖,也就是該事務的 LOCK因為被阻塞而導致事務超時。

trx_id = 21245684 是執行完 SQL 還未提交的事務。

2.2.2 MySql 鎖innodb_locks InnoDB 鎖記錄

主要欄位含義

鎖在 MySql 事務裡是非常主要的,上面的事務就是透過 Primary (主鍵) 在 Record (行) 上加的X (寫) 鎖,先加的 X 鎖會成功,後加的 X 鎖就會被阻塞。下面詳細瞭解一下幾個主要的鎖。

基本鎖

InnoDB 行級鎖,分為共享鎖(S)和獨佔鎖(X)

共享鎖(Sharaed Locks: S鎖),或叫讀鎖mysql允許拿到S鎖的事務讀一行加了S鎖記錄,允許其他事務再加S鎖,不允許其他事務再加X鎖語法:select ... lock in share mode;獨佔鎖(Exclusive Locks:X鎖)或叫寫鎖mysql允許拿到X鎖的事務更新或刪除一行加了X鎖的記錄,不允許其他事務再加X鎖或S鎖語法:select … for update;

所以出現上述事務死鎖超時的原因是 UPDATE 會在記錄上加 X 鎖,阻塞了另一個事務對同一資料加的 X 鎖。

延伸一下,有 X 鎖之後,我們還能正常的讀資料嗎?答案是可以的。

select * from test1;

普通的 SELECT 語句上沒有加鎖,只有 select ... lock in share mode; 才會加 S 鎖。

下面是 MySql 的其他鎖

意向鎖

InnoDB為了支援多粒度(表鎖和行鎖)的鎖並存,引入意向鎖。意向鎖是表級鎖,分為IS鎖和IX鎖。

意向共享鎖(IS)事務在請求S鎖前,需要先獲得對應的IS鎖意向排他鎖 (IX)事務在請求X鎖前,需要先獲得對應的IX鎖

鎖相容矩陣

自增鎖 auto-inc lock

AUTO-INC鎖是事務中的一種特殊的表級鎖,透過AUTO_INCREMENT的列來實現,這種鎖是作用於語句的而不是事務。

記錄鎖 record Lock

即行鎖。單條索引記錄上加鎖,record lock鎖住的永遠是索引,而非記錄本身。

間隙鎖 gap lock

區間鎖, 僅僅鎖住一個索引區間(開區間)。在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄之前或者之後加鎖,並不包括該索引記錄本身。GAP鎖的目的是為了防止同一事務的兩次當前讀,出現幻讀的情況。

臨鍵鎖 next key lock

行鎖和間隙鎖組合起來就叫Next-Key-Lock,左開右閉區間。預設情況下,innodb使用next-key locks來鎖定記錄。但當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行最佳化,將其降級為Record Lock,即僅鎖住索引本身,不是範圍。

插入意向鎖 insert intention lock

Gap Lock中存在一種插入意向鎖(Insert Intention Lock),在insert操作時產生。在多事務同時寫入不同資料至同一索引間隙的時候,並不需要等待其他事務完成,不會發生鎖等待。 假設有一個記錄索引包含鍵值4和7,不同的事務分別插入5和6,每個事務都會產生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因為資料行並不衝突。

注:插入意向鎖並非意向鎖,而是一種特殊的間隙鎖。

如果插入前,該間隙已經有gap鎖,那麼insert 會申請插入意向鎖。因為了避免幻讀,當其他事務持有該間隙的間隔鎖,插入意向鎖就會被阻塞(不用直接用gap鎖,是因為gap鎖不互斥)。

innodb_lock_waits 被阻塞的鎖記錄

這張表裡有記錄就說明有事務被阻塞裡。

主要欄位含義

03. 解決方案及總結

線上遇到死鎖怎麼解決?最快的方式當然是 kill 事務,重啟服務,根本原因還是需要看這三張表,以後再遇到資料庫死鎖、事務死鎖,查這三張表就差不多知道原因了。

我們該如何避免死鎖呢?常規的回答都是以固定的順序訪問資料。但本案例是因為使用了 REQUIRES_NEW 導致。

使用 REQUIRES_NEW 的原因以下場景,內層事務是一個批次更新,但是又不希望因為某一條失敗而影響其他的更新。

begin @t1aMapper.update()for pojo in pojos:  begin @t2  bMapper.update(pojo)  rpc.update()  commitcommit

所以一定要避免內外雙層事務修改同一條資料的情況,對於 Spring 事務傳播機制也要熟知其作用。

要保證資料的最終一致性,應該寫成一個Job,更新失敗後不斷的去補償。

11
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • PostgreSQL每日一貼-測試工具之pgbench