一、MySQL InnoDB死鎖闡述
在MySQL中,當兩個或以上的事務相互持有和請求鎖,並形成一個循環的依賴關系,就會產生死鎖。多個事務同時鎖定同一個資源時,也會產生死鎖。在一個事務系統中,死鎖是確切存在並且是不能完全避免的。 InnoDB會自動檢測事務死鎖,立即回滾其中某個事務,並且返回一個錯誤。它根據某種機制來選擇那個最簡單(代價最小)的事務來進行回滾。偶然發生的死鎖不必擔心,但死鎖頻繁出現的時候就要引起注意了。InnoDB存儲引擎有一個后台的鎖監控線程,該線程負責查看可能的死鎖問題,並自動告知用戶。
在MySQL 5.6之前,只有最新的死鎖信息可以使用show engine innodb status命令來進行查看。使用Percona Toolkit工具包中的pt-deadlock-logger可以從show engine innodb status的結果中得到指定的時間范圍內的死鎖信息,同時寫入文件或者表中,等待后面的診斷分析。對於pt-deadlock-logger工具的更多信息可以參考手冊。 如果使用的是MySQL 5.6或以上版本,您可以啟用一個新增的參數innodb_print_all_deadlocks把InnoDB中發生的所有死鎖信息都記錄在錯誤日志里面。
產生死鎖的必要條件
1. 多個並發事務(2個或者以上);
2. 每個事務都持有鎖(或者是已經在等待鎖);
3. 每個事務都需要再繼續持有鎖(為了完成事務邏輯,還必須更新更多的行);
4. 事務之間產生加鎖的循環等待,形成死鎖。
總結:當兩個或多個事務相互持有對方需要的鎖時,就會產生死鎖,如下圖:
死鎖實例
創建環境
create table money(id int primary key,price int); insert into money values(1,1000); insert into money values(2,1000);
事務A: 更新表,id=1的記錄
mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> update money set price=2000 where id=1; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0
事務B: 更新表,id=2的記錄
mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> update money set price=2000 where id=2; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
事務A: 更新表,id=2的記錄,此時會卡住(因為這條記錄被加上了X鎖)
mysql> update money set price=3000 where id=2;
事務B: 更新表,id=1的記錄,此時會報錯事務進行回滾,並且事務1會執行更新id=2的記錄
mysql> update money set price=3000 where id=1; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
上述,事務拋出1213這個出錯提示,即發生了死鎖,上例中當兩個事務都執行了第一條UPDATE語句,更新了一行數據,同時也鎖定了該行數據,接着每個事務都嘗試去執行第二條UPDATE語句,卻發現該行已經被對方鎖定,然后兩個事務都等待對方釋放鎖,同時又持有對方需要的鎖,則陷入死循環。除非有外部因素接入才可能解除死鎖。為了解決這種問題,數據庫系統實現了各種死鎖檢測和死鎖超時機制。
二、MySQL InnoDB死鎖檢測
1) 盡量不出現死鎖
在代碼層調整SQL操作順序,或者縮短事務長度,以避免出現死鎖。
2) 碰撞檢測
當死鎖出現時,Innodb會主動探知到死鎖,並回滾了某一苦苦等待的事務。問題來了,Innodb是怎么探知死鎖的?
核心就是數據庫會把事務單元鎖維持的鎖和它所等待的鎖都記錄下來,Innodb提供了wait-for graph算法來主動進行死鎖檢測,每當加鎖請求無法立即滿足需要進入等待時,wait-for graph算法都會被觸發。當數據庫檢測到兩個事務不同方向地給同一個資源加鎖(產生循序),它就認為發生了死鎖,觸發wait-for graph算法。比如,事務1給A加鎖,事務2給B加鎖,同時事務1給B加鎖(等待),事務2給A加鎖就發生了死鎖。那么死鎖解決辦法就是終止一邊事務的執行即可,這種效率一般來說是最高的,也是主流數據庫采用的辦法。
Innodb目前處理死鎖的方法就是將持有最少行級排他鎖的事務進行回滾。這也是相對比較簡單的死鎖回滾方式。死鎖發生以后,只有部分或者完全回滾其中一個事務,才能打破死鎖。對於事務型的系統,這是無法避免的,所以應用程序在設計必須考慮如何處理死鎖。大多數情況下只需要重新執行因死鎖回滾的事務即可。
wait-for graph原理
我們怎么知道圖中四輛車是死鎖的?
他們相互等待對方的資源,而且形成環路!我們將每輛車看為一個節點,當節點1需要等待節點2的資源時,就生成一條有向邊指向節點2,最后形成一個有向圖。我們只要檢測這個有向圖是否出現環路即可,出現環路就是死鎖!這就是wait-for graph算法。
Innodb將各個事務看為一個個節點,資源就是各個事務占用的鎖,當事務1需要等待事務2的鎖時,就生成一條有向邊從1指向2,最后行成一個有向圖。
3)等鎖超時
死鎖超時也是一種常見的做法,就是等待鎖持有時間,如果說一個事務持有鎖超過設置時間的話,就直接拋出一個錯誤,參數innodb_lock_wait_timeout用來設置超時時間。如果有些用戶使用哪種超長的事務,你就需要把鎖超時時間大於事務執行時間。在這種情況下這種死鎖超時的方式就會導致每一個死鎖超時被發現的時間是無法接受的。
不要太擔心死鎖,你可能會在MySQL error log中看到關於死鎖的警告信息,或者在show engine InnoDB status輸出中看到它。盡管看起來是一個可怕的名字,但deadlock不是一個嚴重的問題,對於InnoDB來說,通常不需要做任何糾正操作。當兩個事務開始修改多個表時,如果訪問表的順序不同,會出現互相等待對方釋放鎖,然后才能繼續處理的情況。MySQL會立刻發現這種情況並且終止較小的事務,允許其他的事務執行。
你的應用程序的確需要錯誤處理邏輯來重啟該事務。當你重新執行相同的SQL語句時,原來的時間問題不再適用:要么其他的事務已經執行完成,這樣你就可以執行事務了,要么其他的事務還在處理過程中,你的事務只能等它結束。
如果不斷警告發生死鎖,你可能要review你的應用程序源代碼,調整SQL操作順序,或者縮短事務長度。你可以啟用innodb_print_all_deadlocks選項,把deadlock信息記錄到MySQL的錯誤日志總,而不是僅僅通過show engine innob status查看。
鎖跟索引的關系
這時我們要注意到,money表雖然沒有添加索引,但是InnoDB存儲引擎會使用隱式的主鍵來進行鎖定。對於沒有索引或主鍵的表來說,那么MySQL會給整張表的所有數據行的加行鎖。這里聽起來有點不可思議,但是當sql運行的過程中,MySQL並不知道哪些數據行是id=1(沒有索引嘛),如果一個條件無法通過索引快速過濾,存儲引擎層面就會將所有記錄加鎖后返回,再由MySQL Server層進行過濾。但在實際使用過程當中,MySQL做了一些改進,在MySQL Server過濾條件,發現不滿足后,會調用unlock_row方法,把不滿足條件的記錄釋放鎖 (違背了二段鎖協議的約束)。這樣做,保證了最后只會持有滿足條件記錄上的鎖,但是每條記錄的加鎖操作還是不能省略的。可見即使是MySQL,為了效率也是會違反規范的。這種情況同樣適用於MySQL的默認隔離級別RC。所以對一個數據量很大的表做批量修改的時候,如果無法使用相應的索引,MySQL過濾數據的的時候特別慢,就會出現雖然沒有修改某些行的數據,但是它們還是被鎖住了的現象。
三、死鎖擴展—讀寫鎖與U鎖
再談讀寫鎖與U鎖,在數據庫中還有一種U鎖,U鎖非常簡單是用來解決讀寫鎖死鎖問題。當然在現代數據庫中讀寫鎖的死鎖問題不會出現,因為使用了U鎖機制。如下圖:
事務1與事務2第一條語句都是查詢A,然后事務1對A進行更新操作,但是由於事務2的讀鎖對A還沒有釋放,所以事務1要等待;如果此時事務2也要對A進行更新操作,由於事務1對A的讀鎖還沒有釋放,所以事務2要等待。此時發生了什么?就是死鎖。
那么上述情況對應到事務中哪種情況就會出現死鎖呢?看一下下面這條語句。
UPDATE table_name SET A=A-1 WHERE id=100;
這條語句中會先進行id=100的查詢,申請一個讀鎖;然后執行SET A=A-1操作,申請一個寫鎖。也就是對一條記錄進行讀寫鎖。如果有兩個人同時執行這條語句,第一個人執行id=100的查詢,第二個人也執行id=100的查詢,第一個人執行SET A=A-1操作,第二個人執行SET A=A-1操作,那么就產生死鎖。那么可以看到如果你使用讀寫鎖的話,碰到這種場景就會死鎖,那么基本沒法用了。
那么為什么我們數據庫里面不會出現這種情況呢?其實原因就是我們使用了U鎖,U鎖很簡單就是會提前判斷你這個事務中有沒有針對一個事務的寫操作,如果檢查到有寫鎖,那么它會提前在你申請鎖的時候把原來的讀鎖變成寫鎖。當鎖變成寫鎖之后其他的讀寫操作都無進來這個事務內了,也就避免了死鎖。
MySQL目前不支持U鎖,SQLserver是支持U鎖的。
四、開啟死鎖輸出日志
在MySQL中可以把死鎖信息打印到錯誤日志里,開啟如下變量即可。
mysql> set global innodb_print_all_deadlocks = 1;
然后可以再去測試一下死鎖,看看錯誤日志中會出現如下一條信息:
2016-12-16 20:16:30 7f126a1e4700InnoDB: transactions deadlock detected, dumping detailed information.
當然除了這么一條死鎖信息,還有更為詳細的SQL語句和死鎖信息和事務信息。
五、經常出現的死鎖案例
死鎖:當兩個事務都嘗試獲取其他事務已經持有的鎖時,就會出現死鎖。
創建實驗環境:
CREATE TABLE t1 (id int unsigned NOT NULL PRIMARY KEY, val varchar(10)) ENGINE=InnoDB; CREATE TABLE t2 LIKE t1; INSERT INTO db1.t1 VALUES (1, 'aa'), (2, 'ab'), (3, 'ac'), (4, 'ad'), (5, 'ae'), (6, 'af'); INSERT INTO db1.t2 VALUES (1, 'ba'), (2, 'bb'), (3, 'bc'), (4, 'bd'), (5, 'be'), (6, 'bf');
Example 1: Two Transactions Updating Two Records In Two Tables
transaction 1> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 1> UPDATE db1.t1 SET val = 'aa1' WHERE id = 1; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0 transaction 2> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 2> UPDATE db1.t2 SET val = 'ba2' WHERE id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -- Next statement will block waiting for the lock held by Connection 2: transaction 1> UPDATE db1.t2 SET val = 'ab1' WHERE id = 1; transaction 2> UPDATE db1.t1 SET val = 'aa2' WHERE id = 1; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Example 2: Two Transactions Updating Two Records In One Table
transaction 1> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 1> UPDATE db1.t1 SET val = 'aa1' WHERE id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 transaction 2> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 2> UPDATE db1.t1 SET val = 'ab2' WHERE id = 2; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -- Next statement will block waiting for the lock held by Connection 2: transaction 1> UPDATE db1.t1 SET val = 'ab1' WHERE id = 2; transaction 2> UPDATE db1.t1 SET val = 'aa2' WHERE id = 1; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Example 3: Two Transactions Deleting a Series of Records In One Table
transaction 1> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 1> DELETE FROM db1.t1 WHERE id = 1; Query OK, 1 row affected (0.00 sec) transaction 2> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 2> DELETE FROM db1.t1 WHERE id = 6; Query OK, 1 row affected (0.00 sec) transaction 1> DELETE FROM db1.t1 WHERE id = 2; Query OK, 1 row affected (0.00 sec) transaction 2> DELETE FROM db1.t1 WHERE id = 5; Query OK, 1 row affected (0.01 sec) transaction 1> DELETE FROM db1.t1 WHERE id = 3; Query OK, 1 row affected (0.00 sec) transaction 2> DELETE FROM db1.t1 WHERE id = 4; Query OK, 1 row affected (0.00 sec) -- Next statement will block waiting for the lock held by Connection 2: transaction 1> DELETE FROM db1.t1 WHERE id = 4; transaction 2> DELETE FROM db1.t1 WHERE id = 3; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Example 4: Deleting Non-Existing Rows(REPEATABLE READ isolation)
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; Query OK, 0 rows affected (0.00 sec) mysql> CREATE TABLE db1.t3 (id int unsigned NOT NULL PRIMARY KEY) ENGINE=InnoDB; Query OK, 0 rows affected (0.11 sec) mysql> INSERT INTO db1.t3 VALUES (1),(5); Query OK, 2 rows affected (0.02 sec) Records: 2 Duplicates: 0 Warnings: 0
兩個並發事務試圖刪除帶有主鍵的非現有的行,然后試圖插入行,可能會導致死鎖在可重復讀的事務隔離級別︰
transaction 1> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 1> DELETE FROM t1 WHERE id = 2; Query OK, 0 rows affected (0.00 sec) transaction 2> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) transaction 2> DELETE FROM t1 WHERE id = 4; Query OK, 0 rows affected (0.01 sec) -- Next statement will block waiting for the insert intention lock held by Connection 2: transaction 1> INSERT INTO t1 VALUES (2); -- At this stage information_schema.INNODB_LOCKS shows for the two transactions: mysql> SELECT * FROM information_schema.INNODB_LOCKS\G *************************** 1. row *************************** lock_id: 25205128:0:163881:3 lock_trx_id: 25205128 lock_mode: X,GAP lock_type: RECORD lock_table: `db1`.`t3` lock_index: `PRIMARY` lock_space: 0 lock_page: 163881 lock_rec: 3 lock_data: 5 *************************** 2. row *************************** lock_id: 25205129:0:163881:3 lock_trx_id: 25205129 lock_mode: X,GAP lock_type: RECORD lock_table: `db1`.`t3` lock_index: `PRIMARY` lock_space: 0 lock_page: 163881 lock_rec: 3 lock_data: 5 2 rows in set (0.00 sec) transaction 2> INSERT INTO t1 VALUES (4); ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
INNODB_LOCKS輸出具有lock_data = 5,顯示每個事務持有的gap lock。
在InnoDB死鎖信息如下︰
InnoDB: transactions deadlock detected, dumping detailed information. 1 161219 16:20:14 2 *** (1) TRANSACTION: 3 TRANSACTION 25205152, ACTIVE 34 sec inserting 4 mysql tables in use 1, locked 1 5 LOCK WAIT 3 lock struct(s), heap size 376, 2 row lock(s) 6 MySQL thread id 11034, OS thread handle 0x7f57d02a6700, query id 467882 localhost 127.0.0.1 root update 7 INSERT INTO db1.t3 VALUES (2) 8 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: 9 RECORD LOCKS space id 0 page no 163881 n bits 72 index `PRIMARY` of table `db1`.`t3` trx id 25205152 lock_mode X locks gap before rec insert intention waiting 10 Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 00000005; asc ;; 1: len 6; hex 00000180994c; asc L;; 2: len 7; hex e500001089011d; asc ;; 11 *** (2) TRANSACTION: 12 TRANSACTION 25205268, ACTIVE 28 sec inserting 13 mysql tables in use 1, locked 1 14 3 lock struct(s), heap size 376, 2 row lock(s) 15 MySQL thread id 11035, OS thread handle 0x7f57d02e7700, query id 467928 localhost 127.0.0.1 root update 16 INSERT INTO db1.t3 VALUES (4) 17 *** (2) HOLDS THE LOCK(S): 18 RECORD LOCKS space id 0 page no 163881 n bits 72 index `PRIMARY` of table `db1`.`t3` trx id 25205268 lock_mode X locks gap before rec 19 Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 00000005; asc ;; 1: len 6; hex 00000180994c; asc L;; 2: len 7; hex e500001089011d; asc ;; 20 *** (2) WAITING FOR THIS LOCK TO BE GRANTED: 21 RECORD LOCKS space id 0 page no 163881 n bits 72 index `PRIMARY` of table `db1`.`t3` trx id 25205268 lock_mode X locks gap before rec insert intention waiting 23 Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 00000005; asc ;; 1: len 6; hex 00000180994c; asc L;; 2: len 7; hex e500001089011d; asc ;; *** WE ROLL BACK TRANSACTION (2)
第 1 行是死鎖發生的時間。如果你的應用程序捕捉和記錄死鎖錯誤到日志中,那么你可以根據這個時間戳和應用程序日志中的死鎖錯誤的時間戳進行匹配。這樣你可以得到已回滾的事務,及事務中的所有語句。
第 3 和 12 行,注意事務的序號和活躍時間。如果你定期地把 show engine innodb status 的輸出信息記錄到日志文件(這是一個很好的做法),那么你就可以使用事務編號在之前的輸出日志中查到同一個事務中所希望看到的更多的語句。活躍時間提供了一個線索來判斷這個事務是單個語句的事務,還是包含多個語句的事務。
第 4 和 13行,使用到的表和鎖只是針對於當前的語句。因此,使用到一張表,並不意味着事務僅僅涉及到一張表。
第 5 和 14 行,這里的信息需要重點關注,因為它告訴我們事務做了多少的改變,也就是 “undo log entries”;”row lock(s)” 則告訴我們持有多少行鎖。這些信息都會提示我們這個事務的復雜程度。
第 6 和 15 行,留意線程 ID、連接主機和用戶。如果你在不同的應用程序中使用不同的 MySQL 用戶,這將是另外一個好的習慣,這樣你就可以根據連接主機和用戶來定位到事務來自於哪個應用程序。
第 9 行,對於第一個事務,它只是顯示了處於鎖等待狀態,在這個例子中,是表 t3 的 X 鎖。其他的可能:共享鎖(S),有間隙鎖(gap lock)的排他鎖(X),及沒有間隙鎖(gap lock)的排他鎖(X),及AUTO_INC 。
第 9 和 10 行:”space id” 是表空間ID,”page no” 指出了這個表空間里面記錄鎖所在的數據頁,”n bits” 不是數據頁偏移量,而是鎖位圖里面的 bits 數。在第 10 行記錄的 “heap no” 是數據頁偏移量。然后第 10 行下面的數據顯示了記錄數據的十六進制編碼。字段 0 表示聚集索引(即主鍵),忽略最高位,值為 5。字段 1 表示最后修改這條記錄的事務的ID號,上面實例中的十進制值是 25205268,即是 TRANSACTION (2)。字段 2 表示回滾指針。從字段 3 開始,表示的是余下的行數據。通過閱讀這些信息,我們可以准確知道哪一行被鎖了,哪些是當前值。
第 17 和 18 行,對於第二個事務,顯示了它持有的鎖,在本示例中,是事務1 (TRANSACTION (1)) 所請求並等待中的 X 鎖。
第 20 和 21 行,顯示了事務2 (TRANSACTION (2)) 所等待的鎖的信息。在本例中,是t3表產生的 X 鎖。
另外,在InnoDB中有幾種少數情況會產生共享記錄鎖:
1) 使用了 SELECT … LOCK IN SHARE MODE 的語句
2) 外鍵引用記錄
3) 源表上的共享鎖,使用了 INSERT INTO… SELECT 的語句
這些信息結合着其他數據可以幫助開發人員定位到那個事務。
我們還可以從哪里找到事務之前的語句?
除了應用程序日志和之前的 show engine innodb status 的輸出信息外,還可以利用 binlog、low log,甚至是general log。
通過 binlog,如果 binlog_format = statement,binlog 中的每個 event 都會擁有一個 thread_id。只有已提交的事務會被記錄到 binlog 中,
Example 5︰Two Transactions execute ‘select * from t1 WHERE id =…for update’ In One Table(REPEATABLE READ isolation)
兩個會話參與,在RR隔離級別下。
create table t1 (a int primary key ,b int); insert into t1 values (1,2),(2,3),(3,4),(11,22);
開啟兩個事務會話。
transaction 1> begin; transaction 1> select * from t1 where a = 5 for update;
獲取記錄(11,22)上的GAP X鎖。
transaction 2> begin; transaction 2> select * from t1 where a = 5 for update;
同上,GAP鎖之間不沖突。
# block,wait transaction 1; transaction 1> insert into t1 values (4,5);
# block,wait transaction 2,deadlock; transaction 2> insert into t1 values (4,5);
引起這個死鎖的原因是非插入意向的GAP X鎖和插入意向X鎖之間是沖突的。
六、如何避免死鎖?
在了解死鎖之后,我們可以做一些事情來避免它。
- 對應用程序進行調整/修改。在某些情況下,你可以通過把大事務分解成多個小事務,使得鎖能夠更快被釋放,從而極大程度地降低死鎖發生的頻率。在其他情況下,死鎖的發生是因為兩個事務采用不同的順序操作了一個或多個表的相同的數據集。需要改成以相同順序讀寫這些數據集,換言之,就是對這些數據集的訪問采用串行化方式。這樣在並發事務時,就讓死鎖變成了鎖等待。
- 修改表的schema,例如刪除外鍵約束來分離兩張表,或者添加索引來減少掃描和鎖定的行。
- 如果發生了間隙鎖,你可以把會話或者事務的事務隔離級別更改為RC(read committed)級別來避免,可以避免掉很多因為gap鎖造成的死鎖,但此時需要把binlog_format設置成row或者mixed格式。
- 為表添加合理的索引,不走索引將會為表的每一行記錄添加上鎖,死鎖的概率大大增大。
轉載:http://www.ywnds.com/?p=4949