字節(jié)二面,兩個(gè)事務(wù)執(zhí)行 SQL 語(yǔ)句的過(guò)程中,導(dǎo)致死鎖
大家好,我是小林。
之前收到讀者面試字節(jié)時(shí),被問(wèn)到一個(gè)關(guān)于 MySQL 的問(wèn)題。
如果對(duì) MySQL 加鎖機(jī)制比較熟悉的同學(xué),應(yīng)該一眼就能看出會(huì)發(fā)生死鎖。
但是具體加了什么鎖而導(dǎo)致死鎖,是需要我們具體分析的。
接下來(lái),就跟聊聊上面兩個(gè)事務(wù)執(zhí)行 SQL 語(yǔ)句的過(guò)程中,加了什么鎖,從而導(dǎo)致死鎖的。
準(zhǔn)備工作
先創(chuàng)建一張 t_student 表,假設(shè)除了 id 字段,其他字段都是普通字段。
CREATE TABLE `t_student` (
`id` int NOT NULL,
`no` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL,
`score` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
然后,插入相關(guān)的數(shù)據(jù)后,t_student 表中的記錄如下:
開(kāi)始實(shí)驗(yàn)
在實(shí)驗(yàn)開(kāi)始前,先說(shuō)明下實(shí)驗(yàn)環(huán)境:
- MySQL 版本:8.0.26
- 隔離級(jí)別:可重復(fù)讀(RR)
啟動(dòng)兩個(gè)事務(wù),按照題目的 SQL 執(zhí)行順序,過(guò)程如下表格:
可以看到,事務(wù) A 和 事務(wù) B 都在執(zhí)行 insert 語(yǔ)句后,都陷入了等待狀態(tài)(前提沒(méi)有打開(kāi)死鎖檢測(cè)),也就是發(fā)生了死鎖,因?yàn)槎荚谙嗷サ却龑?duì)方釋放鎖。
為什么會(huì)發(fā)生死鎖?
我們可以通過(guò) select * from performance_schema.data_locks\G; 這條語(yǔ)句,查看事務(wù)執(zhí)行 SQL 過(guò)程中加了什么鎖。
接下來(lái),針對(duì)每一條 SQL 語(yǔ)句分析具體加了什么鎖。
Time 1 階段加鎖分析
Time 1 階段,事務(wù) A 執(zhí)行以下語(yǔ)句:
# 事務(wù) A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update t_student set score = 100 where id = 25;
Query OK, 0 rows affected (0.01 sec)
Rows matched: 0 Changed: 0 Warnings: 0
然后執(zhí)行 select * from performance_schema.data_locks\G; 這條語(yǔ)句,查看事務(wù) A 此時(shí)加了什么鎖。
從上圖可以看到,共加了兩個(gè)鎖,分別是:
- 表鎖:X 類(lèi)型的意向鎖;
- 行鎖:X 類(lèi)型的間隙鎖;
這里我們重點(diǎn)關(guān)注行鎖,圖中 LOCK_TYPE 中的 RECORD 表示行級(jí)鎖,而不是記錄鎖的意思,通過(guò) LOCK_MODE 可以確認(rèn)是 next-key 鎖,還是間隙鎖,還是記錄鎖:
- 如果 LOCK_MODE 為X,說(shuō)明是 next-key 鎖;
- 如果 LOCK_MODE 為X, REC_NOT_GAP,說(shuō)明是記錄鎖;
- 如果 LOCK_MODE 為X, GAP,說(shuō)明是間隙鎖;
因此,此時(shí)事務(wù) A 在主鍵索引(INDEX_NAME : PRIMARY)上加的是間隙鎖,鎖范圍是(20, 30)。
Time 2 階段加鎖分析
Time 2 階段,事務(wù) B 執(zhí)行以下語(yǔ)句:
# 事務(wù) B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update t_student set score = 100 where id = 26;
Query OK, 0 rows affected (0.01 sec)
Rows matched: 0 Changed: 0 Warnings: 0
然后執(zhí)行 select * from performance_schema.data_locks\G; 這條語(yǔ)句,查看事務(wù) B 此時(shí)加了什么鎖。
從上圖可以看到,共加了兩個(gè)鎖,分別是:
- 表鎖:X 類(lèi)型的意向鎖;
- 行鎖:X 類(lèi)型的間隙鎖;
因此,此時(shí)事務(wù) B 在主鍵索引(INDEX_NAME : PRIMARY)上加的是間隙鎖,鎖范圍是(20, 30)。
事務(wù) A 和 事務(wù) B 的間隙鎖范圍都是一樣的,為什么不會(huì)沖突?
兩個(gè)事務(wù)的間隙鎖之間是相互兼容的,不會(huì)產(chǎn)生沖突。
在MySQL官網(wǎng)上還有一段非常關(guān)鍵的描述:
Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from Inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.
間隙鎖的意義只在于阻止區(qū)間被插入,因此是可以共存的。一個(gè)事務(wù)獲取的間隙鎖不會(huì)阻止另一個(gè)事務(wù)獲取同一個(gè)間隙范圍的間隙鎖,共享和排他的間隙鎖是沒(méi)有區(qū)別的,他們相互不沖突,且功能相同。
Time 3 階段加鎖分析
Time 3,事務(wù) A 插入了一條記錄:
# Time 3 階段,事務(wù) A 插入了一條記錄
mysql> insert into t_student(id, no, name, age,score) value (25, 'S0025', 'sony', 28, 90);
/// 阻塞等待......
此時(shí),事務(wù) A 就陷入了等待狀態(tài)。
然后執(zhí)行 select * from performance_schema.data_locks\G; 這條語(yǔ)句,查看事務(wù) A 在獲取什么鎖而導(dǎo)致被阻塞。
可以看到,事務(wù) A 的狀態(tài)為等待狀態(tài)(LOCK_STATUS: WAITING),因?yàn)橄蚴聞?wù) B 生成的間隙鎖(范圍 (20, 30)?)中插入了一條記錄,所以事務(wù) A 的插入操作生成了一個(gè)插入意向鎖(LOCK_MODE:INSERT_INTENTION)。
插入意向鎖是什么?
注意!插入意向鎖名字里雖然有意向鎖這三個(gè)字,但是它并不是意向鎖,它屬于行級(jí)鎖,是一種特殊的間隙鎖。
在MySQL的官方文檔中有以下重要描述:
An Insert intention lock is a type of gap lock set by Insert operations prior to row Insertion. This lock signals the intent to Insert in such a way that multiple transactions Inserting into the same index gap need not wait for each other if they are not Inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to Insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with Insert intention locks prior to obtaining the exclusive lock on the Inserted row, but do not block each other because the rows are nonconflicting.
這段話表明盡管插入意向鎖是一種特殊的間隙鎖,但不同于間隙鎖的是,該鎖只用于并發(fā)插入操作。
如果說(shuō)間隙鎖鎖住的是一個(gè)區(qū)間,那么「插入意向鎖」鎖住的就是一個(gè)點(diǎn)。因而從這個(gè)角度來(lái)說(shuō),插入意向鎖確實(shí)是一種特殊的間隙鎖。
插入意向鎖與間隙鎖的另一個(gè)非常重要的差別是:盡管「插入意向鎖」也屬于間隙鎖,但兩個(gè)事務(wù)卻不能在同一時(shí)間內(nèi),一個(gè)擁有間隙鎖,另一個(gè)擁有該間隙區(qū)間內(nèi)的插入意向鎖(當(dāng)然,插入意向鎖如果不在間隙鎖區(qū)間內(nèi)則是可以的)。所以,插入意向鎖和間隙鎖之間是沖突的。
另外,我補(bǔ)充一點(diǎn),插入意向鎖的生成時(shí)機(jī):
每插入一條新記錄,都需要看一下待插入記錄的下一條記錄上是否已經(jīng)被加了間隙鎖,如果已加間隙鎖,那 Insert 語(yǔ)句會(huì)被阻塞,并生成一個(gè)插入意向鎖 。
Time 4 階段加鎖分析
Time 4,事務(wù) B 插入了一條記錄:
# Time 4 階段,事務(wù) B 插入了一條記錄
mysql> insert into t_student(id, no, name, age,score) value (26, 'S0026', 'ace', 28, 90);
/// 阻塞等待......
此時(shí),事務(wù) B 就陷入了等待狀態(tài)。
然后執(zhí)行 select * from performance_schema.data_locks\G; 這條語(yǔ)句,查看事務(wù) B 在獲取什么鎖而導(dǎo)致被阻塞。
可以看到,事務(wù) B 在生成插入意向鎖時(shí)而導(dǎo)致被阻塞,這是因?yàn)槭聞?wù) B 向事務(wù) A 生成的間隙鎖(范圍 (20, 30))中插入了一條記錄,而插入意向鎖和間隙鎖是沖突的,所以事務(wù) B 在獲取插入意向鎖時(shí)就陷入了等待狀態(tài)。
最后回答,為什么會(huì)發(fā)生死鎖?
本次案例中,事務(wù) A 和事務(wù) B 在執(zhí)行完后 update 語(yǔ)句后都持有范圍為(20, 30)的間隙鎖,而接下來(lái)的插入操作為了獲取到插入意向鎖,都在等待對(duì)方事務(wù)的間隙鎖釋放,于是就造成了循環(huán)等待,滿足了死鎖的四個(gè)條件:互斥、占有且等待、不可強(qiáng)占用、循環(huán)等待,因此發(fā)生了死鎖。
總結(jié)
兩個(gè)事務(wù)即使生成的間隙鎖的范圍是一樣的,也不會(huì)發(fā)生沖突,因?yàn)殚g隙鎖目的是為了防止其他事務(wù)插入數(shù)據(jù),因此間隙鎖與間隙鎖之間是相互兼容的。
在執(zhí)行插入語(yǔ)句時(shí),如果插入的記錄在其他事務(wù)持有間隙鎖范圍內(nèi),插入語(yǔ)句就會(huì)被阻塞,因?yàn)椴迦胝Z(yǔ)句在碰到間隙鎖時(shí),會(huì)生成一個(gè)插入意向鎖,然后插入意向鎖和間隙鎖之間是互斥的關(guān)系。
如果兩個(gè)事務(wù)分別向?qū)Ψ匠钟械拈g隙鎖范圍內(nèi)插入一條記錄,而插入操作為了獲取到插入意向鎖,都在等待對(duì)方事務(wù)的間隙鎖釋放,于是就造成了循環(huán)等待,滿足了死鎖的四個(gè)條件:互斥、占有且等待、不可強(qiáng)占用、循環(huán)等待,因此發(fā)生了死鎖。