在生產(chǎn)環(huán)境中如果出現(xiàn)MySQL死鎖問題該如何排查和解決呢,本文將模擬真實(shí)死鎖場(chǎng)景進(jìn)行排查,最后總結(jié)下實(shí)際開發(fā)中如何盡量避免死鎖發(fā)生哪亿。
一、準(zhǔn)備好相關(guān)數(shù)據(jù)和環(huán)境
當(dāng)前自己的數(shù)據(jù)版本是8.0.22
mysql> select @@version;
+-----------+
| @@version |
+-----------+
| 8.0.22 |
+-----------+
1 row in set (0.00 sec)
數(shù)據(jù)庫隔離級(jí)別(默認(rèn)隔離級(jí)別)
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
自動(dòng)提交關(guān)閉
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 0 |
+--------------+
1 row in set (0.00 sec)
表結(jié)構(gòu)
這個(gè)age為 非唯一索引
,這點(diǎn)對(duì)下面整個(gè)案例非常重要板辽。
-- id是自增主鍵劲弦,age是非唯一索引醇坝,name普通字段
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`age` int DEFAULT NULL COMMENT '年齡',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用戶信息表';
表中暫時(shí)先插入兩條數(shù)據(jù)
二画畅、模擬出真實(shí)死鎖案例
開啟兩個(gè)終端模擬事務(wù)并發(fā)情況轴踱,執(zhí)行順序以及實(shí)驗(yàn)現(xiàn)象如下:
1)事務(wù)A執(zhí)行更新操作谚赎,更新成功
mysql> update user set name = 'wangwu' where age= 20;
Query OK, 1 row affected (0.00 sec)
- 事務(wù)B執(zhí)行更新操作摊腋,更新成功
mysql> update user set name = 'zhaoliu' where age= 10;
Query OK, 1 row affected (0.00 sec)
3)事務(wù)A執(zhí)行插入操作,陷入阻塞~
mysql> insert into user values (null, 15, "tianqi");
4)事務(wù)B執(zhí)行插入操作兴蒸,插入成功,同時(shí)事務(wù)A的插入由阻塞變?yōu)樗梨ierror橙凳。
insert into user values (null, 30, "wangba");
Query OK, 1 row affected (0.00 sec)
事務(wù)A的插入操作變成報(bào)錯(cuò)。
上面四步操作后笑撞,我們分別對(duì)事務(wù)A和事務(wù)B進(jìn)行commit操作岛啸。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
我們?cè)賮砜磾?shù)據(jù)庫中表的數(shù)據(jù)茴肥。
我們發(fā)現(xiàn)瓤狐,事務(wù)B的所有操作最終都成功了础锐,而事務(wù)A的操作因?yàn)閳?bào)錯(cuò)都回滾了。所以事務(wù)A的操作都失敗皆警。
那既然是死鎖鸵隧,為什么回滾事務(wù)A,而不是事務(wù)B,是隨機(jī)的還是有機(jī)制在里面?
我們可以理解死鎖是數(shù)據(jù)庫對(duì)事務(wù)的保護(hù)機(jī)制,一旦發(fā)生死鎖,MySQL會(huì)選擇相對(duì)小的事務(wù)(undo較少的)進(jìn)行回滾意推。
三掰派、查看分析死鎖日志
可以用 show engine innodb status
,查看最近一次死鎖日志哈左痢,執(zhí)行后靡羡,死鎖日志如下(只展示部分日志):
LATEST DETECTED DEADLOCK
------------------------
2021-12-24 06:02:52 0x7ff7074f8700
*** (1) TRANSACTION:
TRANSACTION 2554368, ACTIVE 22 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
INSERT INTO user VALUES (NULL, 15, "tianqi")
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554368 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554368 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 4; hex 80000002; asc ;;
*** (2) TRANSACTION:
TRANSACTION 2554369, ACTIVE 14 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
INSERT INTO user VALUES (NULL, 30, "wangba")
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554369 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 4; hex 80000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 309 page no 5 n bits 72 index idx_age of table `mall_goods`.`user` trx id 2554369 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (1)
1、事務(wù)A相關(guān)日志
1)找到關(guān)鍵詞TRANSACTION俊性,事務(wù)2554368
2)查看事務(wù)1正在執(zhí)行的sql
insert into user values (null, 15, "tianqi")
- 查看當(dāng)前事務(wù)已占有的鎖和等待其它事務(wù)釋放的鎖
2略步、事務(wù)B相關(guān)日志
1)找到關(guān)鍵詞TRANSACTION,事務(wù)2554369
2)查看事務(wù)2正在執(zhí)行的sql
insert into user values (null, 30, "wangba")
- 查看當(dāng)前事務(wù)已占有的鎖和等待其它事務(wù)釋放的鎖
3定页、總結(jié)
這里把一些關(guān)鍵的日志截圖了下
我們把這張圖換一種方式畫下
1)從圖中可以很明顯的看出,事務(wù)1和事務(wù)2都在等對(duì)方的鎖釋放趟薄,所以導(dǎo)致了死鎖問題。而且最終是事務(wù)1進(jìn)行了回滾典徊。
2)這個(gè)日志提供比較重要的信息就是我們可以看出的是哪兩條sql在互相一直等待其它事務(wù)的鎖釋放而產(chǎn)生了死鎖杭煎,也知道是哪個(gè)索引導(dǎo)致產(chǎn)生的死鎖恩够,同時(shí)也知道最終哪個(gè)事務(wù)被回滾了。
3)如果上面的信息還不能幫你定位解決問題羡铲,那可以問數(shù)據(jù)庫DB要詳細(xì)的binlog日志來分析這段時(shí)間這兩個(gè)事務(wù)具體執(zhí)行的所有sql蜂桶。
四、總結(jié)分析案例中產(chǎn)生死鎖的原因
這個(gè)分析就需要對(duì)MySQL中的各種鎖機(jī)制有所了解也切,還不清楚的話可以看我之前寫的兩篇文章扑媚,看完你就清楚我下面所寫的了。
1、事務(wù)A的SQL產(chǎn)生了哪些鎖
1) 事務(wù)A的update語句產(chǎn)生哪些鎖
我們先來看
update user set name = 'wangwu' where age= 20;
記錄鎖
因?yàn)槭堑戎挡樵兊够保赃@里會(huì)在滿足age=20的所有數(shù)據(jù)請(qǐng)求一個(gè)記錄鎖旬痹。
間隙鎖
因?yàn)檫@里是非唯一索引的等值查詢,所以一樣會(huì)產(chǎn)生間隙鎖(如果是唯一索引的等值查詢那就不會(huì)產(chǎn)生間隙鎖,只會(huì)有記錄鎖),因?yàn)檫@里只有2條記錄
所以左邊為(10,20),右邊因?yàn)闆]有記錄了讨越,所以請(qǐng)求間隙鎖的范圍就是(20,+∞),加一起就是(10,20) +(20,+∞)两残。
Next-Key鎖
Next-Key鎖=記錄鎖+間隙鎖,所以該Update語句就有了(10,+∞)的 Next-Key鎖
事務(wù)A的install語句產(chǎn)生哪些鎖
INSERT INTO user VALUES (NULL, 15, "tianqi");
間隙鎖
- 因?yàn)閍ge 15(在10和20之間)谎痢,所以需要請(qǐng)求加(10,20)的間隙鎖
插入意向鎖(Insert Intention)
- 插入意向鎖是在插入一行記錄操作之前設(shè)置的一種間隙鎖磕昼,這個(gè)鎖釋放了一種插入方式的信號(hào)卷雕,即事務(wù)A需要插入意向鎖(10,20),這個(gè)插入鎖的作用就是提高插入效率的节猿,在分析死鎖的時(shí)候我們可以不用關(guān)心它,只關(guān)心上面間隙鎖就好了漫雕。
2滨嘱、事務(wù)B的SQL產(chǎn)生了哪些鎖
事務(wù)B的update語句產(chǎn)生哪些鎖
我們先來看
update user set name = 'zhaoliu' where age= 10
記錄鎖
因?yàn)槭堑戎挡樵儯赃@里會(huì)在滿足age=10的所有數(shù)據(jù)請(qǐng)求一個(gè)記錄鎖浸间。
間隙鎖
因?yàn)樽筮厸]有記錄,右邊有一個(gè)age=20的記錄太雨,所以間隙鎖的范圍是(-∞,10),右邊為(10,20),一起就是(-∞,10)+(10,20)。
Next-Key鎖
Next-Key鎖=記錄鎖+間隙鎖魁蒜,所以該Update語句就有了(-∞,20)的 Next-Key鎖
事務(wù)A的install語句產(chǎn)生哪些鎖
INSERT INTO user VALUES (NULL, 30, "wangba")
間隙鎖
- 因?yàn)閍ge 30(左邊是20囊扳,右邊沒有值),所以需要請(qǐng)求加(20,+∞)的間隙鎖
插入意向鎖(Insert Intention)
- (20,+∞)
鎖都分析清楚了兜看,接下來再來看下是什么地方導(dǎo)致死鎖的呢锥咸?
這樣以來產(chǎn)生整個(gè)死鎖的原因也就清楚了,不過這里再補(bǔ)充兩點(diǎn)
1)MySQL的間隙鎖雖然有左開右閉的原則,但是其實(shí)這個(gè)并不完全正確,因?yàn)樗锌赡苁亲箝]右開,也可能是左開右開细移,它會(huì)跟你插入主鍵值位置有關(guān)搏予,具體的可以看我之前寫的一篇文章
里面有完整示例MySQL記錄鎖、間隙鎖弧轧、臨鍵鎖小案例演示雪侥。所以這里間隙鎖寫的都是左開右開的范圍碗殷,可能臨界點(diǎn)有點(diǎn)模糊,但不影響分析這個(gè)案例的死鎖問題速缨。
2)通過事務(wù)A和事務(wù)B的update語句锌妻,我們可以發(fā)現(xiàn)其實(shí)它們都持有間隙鎖(10,20)的這段范圍,說明間隙鎖范圍是可以相互兼容的,意思就是只要你的10不在我(10,+∞)的間隙鎖范圍內(nèi)鸟廓,那就可以產(chǎn)生部分重合的間隙鎖从祝,也就是這里的(10,20)。
五引谜、實(shí)際開發(fā)中如何盡量避免死鎖發(fā)生
一般來講在實(shí)際開發(fā)中牍陌,很少會(huì)發(fā)生死鎖的情況,尤其是在業(yè)務(wù)并發(fā)量不是很大的情況下员咽。在并發(fā)很大的情況下可能會(huì)存在偶爾產(chǎn)生死鎖毒涧。
不過呢,在自己實(shí)際開發(fā)中贝室,有遇到過請(qǐng)求一個(gè)接口出現(xiàn)100%概率死鎖的情況契讲。
當(dāng)時(shí)的場(chǎng)景其實(shí)很簡(jiǎn)單。一段業(yè)務(wù)代碼中滑频,有去走Dubbo調(diào)其它接口服務(wù),這就存在了兩個(gè)事務(wù),結(jié)果各自事務(wù)提交的時(shí)候捡偏,都需要等待對(duì)方的鎖釋放,就導(dǎo)致每次都發(fā)生死鎖超時(shí)峡迷。這其實(shí)是一種代碼不規(guī)范而導(dǎo)致死鎖的發(fā)生银伟。
這里也總結(jié)下如何盡量避免死鎖發(fā)生。
1)不同的應(yīng)用訪問同一組表時(shí)绘搞,應(yīng)盡量約定以相同的順序訪問各表彤避。對(duì)一個(gè)表而言,應(yīng)盡量以固定的順序存取表中的行夯辖。這點(diǎn)真的很重要,它可以明顯的減少死鎖的發(fā)生琉预。
舉例:好比有a,b兩張表,如果事務(wù)1先a后b,事務(wù)2先b后a,那就可能存在相互等待產(chǎn)生死鎖蒿褂。那如果事務(wù)1和事務(wù)2都先a后b圆米,那事務(wù)1先拿到a的鎖,事務(wù)2再去拿a的鎖啄栓,如果鎖沖突那就會(huì)等待事務(wù)1釋放鎖娄帖,那自然事務(wù)2就不會(huì)拿到b的鎖,那就不會(huì)堵塞事務(wù)1拿到b的鎖谴供,這樣就避免死鎖了块茁。
2)在主鍵等值更新的時(shí)候,盡量先查詢看數(shù)據(jù)庫中有沒有滿足條件的數(shù)據(jù),如果不存在就不用更新数焊,存在才更新永淌。為什么要這么做呢,因?yàn)槿绻ジ乱粭l數(shù)據(jù)庫不存在的數(shù)據(jù)佩耳,一樣會(huì)產(chǎn)生間隙鎖遂蛀。
舉例:如果表中只有id=1和id=5的數(shù)據(jù),那么如果你更新id=3的sql干厚,因?yàn)檫@條記錄表中不存在李滴,那就會(huì)產(chǎn)生一個(gè)(1,5)的間隙鎖,但其實(shí)這個(gè)鎖就是多余的蛮瞄,因?yàn)槟闳ジ乱粋€(gè)數(shù)據(jù)都不存在的數(shù)據(jù)沒有任何意義所坯。
3)盡量使用主鍵更新數(shù)據(jù),因?yàn)橹麈I是唯一索引,在等值查詢能查到數(shù)據(jù)的情況下只會(huì)產(chǎn)生行鎖挂捅,不會(huì)產(chǎn)生間隙鎖芹助,這樣產(chǎn)生死鎖的概率就減少了。當(dāng)然如果是范圍查詢闲先,一樣會(huì)產(chǎn)生間隙鎖状土。
4)避免長(zhǎng)事務(wù),小事務(wù)發(fā)送鎖沖突的幾率也小伺糠。這點(diǎn)應(yīng)該很好理解蒙谓。
5)在允許幻讀和不可重復(fù)度的情況下,盡量使用RC的隔離級(jí)別训桶,避免gap lock造成的死鎖累驮,因?yàn)楫a(chǎn)生死鎖經(jīng)常都跟間隙鎖有關(guān),間隙鎖的存在本身也是在RR隔離級(jí)別來解決幻讀的一種措施渊迁。
感謝
這篇文章給自己提供了很好的思路慰照,這篇文章也基本上按照這個(gè)思路往下寫的
關(guān)注公眾號(hào): 后端元宇宙灶挟。持續(xù)輸出優(yōu)質(zhì)好文