前提:InnoDB存儲引擎 + 默認的事務隔離級別 Repeatable Read
用MySQL客戶端模擬並發事務操作數據時,如下表按照時間的先后順序執行命令,會導致死鎖。
數據庫數據如下,id為主鍵。
select * from a ;
+----+
| id |
+----+
| 3 |
+----+
| 8 |
+----+
| 11 |
+----+
時間 會話A 會話B
1 begin;
2 delete from a where id = 4;
3 begin;
4 delete from a where id = 6;
5 insert into a values(5);
6 insert into a values(7);
7 Query OK, 1 row affected
8 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
9 commit;
為什么看似互不影響的事務會出現死鎖的問題?
我們一定聽說過MySQL中存在共享鎖(S鎖)和排他鎖(X鎖),可能聽說過有意向共享鎖(IS鎖)和意向排他鎖(IX鎖),上面出現死鎖的情況,一定是存在這幾種鎖的相互等待。
InnoDB存儲引擎實現共享鎖(S Lock)和排它鎖(X Lock)兩種行級鎖,注意:行鎖!行鎖!行鎖!
S Lock:允許事務讀一行數據,多個事務可以並發的對行數據加S Lock
X Lock:允許事務刪除或更新一行數據,只有行數據沒有任何鎖才可以獲取X Lock
InnoDB支持意向共享鎖(IS Lock)和意向排它鎖(IX Lock),這兩種鎖是表級別的鎖,但實際上也應用在行鎖之中
IS Lock:事務想要獲得一張表中某幾行的共享鎖
IX Lock:事務想要獲得一張表中某幾行的排它鎖
鎖的分類:
行鎖
鎖定一行數據,即上面所說的共享鎖和排他鎖
間隙鎖
鎖定一個范圍,但不包含記錄本身。例如數據庫中數據id為3,8,11,那么鎖定的區間可能為(-∞,3),(3,8)(8,11),(11,+∞),假如插入的數據為6,那此時鎖定的區間為(3,6),(6,8)被鎖定,不包括要插入的6.
行鎖 + 間隙鎖
鎖定一個范圍,包括記錄本身,例如數據庫中數據id為3,8,11,那么鎖定的區間可能為(-∞,3],(3,8](8,11],(11,+∞],假如插入的數據為6,此時鎖定的區間(3,8]變為(3,6],(6,8]兩個部分,可以看到,6也被鎖定。
為什么要有間隙鎖?
我們應該聽說過幻讀,即在同一事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能返回之前不存在的行。InnoDB使用行鎖 + 間隙鎖的方式解決這個問題。當然,InnoDB存儲引擎在查詢數據時是不存在鎖的,這是因為查詢的數據來自於快照版本,即歷史數據。
鎖的應用:
insert 插入記錄時,需要獲取行鎖
update 更新一條記錄時,如果記錄存在,需要行鎖;如果記錄不存在,行鎖 + 間隙鎖
delete 刪除一條記錄時,如果記錄存在,需要行鎖;如果記錄不存在,行鎖 + 間隙鎖
select 查詢記錄時,不會存在鎖,除非顯示的調用lock in share mode或者for update,如下所示。為什么查詢不存在鎖呢?因為InnoDB引擎select查詢返回的是數據的快照版本,這也是為什么在許多mysql書中,事務的select查詢需要鎖時,要顯示的使用加鎖語法。參見MySQL查詢不需要鎖,了解更多有關InnoDB查詢的機制。
# S Lock
select * from a where id = 1 lock in share mode ;
# X Lock
select * from a where id = 1 for update ;
掌握了這些知識的話,我們再來看上面兩個事務為什么會出現死鎖的問題。上面說列id是主鍵,實際上只要是索引,不論是唯一索引、組合索引、普通索引,都會存在間隙鎖的問題。
上面發生死鎖的情況是當數據不存在時,當數據存在時,也會出現死鎖的情況,這種情況可以通過3個會話來模擬,當然在實際的項目情況下,並發事務確實是帶來了死鎖的問題,例如在Spring事務中,先刪除表A中的數據,再向表A插入數據,如果並發量比較大的話,如果存在間隙鎖,那么有幾率會出現死鎖的問題。
Spring事務中大致的運行流程如下:
一個事務中存在先刪除再插入的邏輯,並發時,事務A將存在的數據id=6刪除,此時事務B也刪除id=6的數據,事務C同樣刪除id=6的數據,這種情況下,如果並發量夠大,一定會出現間隙鎖,從而發生死鎖。
解決方法:
方法一:通常情況下,要刪除一條數據,先查詢數據是否存在,如果存在,再根據主鍵(最好非聯合主鍵)刪除,否則不執行刪除邏輯。其實這種方式也存在一定的風險,我們可以通過軟刪除的方式,避免高並發時出現數據已被刪除,而其他事務正在刪除不存在的數據。軟刪除是指通過字段決定數據是否已刪除,然后定時的手動處理數據庫中的數據。
方法二:使用隊列,通過手動處理數據關鍵字做hash,把一類數據路由到相同的隊列,隊列會按串行的方式處理數據,但是這種方式只能保證一個服務節點是正常的,如果高可用下多個服務節點同時處理數據,仍然有幾率出現這樣的問題。此時可以通過外部調整,使一類數據只請求同一個服務節點。這種方法適用於對數據完整性不做要求的情況,因為服務宕機會導致內存數據丟失。這種方式我實現過一套。
方法三:使用高可用消息中間件處理數據,類似於方法二。但這種方式不會因為服務宕機導致數據丟失,並且消息中間件如MQ都會有保證數據最終一致性的策略。
方法四:盡量避免間隙鎖的存在,參見MySQL常見死鎖及解決方案。
方法五:其他方式,我還沒有了解到,如果您知道方法,請給我留言,我會做進一步的驗證。
————————————————
原文鏈接:https://blog.csdn.net/qq_30038111/article/details/85480791