搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

前言

我們使用 MySQL 大機率上都會遇到死鎖問題,這實在是個使人很是頭痛的問題。本文將會對死鎖進行相應介紹,對常見的死鎖案例進行相關分析與探討,以及如何去儘量避免死鎖給出一些建議。mysql

--什麼是死鎖 --

死鎖是併發系統中常見的問題,一樣也會出如今數據庫MySQL的併發讀寫請求場景中。當兩個及以上的事務,雙方都在等待對方釋放已經持有的鎖或由於加鎖順序不一致形成循環等待鎖資源,就會出現「死鎖」。常見的報錯信息爲 」 Deadlock found when trying to get lock... 」。面試

舉例來講 A 事務持有 X1 鎖 ,申請 X2 鎖,B事務持有 X2 鎖,申請 X1 鎖。A 和 B 事務持有鎖而且申請對方持有的鎖進入循環等待,就形成了死鎖。sql

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

如上圖,是右側的四輛汽車資源請求產生了迴路現象,即死循環,致使了死鎖。數據庫

從死鎖的定義來看,MySQL 出現死鎖的幾個要素爲:併發

a.兩個或者兩個以上事務ide

b.每一個事務都已經持有鎖而且申請新的鎖工具

c.鎖資源同時只能被同一個事務持有或者不兼容學習

d.事務之間由於持有鎖和申請鎖致使彼此循環等待測試

說明:後續內容實驗環境爲 5.7 版本,隔離級別爲 RR(可重複讀)優化

-- InnoDB 鎖類型--

爲了分析死鎖,咱們有必要對 InnoDB 的鎖類型有一個瞭解。

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

MySQL InnoDB 引擎實現了標準的行級別鎖:共享鎖( S lock ) 和排他鎖 ( X lock )

  • 不一樣事務能夠同時對同一行記錄加 S 鎖。
  • 若是一個事務對某一行記錄加 X 鎖,其餘事務就不能加 S 鎖或者 X 鎖,從而致使鎖等待。

若是事務 T1 持有行 r 的 S 鎖,那麼另外一個事務 T2 請求 r 的鎖時,會作以下處理:

  • T2 請求 S 鎖當即被容許,結果 T1 T2 都持有 r 行的 S 鎖
  • T2 請求 X 鎖不能被當即容許

若是 T1 持有 r 的 X 鎖,那麼 T2 請求 r 的 X、S 鎖都不能被當即容許,T2 必須等待 T1 釋放 X 鎖才能夠,由於 X 鎖與任何的鎖都不兼容。共享鎖和排他鎖的兼容性以下所示:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

間隙鎖( gap lock )

間隙鎖鎖住一個間隙以防止插入。假設索引列有2, 4, 8 三個值,若是對 4 加鎖,那麼也會同時對(2,4)和(4,8)這兩個間隙加鎖。其餘事務沒法插入索引值在這兩個間隙之間的記錄。可是,間隙鎖有個例外:

  • 若是索引列是惟一索引,那麼只會鎖住這條記錄(只加行鎖),而不會鎖住間隙。
  • 對於聯合索引且是惟一索引,若是 where 條件只包括聯合索引的一部分,那麼依然會加間隙鎖。

next-key lock

next-key lock 實際上就是 行鎖+這條記錄前面的 gap lock 的組合。假設有索引值10,11,13和 20,那麼可能的 next-key lock 包括:

(負無窮,10]

(10,11]

(11,13]

(13,20]

(20,正無窮)

在 RR 隔離級別下,InnoDB 使用 next-key lock 主要是防止幻讀問題產生。

意向鎖( Intention lock )

InnoDB 爲了支持多粒度的加鎖,容許行鎖和表鎖同時存在。爲了支持在不一樣粒度上的加鎖操做,InnoDB 支持了額外的一種鎖方式,稱之爲意向鎖( Intention Lock )。意向鎖是將鎖定的對象分爲多個層次,意向鎖意味着事務但願在更細粒度上進行加鎖。意向鎖分爲兩種:

  • 意向共享鎖( IS ):事務有意向對錶中的某些行加共享鎖
  • 意向排他鎖( IX ):事務有意向對錶中的某些行加排他鎖

因爲 InnoDB 存儲引擎支持的是行級別的鎖,所以意向鎖其實不會阻塞除全表掃描之外的任何請求。表級意向鎖與行級鎖的兼容性以下所示:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

插入意向鎖( Insert Intention lock )

插入意向鎖是在插入一行記錄操做以前設置的一種間隙鎖,這個鎖釋放了一種插入方式的信號,即多個事務在相同的索引間隙插入時若是不是插入間隙中相同的位置就不須要互相等待。假設某列有索引值2,6,只要兩個事務插入位置不一樣(如事務 A 插入3,事務 B 插入4),那麼就能夠同時插入。

鎖模式兼容矩陣

橫向是已持有鎖,縱向是正在請求的鎖:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

--閱讀死鎖日誌--

一個舒適小提示: xmen 平臺支持查看死鎖主庫的死鎖日誌,訪問方式以下:

登陸 https://xmen.intra.ke.com/#/mysql/mysql-cluster 點擊集羣管理 -> 輸入集羣端口 -> 工具集合 -> 查看死鎖日誌 點擊查詢便可查看最近一次死鎖日誌。

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

在進行具體案例分析以前,我們先了解下如何去讀懂死鎖日誌,儘量地使用死鎖日誌裏面的信息來幫助咱們來解決死鎖問題。

後面測試用例的數據庫場景以下:

MySQL 5.7 事務隔離級別爲 RR

表結構和數據以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

測試用例以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

經過執行show engine innodb status 能夠查看到最近一次死鎖的日誌。

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

日誌分析以下:

*** (1) TRANSACTION:

TRANSACTION 2322, ACTIVE 6 sec starting index read

事務號爲2322,活躍 6秒,starting index read 表示事務狀態爲根據索引讀取數據。常見的其餘狀態有:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

mysql tables in use 1 說明當前的事務使用一個表。

locked 1 表示表上有一個表鎖,對於 DML 語句爲 LOCK_IX

LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)

LOCK WAIT 表示正在等待鎖,2 lock struct(s) 表示 trx->trx_locks 鎖鏈表的長度爲2,每一個鏈表節點表明該事務持有的一個鎖結構,包括表鎖,記錄鎖以及自增鎖等。本用例中 2locks 表示 IX 鎖和lock_mode X (Next-key lock)

1 row lock(s) 表示當前事務持有的行記錄鎖/ gap 鎖的個數。

MySQL thread id 37, OS thread handle 140445500716800, query id 1234 127.0.0.1 root updating

MySQL thread id 37 表示執行該事務的線程 ID 爲 37 (即 show processlist; 展現的 ID )

delete from student where stuno=5 表示事務1正在執行的 sql,比較難受的事情是 show engine innodb status 是查看不到完整的 sql 的,一般顯示當前正在等待鎖的 sql。

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table cw.student trx id 2322 lock_mode X waiting

RECORD LOCKS 表示記錄鎖, 此條內容表示事務 1 正在等待表 student 上的 idx_stuno 的 X 鎖,本案例中實際上是 Next-Key Lock 。

事務2的 log 和上面分析相似:

*** (2) HOLDS THE LOCK(S):

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table cw.student trx id 2321 lock_mode X

顯示事務 2 的 insert into student(stuno,score) values(2,10) 持有了 a=5 的 Lock mode X

| LOCK_gap,不過咱們從日誌裏面看不到事務2執行的 delete from student where stuno=5;

這點也是形成 DBA 僅僅根據日誌難以分析死鎖的問題的根本緣由。

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table cw.student trx id 2321 lock_mode X locks gap before rec insert intention waiting

表示事務 2 的 insert 語句正在等待插入意向鎖 lock_mode X locks gap before rec insert intention waiting ( LOCK_X + LOCK_REC_gap )

--經典案例分析--

案例一:併發申請 gap 鎖致使死鎖

表結構和數據以下所示:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

測試用例以下(本測試用例場景是兩個事務刪除不存在的行,而後再 insert 記錄):

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖日誌以下所示:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖日誌分析以下:

重點說明下 delete 不存在的記錄是要加上 gap 鎖, 事務日誌中顯示lock_mode X locks gap before rec .

  1. T2:delete from t4 where kdt_id=15 and admin_id= 1 and biz='retail' and role_id=1; 符合條件的

記錄不存在,致使T2先持有了( lock_mode X locks gap before rec ) 鎖住

[(2,20,1,1,’ratail’,1,0)-(3,30,1,’retail’,1,0)]的區間,防止符合條件的記錄插入。

  1. T1 的 delete 與 T1 的 delete 同樣一樣申請了( lock_mode X locks gap but rec ) 鎖住了

[(2,20,1,'retail',1,0)-(3,30,1,'retail',1,0)]的區間。

  1. T1 的 insert 語句申請插入意向鎖,可是插入意向鎖和 T2 持有的 X gap ( lock_mode X locks gap before rec ) 衝突,故等待 T2 中的 gap 鎖釋放。

  2. T2 的 insert 語句申請插入意向鎖,可是插入意向鎖和 T1 持有 X gap (lock_mode X locks gap before rec )衝突,故等待T1中的 gap 鎖釋放。

總結來講,就是 T1 (insert) 等待 T2 (delete) , T2 (insert) 等待 T1 (delete) 故而循環等待,出現死鎖。

案例二:事務併發 insert 惟一鍵衝突

表結構和數據以下所示:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

測試用例以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖日誌以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

日誌分析以下:

1.事務 T2 insert into t7(id,a) values (26,10) 語句 insert 成功,持有 a=10 的 排他行鎖( X

locks rec but no gap )

2.事務 T1 insert into t7(id,a) values (30,10), 由於T2的第一條 insert 已經插入 a=10 的記錄,

事務 T1 insert a=10 則發生惟一鍵衝突,須要申請對衝突的惟一索引加上S Next-key Lock

( 即 lock mode S waiting ) 這是一個間隙鎖會申請鎖住(,10],(10,20]之間的 gap 區域。

3.事務 T2 insert into t7(id,a) values (40,9)該語句插入的 a=9 的值在事務 T1 申請的 gap 鎖

[4,10]之間, 故需事務 T2 的第二條 insert 語句要等待事務 T1 的 S-Next-key Lock 鎖釋放,

在日誌中顯示 lock_mode X locks gap before rec insert intention waiting 。

案例三:普通索引和主鍵相互競爭致使循環等待

表結構和數據以下所示:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

測試用例以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖日誌:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖日誌分析:

首先要理解的是 對同一個字段申請加鎖是須要排隊的。

其次表tx中索引 idx_c1 爲非惟一普通索引。

(1). T2 執行 select for update 操做持有記錄 id=30 的主鍵行鎖:PRIMARY of table test.tx trx id 2077 lock_mode X locks rec but not gap。

(2). T1 語句 update 經過普通索引 idx_c1 更新 c2,先獲取 idx_c1 c1=5 的 X 鎖 lock_mode X locks rec but not gap, 而後去申請對應主鍵 id=30 的行鎖, 可是 T2 已經持有主鍵的行數,因而 T1 等待。

(3). T2 執行根據主鍵 id=30 刪除記錄,須要申請 id=30 的行鎖以及 c1=5 的索引行鎖。可是 T1 尚及持有該鎖, 故會出現 index idx_c1 of table test.tx trx id 2077 lock_mode X locks rec but not gap waiting .

T2(delete) 等待 T1(update), T1(update) 等待 T2 (select for update)循環等待,形成死鎖。

案例四:先 update 再 insert 的併發死鎖問題

表結構以下,無數據:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

測試用例以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖日誌以下:

搜狐三面:說說你是怎麼解決MySQL死鎖問題的!

死鎖分析:

能夠看到兩個事務 update 不存在的記錄,前後得到間隙鎖( gap 鎖),gap 鎖之間是兼容的因此在update環節不會阻塞。二者都持有 gap 鎖,而後去競爭插入意向鎖。當存在其餘會話持有 gap 鎖的時候,當前會話申請不了插入意向鎖,致使死鎖。

今日讀者福利:關注公衆號:麒麟改bug,便可領取一份阿里內部Java學習筆記+金三銀四面試真題分享【附答案解析】

--如何儘量避免死鎖--

1.合理的設計索引,區分度高的列放到組合索引前面,使業務 SQL 儘量經過索引定位更少的行,減小

鎖競爭。

2.調整業務邏輯 SQL 執行順序, 避免 update/delete 長時間持有鎖的 SQL 在事務前面。

3.避免大事務,儘可能將大事務拆成多個小事務來處理,小事務發生鎖衝突的概率也更小。

4.以固定的順序訪問表和行。好比兩個更新數據的事務,事務 A 更新數據的順序爲 1,2;事

務 B 更新數據的順序爲 2,1。這樣更可能會形成死鎖。

5.在併發比較高的系統中,不要顯式加鎖,特別是是在事務裏顯式加鎖。如 select … for

update 語句,若是是在事務裏(運行了 start transaction 或設置了autocommit 等於0),

那麼就會鎖定所查找到的記錄。

6.儘可能按主鍵/索引去查找記錄,範圍查找增長了鎖衝突的可能性,也不要利用數據庫作一些

額外額度計算工做。好比有的程序會用到 「select … where … order by rand();」

這樣的語句,因爲相似這樣的語句用不到索引,所以將致使整個表的數據都被鎖住。

7.優化 SQL 和表設計,減小同時佔用太多資源的狀況。好比說,減小鏈接的表,將複雜 SQL

分解爲多個簡單的 SQL。

相關文章
相關標籤/搜索