解決死鎖之路(終結篇)- 再見死鎖



1、開啓鎖監控
mysql

在遇到線上死鎖問題時,咱們應該第一時間獲取相關的死鎖日誌。咱們能夠經過 show engine innodb status 命令來獲取死鎖信息,可是它有個限制,只能拿到最近一次的死鎖日誌。MySQL 提供了一套 InnoDb 的監控機制,用於週期性(每隔 15 秒)輸出 InnoDb 的運行狀態到 mysqld 服務的標準錯誤輸出(stderr)。默認狀況下監控是關閉的,只有當須要分析問題時再開啓,而且在分析問題以後,建議將監控關閉,由於它對數據庫的性能有必定影響,另外每 15 秒輸出一第二天志,會使日誌文件變得特別大。web

InnoDb 的監控主要分爲四種:標準監控(Standard InnoDB Monitor)、鎖監控(InnoDB Lock Monitor)、表空間監控(InnoDB Tablespace Monitor)和表監控(InnoDB Table Monitor)。後兩種監控已經基本上廢棄了,關於各類監控的做用能夠參考 MySQL 的官方文檔 Enabling InnoDB Monitors 或者 這篇文章。sql

要獲取死鎖日誌,咱們須要開啓 InnoDb 的標準監控,我推薦將鎖監控也打開,它能夠提供一些額外的鎖信息,在分析死鎖問題時會頗有用。開啓監控的方法有兩種:數據庫

1. 基於系統表

MySQL 使用了幾個特殊的表名來做爲監控的開關,好比在數據庫中建立一個表名爲 innodb_monitor 的表開啓標準監控,建立一個表名爲 innodb_lock_monitor 的表開啓鎖監控。MySQL 經過檢測是否存在這個表名來決定是否開啓監控,至於表的結構和表裏的內容無所謂。相反的,若是要關閉監控,則將這兩個表刪除便可。這種方法有點奇怪,在 5.6.16 版本以後,推薦使用系統參數的形式開啓監控。微信

1網絡

2併發

3app

4框架

5工具

6

7

8

9

10

11

-- 開啓標準監控

CREATE TABLE innodb_monitor (a INT) ENGINE=INNODB;

 

-- 關閉標準監控

DROP TABLE innodb_monitor;

 

-- 開啓鎖監控

CREATE TABLE innodb_lock_monitor (a INT) ENGINE=INNODB;

 

-- 關閉鎖監控

DROP TABLE innodb_lock_monitor;

2. 基於系統參數

在 MySQL 5.6.16 以後,能夠經過設置系統參數來開啓鎖監控,以下:

1

2

3

4

5

6

7

8

9

10

11

-- 開啓標準監控

set GLOBAL innodb_status_output=ON;

 

-- 關閉標準監控

set GLOBAL innodb_status_output=OFF;

 

-- 開啓鎖監控

set GLOBAL innodb_status_output_locks=ON;

 

-- 關閉鎖監控

set GLOBAL innodb_status_output_locks=OFF;

另外,MySQL 提供了一個系統參數 innodb_print_all_deadlocks 專門用於記錄死鎖日誌,當發生死鎖時,死鎖日誌會記錄到 MySQL 的錯誤日誌文件中。

1

set GLOBAL innodb_print_all_deadlocks=ON;

除了 MySQL 自帶的監控機制,還有一些有趣的監控工具也頗有用,好比 Innotop 和 Percona Toolkit 裏的小工具 pt-deadlock-logger。

2、讀懂死鎖日誌

一切準備就緒以後,咱們從 DBA 那裏拿到了死鎖日誌(其中的SQL語句作了省略):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

------------------------

LATEST DETECTED DEADLOCK

------------------------

2017-09-06 11:58:16 7ff35f5dd700

*** (1) TRANSACTION:

TRANSACTION 182335752, ACTIVE 0 sec inserting

mysql tables in use 1, locked 1

LOCK WAIT 11 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 15

MySQL thread id 12032077, OS thread handle 0x7ff35ebf6700, query id 196418265 10.40.191.57 RW_bok_db update

INSERT INTO bok_task

                 ( order_id ...

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

RECORD LOCKS space id 300 page no 5480 n bits 552 index `order_id_un` of table `bok_db`.`bok_task`

    trx id 182335752 lock_mode X insert intention waiting

*** (2) TRANSACTION:

TRANSACTION 182335756, ACTIVE 0 sec inserting

mysql tables in use 1, locked 1

11 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 15

MySQL thread id 12032049, OS thread handle 0x7ff35f5dd700, query id 196418268 10.40.189.132 RW_bok_db update

INSERT INTO bok_task

                 ( order_id ...

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

RECORD LOCKS space id 300 page no 5480 n bits 552 index `order_id_un` of table `bok_db`.`bok_task`

    trx id 182335756 lock_mode X

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

RECORD LOCKS space id 300 page no 5480 n bits 552 index `order_id_un` of table `bok_db`.`bok_task`

    trx id 182335756 lock_mode X insert intention waiting

*** WE ROLL BACK TRANSACTION (2)

日誌中列出了死鎖發生的時間,以及致使死鎖的事務信息(只顯示兩個事務,若是由多個事務致使的死鎖也只顯示兩個),並顯示出每一個事務正在執行的 SQL 語句、等待的鎖以及持有的鎖信息等。下面咱們就來研究下這份死鎖日誌,看看從這份死鎖日誌中能不能發現死鎖的緣由?

首先看事務一的信息:

* (1) TRANSACTION:
TRANSACTION 182335752, ACTIVE 0 sec inserting

ACTIVE 0 sec 表示事務活動時間,inserting 爲事務當前正在運行的狀態,可能的事務狀態有:fetching rows,updating,deleting,inserting 等。

mysql tables in use 1, locked 1
LOCK WAIT 11 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 15

tables in use 1 表示有一個表被使用,locked 1 表示有一個表鎖。LOCK WAIT 表示事務正在等待鎖,11 lock struct(s) 表示該事務的鎖鏈表的長度爲 11,每一個鏈表節點表明該事務持有的一個鎖結構,包括表鎖,記錄鎖以及 autoinc 鎖等。heap size 1184 爲事務分配的鎖堆內存大小。
2 row lock(s) 表示當前事務持有的行鎖個數,經過遍歷上面提到的 11 個鎖結構,找出其中類型爲 LOCK_REC 的記錄數。undo log entries 15 表示當前事務有 15 個 undo log 記錄,由於二級索引不記 undo log,說明該事務已經更新了 15 條彙集索引記錄。

MySQL thread id 12032077, OS thread handle 0x7ff35ebf6700, query id 196418265 10.40.191.57 RW_bok_db update

事務的線程信息,以及數據庫 IP 地址和數據庫名,對咱們分析死鎖用處不大。

INSERT INTO bok_task

1

( order_id ...

這裏顯示的是正在等待鎖的 SQL 語句,死鎖日誌裏每一個事務都只顯示一條 SQL 語句,這對咱們分析死鎖很不方便,咱們必需要結合應用程序去具體分析這個 SQL 以前還執行了哪些其餘的 SQL 語句,或者根據 binlog 也能夠大體找到一個事務執行的 SQL 語句。

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

RECORD LOCKS space id 300 page no 5480 n bits 552 index `order_id_un` of table `bok_db`.`bok_task` trx id 182335752 lock_mode X insert intention waiting

這裏顯示的是事務正在等待什麼鎖。RECORD LOCKS 表示記錄鎖(而且能夠看出要加鎖的索引爲 order_id_un),space id 爲 300,page no 爲 5480,n bits 552 表示這個記錄鎖結構上留有 552 個 bit 位(該 page 上的記錄數 + 64)。
lock_mode X 表示該記錄鎖爲排他鎖,insert intention waiting 表示要加的鎖爲插入意向鎖,並處於鎖等待狀態。

在上面有提到 innodb_status_output_locks 這個系統變量能夠開啓 InnoDb 的鎖監控,若是開啓了,這個地方還會顯示出鎖的一些額外信息,包括索引記錄的 info bits 和數據信息等:

1

2

3

Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

 0: len 4; hex 80000002; asc     ;;

 1: len 4; hex 80000001; asc     ;;

在 《瞭解常見的鎖類型》 中咱們說過,一共有四種類型的行鎖:記錄鎖,間隙鎖,Next-key 鎖和插入意向鎖。這四種鎖對應的死鎖日誌各不相同,以下:

  • 記錄鎖(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap

  • 間隙鎖(LOCK_GAP): lock_mode X locks gap before rec

  • Next-key 鎖(LOCK_ORNIDARY): lock_mode X

  • 插入意向鎖(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention

這裏有一點要注意的是,並非在日誌裏看到 lock_mode X 就認爲這是 Next-key 鎖,由於還有一個例外:若是在 supremum record 上加鎖,locks gap before rec 會省略掉,間隙鎖會顯示成 lock_mode X,插入意向鎖會顯示成 lock_mode X insert intention。譬以下面這個:

1

2

RECORD LOCKS space id 0 page no 307 n bits 72 index `PRIMARY` of table `test`.`test` trx id 50F lock_mode X

Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0

看起來像是 Next-key 鎖,可是看下面的 heap no 1 表示這個記錄是 supremum record(另外,infimum record 的 heap no 爲 0),因此這個鎖應該看做是一個間隙鎖。

看完第一個事務,再來看看第二個事務:

* (2) TRANSACTION:

TRANSACTION 182335756, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
11 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 15
MySQL thread id 12032049, OS thread handle 0x7ff35f5dd700, query id 196418268 10.40.189.132 RW_bok_db update
INSERT INTO bok_task

1

( order_id ...

* (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 300 page no 5480 n bits 552 index `order_id_un` of table `bok_db`.`bok_task` trx id 182335756 lock_mode X
* (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 300 page no 5480 n bits 552 index `order_id_un` of table `bok_db`.`bok_task` trx id 182335756 lock_mode X insert intention waiting

事務二和事務一的日誌基本相似,不過它多了一部分 HOLDS THE LOCK(S),表示事務二持有什麼鎖,這個鎖每每就是事務一處於鎖等待的緣由。這裏能夠看到事務二正在等待索引 order_id_un 上的插入意向鎖,而且它已經持有了一個 X 鎖(Next-key 鎖,也有多是 supremum 上的間隙鎖)。

到這裏爲止,咱們獲得了不少關鍵信息,此時咱們能夠逆推出死鎖發生的緣由嗎?這可能也是每一個開發人員和 DBA 最關心的問題,如何經過死鎖日誌來診斷死鎖的成因?實際上這是很是困難的。

若是每一個事務都只有一條 SQL 語句,這種狀況的死鎖成因還算比較好分析,由於咱們能夠從死鎖日誌裏找到每一個事務執行的 SQL 語句,只要對這兩條 SQL 語句的加鎖過程有必定的瞭解,死鎖緣由通常不難定位。但也有可能死鎖的成因很是隱蔽,這時須要咱們對這兩條 SQL 語句的加鎖流程作很是深刻的研究纔有可能分析出死鎖的根源。

不過大多數狀況下,每一個事務都不止一條 SQL 語句,譬如上面的死鎖日誌裏顯示的 undo log entries 15,說明執行 INSERT 語句以前確定還執行了其餘的 SQL 語句,可是具體是什麼,咱們不得而知,咱們只能根據 HOLDS THE LOCK(S) 部分知道有某個 SQL 語句對 order_id_un 索引加了 Next-key 鎖(或間隙鎖)。另外事務二在 WAITING FOR 插入意向鎖,至於它和事務一的哪一個鎖衝突也不得而知,由於事務一的死鎖日誌裏並無 HOLDS THE LOCK(S) 部分。

因此,對死鎖的診斷不能僅僅靠死鎖日誌,還應該結合應用程序的代碼來進行分析,若是實在接觸不到應用代碼,還能夠經過數據庫的 binlog 來分析(只要你的死鎖不是 100% 必現,那麼 binlog 日誌裏確定能找到一份完整的事務一和事務二的 SQL 語句)。經過應用代碼或 binlog 理出每一個事務的 SQL 執行順序,這樣分析死鎖時就會容易不少。

3、常見死鎖分析

儘管上面說經過死鎖日誌來推斷死鎖緣由很是困難,但我想也不是徹底不可能。我在 Github 上新建了一個項目 mysql-deadlocks,這個項目收集了一些常見的 MySQL 死鎖案例,大多數案例都來源於網絡,並對它們進行分類彙總,試圖經過死鎖日誌分析出每種死鎖的緣由,還原出死鎖現場。這雖然有點癡人說夢的感受,但仍是但願能給後面的開發人員在定位死鎖問題時帶來一些便利。

我將這些死鎖按事務執行的語句和正在等待或已持有的鎖進行分類彙總(目前已經收集了十餘種死鎖場景):

表中的語句雖然只列出了 delete 和 insert,但實際上絕大多數的 delete 語句和 update 或 select ... for update 加鎖機制是同樣的,因此爲了不重複,對於 update 語句就不在一塊兒彙總了(固然也有例外,譬如使用 update 對索引進行更新時加鎖機制和 delete 是有區別的,這種狀況我會單獨列出)。

對每個死鎖場景,我都會定義一個死鎖名稱(實際上就是事務等待和持有的鎖),每一篇分析,我都分紅了 死鎖特徵、死鎖日誌、表結構、重現步驟、分析和參考 這幾個部分。

對於這種分類方法我感受並非很好,但也想不出什麼其餘更好的方案,若是你有更好的建議,歡迎討論。另外,若是你有新的死鎖案例,或者對某個死鎖的解釋有異議,歡迎 給我提 Issue 或 PR。

下面咱們介紹幾種常見的死鎖場景,仍是之前面提到的 students 表爲例:

其中,id 爲主鍵,no(學號)爲二級惟一索引,name(姓名)和 age(年齡)爲二級非惟一索引,score(學分)無索引。數據庫隔離級別爲 RR。

3.1 死鎖案例一

死鎖的根本緣由是有兩個或多個事務之間加鎖順序的不一致致使的,這個死鎖案例實際上是最經典的死鎖場景。

首先,事務 A 獲取 id = 20 的鎖(lock_mode X locks rec but not gap),事務 B 獲取 id = 30 的鎖;而後,事務 A 試圖獲取 id = 30 的鎖,而該鎖已經被事務 B 持有,因此事務 A 等待事務 B 釋放該鎖,而後事務 B 又試圖獲取 id = 20 的鎖,這個鎖被事務 A 佔有,因而兩個事務之間相互等待,致使死鎖。

3.2 死鎖案例二

首先事務 A 和事務 B 執行了兩條 UPDATE 語句,可是因爲 id = 25 和 id = 26 記錄都不存在,事務 A 和 事務 B 並無更新任何記錄,可是因爲數據庫隔離級別爲 RR,因此會在 (20, 30) 之間加上間隙鎖(lock_mode X locks gap before rec),間隙鎖和間隙鎖並不衝突。以後事務 A 和事務 B 分別執行 INSERT 語句要插入記錄 id = 25 和 id = 26,須要在 (20, 30) 之間加插入意向鎖(lock_mode X locks gap before rec insert intention),插入意向鎖和間隙鎖衝突,因此兩個事務互相等待,最後造成死鎖。

要解決這個死鎖很簡單,顯然,前面兩條 UPDATE 語句是無效的,將其刪除便可。另外也能夠將數據庫隔離級別改爲 RC,這樣在 UPDATE 的時候就不會有間隙鎖了。這個案例正是文章開頭提到的死鎖日誌中的死鎖場景,別看這個 UPDATE 語句是無效的,看起來很傻,可是確實是真實的場景,由於在真實的項目中代碼會很是複雜,好比採用了 ORM 框架,應用層和數據層代碼分離,通常開發人員寫代碼時都不知道會生成什麼樣的 SQL 語句,我也是從 DBA 那裏拿到了 binlog,而後從裏面找到了事務執行的全部 SQL 語句,發現其中居然有一行無效的 UPDATE 語句,最後追本溯源,找到對應的應用代碼,將其刪除,從而修復了這個死鎖。

3.3 死鎖案例三

別看這個案例裏每一個事務都只有一條 SQL 語句,可是卻實實在在可能會致使死鎖問題,其實提及來,這個死鎖和案例一併無什麼區別,只不過理解起來要更深刻一點。要知道在範圍查詢時,加鎖是一條記錄一條記錄挨個加鎖的,因此雖然只有一條 SQL 語句,若是兩條 SQL 語句的加鎖順序不同,也會致使死鎖。

在案例一中,事務 A 的加鎖順序爲:id = 20 -> 30,事務 B 的加鎖順序爲:id = 30 -> 20,正好相反,因此會致使死鎖。這裏的情景也是同樣,事務 A 的範圍條件爲 id < 30,加鎖順序爲:id = 15 -> 18 -> 20,事務 B 走的是二級索引 age,加鎖順序爲:(age, id) = (24, 18) -> (24, 20) -> (25, 15) -> (25, 49),其中,對 id 的加鎖順序爲 id = 18 -> 20 -> 15 -> 49。能夠看到事務 A 先鎖 15,再鎖 18,而事務 B 先鎖 18,再鎖 15,從而造成死鎖。

3.4 如何避免死鎖

在工做過程當中偶爾會遇到死鎖問題,雖然這種問題遇到的機率不大,但每次遇到的時候要想完全弄懂其原理並找到解決方案卻並不容易。其實,對於 MySQL 的 InnoDb 存儲引擎來講,死鎖問題是避免不了的,沒有哪一種解決方案能夠說徹底解決死鎖問題,可是咱們能夠經過一些可控的手段,下降出現死鎖的機率。

  1. 如上面的案例一和案例三所示,對索引加鎖順序的不一致極可能會致使死鎖,因此若是能夠,儘可能以相同的順序來訪問索引記錄和表。在程序以批量方式處理數據的時候,若是事先對數據排序,保證每一個線程按固定的順序來處理記錄,也能夠大大下降出現死鎖的可能;

  2. 如上面的案例二所示,Gap 鎖每每是程序中致使死鎖的真兇,因爲默認狀況下 MySQL 的隔離級別是 RR,因此若是能肯定幻讀和不可重複讀對應用的影響不大,能夠考慮將隔離級別改爲 RC,能夠避免 Gap 鎖致使的死鎖;

  3. 爲表添加合理的索引,若是不走索引將會爲表的每一行記錄加鎖,死鎖的機率就會大大增大;

  4. 咱們知道 MyISAM 只支持表鎖,它採用一次封鎖技術來保證事務之間不會發生死鎖,因此,咱們也可使用一樣的思想,在事務中一次鎖定所須要的全部資源,減小死鎖機率;

  5. 避免大事務,儘可能將大事務拆成多個小事務來處理;由於大事務佔用資源多,耗時長,與其餘事務衝突的機率也會變高;

  6. 避免在同一時間點運行多個對同一表進行讀寫的腳本,特別注意加鎖且操做數據量比較大的語句;咱們常常會有一些定時腳本,避免它們在同一時間點運行;

  7. 設置鎖等待超時參數:innodb_lock_wait_timeout,這個參數並非只用來解決死鎖問題,在併發訪問比較高的狀況下,若是大量事務因沒法當即得到所需的鎖而掛起,會佔用大量計算機資源,形成嚴重性能問題,甚至拖跨數據庫。咱們經過設置合適的鎖等待超時閾值,能夠避免這種狀況發生。

總結

一開始是去年 9 月份的時候,線上某個系統遇到了一個死鎖問題,當時對這個死鎖百思不得其解,慢慢的從困惑到感興趣,雖然那時花了大概一個禮拜的時間研究後就已經把這個死鎖問題解決了,可是對死鎖的執念卻一直沒有放下,開始翻閱大量的文檔和資料,看 MySQL 官方文檔,買 MySQL 書籍,甚至去讀 MySQL 源碼,從事務、隔離級別、索引一直看到加鎖機制、死鎖分析等等。再到後來,忽然以爲沒意思想放棄,感受就算知道了什麼語句加什麼鎖也沒有多大意義,這個都是死記硬背的東西,花時間把這些規則背下來沒什麼價值,還不如須要的時候本身去實驗一把也就知道了。中間歇了有兩個多月時間,後來想一想不能半途而廢,逼本身必定要把這個系列寫完,最後一篇死鎖問題的分析確定須要收集而後對這些死鎖問題進行大量的分析,因而去網上找了各類各樣的死鎖日誌,而後一次一次的作實驗,這不收集還好,一收集便停不下來,這應該就是死鎖收藏癖吧。

對死鎖的研究前先後後燒了很多的腦細胞,特別是後期收集死鎖日誌的時候,才發現死鎖場景各式各樣,有些死的很荒謬,有些死的很精妙,還有些死的不明不白,直到如今我還沒搞懂爲何。全部的這些死鎖案例都收集在 這裏,若是你感興趣,歡迎來和我一塊兒添磚加瓦,但願有一天,真的能夠和死鎖說再見。




物流IT圈 



泛物流行業IT知識分享傳播、從業人士互幫互助,覆蓋快遞快運/互聯網物流平臺/城配/即時配送/3PL/倉配/貨代/冷鏈/物流軟件公司/物流裝備/物流自動化設備/物流機器人等細分行業。長按二維碼即刻加入咱們,若是你是以上行業公司中的IT從業人士加運營小哥微信後可入羣交流。

              公衆號              

運營小哥


本文分享自微信公衆號 - 物流IT圈(exiter18)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索