背景
最近雙十一開(kāi)門紅期間組內(nèi)出現(xiàn)了一次因 Mysql 死鎖導(dǎo)致的線上問(wèn)題,當(dāng)時(shí)從監(jiān)控可以看到數(shù)據(jù)庫(kù)活躍連接數(shù)飆升,導(dǎo)致應(yīng)用層數(shù)據(jù)庫(kù)連接池被打滿,后續(xù)所有請(qǐng)求都因獲取不到連接而失敗
整體業(yè)務(wù)代碼精簡(jiǎn)邏輯如下:
@Transaction
public void service(Integer id) {
delete(id);
insert(id);
}
數(shù)據(jù)庫(kù)實(shí)例監(jiān)控:
當(dāng)時(shí)通過(guò)分析上游問(wèn)題流量限流解決后,后續(xù)找時(shí)間又重新分析了下問(wèn)題發(fā)生的根本原因,現(xiàn)將其總結(jié)如下:本篇文章會(huì)先對(duì) Mysql 中的各種鎖進(jìn)行分析,包括互斥鎖、間隙鎖和插入意向鎖,讓大家對(duì)各種鎖的使用場(chǎng)景有一個(gè)了解,然后在此基礎(chǔ)上再對(duì)本問(wèn)題進(jìn)行分析,希望大家未來(lái)再碰到相似場(chǎng)景時(shí),能夠快速的定位問(wèn)題
Mysql 鎖機(jī)制
在 Mysql 中為了解決對(duì)同一行記錄并發(fā)寫的問(wèn)題,引入了行鎖機(jī)制,多個(gè)事務(wù)不能同時(shí)對(duì)一行數(shù)據(jù)進(jìn)行修改操作,當(dāng)需要對(duì)數(shù)據(jù)庫(kù)中的一行數(shù)據(jù)進(jìn)行修改時(shí),會(huì)首先判斷該行數(shù)據(jù)是否加鎖,如果沒(méi)加鎖,那么當(dāng)前事務(wù)加鎖成功,可以進(jìn)行后續(xù)的修改操作;但如果該行數(shù)據(jù)已經(jīng)被其他事務(wù)加鎖,則當(dāng)前事務(wù)只有等待加鎖的事務(wù)釋放鎖后才能加鎖成功,繼續(xù)執(zhí)行修改操作
本篇文章中所有實(shí)驗(yàn)用到的建表語(yǔ)句:
create table `test` (
`id` int(11) NOT NULL,
`num` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `num` (`num`)
) ENGINE = InnoDB;
insert into
test
values
(10, 10),
(20, 20),
(30, 30),
(40, 40),
(50, 50);
Shared and Exclusive Locks
shared(S) lock 表示共享鎖,當(dāng)一個(gè)事務(wù)持有某行上的 S 鎖后可以對(duì)該行的數(shù)據(jù)進(jìn)行讀操作,通過(guò)語(yǔ)句 select ... from test lock in share mode 可以添加共享鎖,一般使用的較少,不做過(guò)多闡述
exclusive(X) lock 表示互斥鎖,當(dāng)一個(gè)事務(wù)對(duì)某行數(shù)據(jù)進(jìn)行 update 或 delete 操作時(shí)都要先獲取到該記錄上的 X 鎖,如果已經(jīng)有其他事務(wù)獲取到了該記錄上的 X 鎖,那么當(dāng)前事務(wù)會(huì)阻塞等待直到上一事務(wù)釋放了對(duì)應(yīng)記錄上的 X 鎖
S 鎖之間不互斥,多個(gè)事務(wù)可以同時(shí)獲取一條記錄上的 S 鎖 X 鎖之間互斥,多個(gè)事務(wù)不能同時(shí)獲取同一條記錄上的 X 鎖 S 鎖和 X 鎖之間互斥,多個(gè)事務(wù)不能同時(shí)獲取同一條記錄上的 S 鎖和 X 鎖
當(dāng)多個(gè)事務(wù)同時(shí)去 update 索引上同一條記錄時(shí),都需要先獲取到該記錄上的 X 鎖,所謂的鎖也就是會(huì)在內(nèi)存中生成一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)記錄當(dāng)前的事務(wù)信息、鎖類型和是否等待等信息。下圖中就是 T1 和 T2 同時(shí)去更新 id = 30 的這行記錄,并且 T1 成功獲取到了鎖,其在內(nèi)存中生成的鎖結(jié)構(gòu)信息中字段 is_wating 為 false,可以繼續(xù)執(zhí)行事務(wù)的后續(xù)邏輯,而 T2 獲取鎖失敗,則生成的鎖結(jié)構(gòu)信息字段 is_wating 為 true,阻塞等待 T1 上的鎖釋放
互斥鎖在 Mysql 日志中的鎖信息為:lock_mode X locks rec but not gap
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
Gap Locks
上一小節(jié)中介紹了 Exclusive Locks,該鎖可以避免多個(gè)事務(wù)同時(shí)對(duì)一行記錄進(jìn)行更新操作,但不能解決幻讀的問(wèn)題,所謂的幻讀就是指一個(gè)事務(wù)在前后兩次查詢同一個(gè)范圍時(shí),后一次查詢到了前一次沒(méi)有的記錄
session A | session B | |
---|---|---|
T1 | select num from test where num > 10 and num < 15 for update; (0 rows) | |
T2 | insert into test values(12, 12); | |
T3 | select num from test where num > 10 and num < 15 for update; (1 rows) |
在上面這個(gè)場(chǎng)景中,session A 分別在 T1、T3 時(shí)刻進(jìn)行了兩次范圍查詢,session B 在 T2 時(shí)刻插入了一條該范圍內(nèi)的數(shù)據(jù),如果 session A 能在 T3 時(shí)刻查詢出 session B 插入的數(shù)據(jù),就說(shuō)明發(fā)生了幻讀。此時(shí)只使用互斥鎖是無(wú)法解決幻讀的,因?yàn)?num = 12 的記錄在數(shù)據(jù)庫(kù)中還不存在,不能給其加上互斥鎖來(lái)防止 T2 時(shí)刻 session B 的插入
因此為了解決幻讀問(wèn)題,只有引入新的鎖機(jī)制,也就是間隙鎖(Gap Locks)。間隙鎖和互斥鎖不同,互斥鎖是行鎖,只會(huì)鎖定一行特定的記錄,而間隙鎖則是鎖定兩行記錄之間的空隙,防止其他事務(wù)在此間隙中插入新的記錄
引入了間隙鎖之后,session A 在 T1 時(shí)刻會(huì)給 id = 20 記錄生成一個(gè) Gap Locks,之后 session B 在 T2 時(shí)刻想要插入記錄時(shí),需要先判斷待插入位置的后一條記錄上是否存在 Gap Locks,很明顯此時(shí) id = 20 的記錄上已經(jīng)存在了 Gap Locks,那么session B 就需要在 id = 20 的記錄上生成一個(gè)插入意向鎖,并進(jìn)入鎖等待
間隙鎖在 Mysql 中的鎖日志信息如下:lock_mode X locks gap before rec
RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table `test`.`test` trx id 38849 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 8000001e; asc 30 ;;
1: len 6; hex 00000000969c; asc ;;
2: len 7; hex a60000011a0128; asc (;;
3: len 4; hex 8000001e; asc ;;
間隙鎖雖然解決了幻讀問(wèn)題,但因每次都會(huì)鎖住一段間隙,大大降低了數(shù)據(jù)庫(kù)整體的并發(fā)度,且因間隙鎖和間隙鎖之間不互斥,不同事務(wù)可以同時(shí)對(duì)同一間隙加上 Gap Locks,這也往往是各種死鎖產(chǎn)生的源頭
Next-Key Locks
Next-Key Locks 是 (Shard/Exclusive Locks + Gap Locks) 的結(jié)合,當(dāng) session A 給某行記錄 R 添加了互斥型的 Next-Key Locks 后, 相當(dāng)于擁有了記錄 R 的 X 鎖和記錄 R 的 Gap Locks
在上面 Gap Locks 的例子中事務(wù) 1 加的就是 Next-Key Locks,即同時(shí)給 id = 20 的記錄加了 X 鎖和 Gap 鎖
在可重復(fù)讀隔離級(jí)別下,update 和 delete 操作默認(rèn)都會(huì)給記錄添加 Next-Key Locks,Mysql 中 Next-Key Locks 的鎖日志信息為:lock_mode X
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 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;;
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;
Insert Intention Locks
插入意向鎖(Insert Intention Locks) 也是一種間隙鎖,由 INSERT 操作在行數(shù)據(jù)插入之前獲取
在插入一條記錄前,需要先定位到該記錄在 B+ 樹(shù)中的存儲(chǔ)位置,然后判斷待插入位置的下一條記錄上是否添加了 Gap Locks,如果下一條記錄上存在 Gap Locks,那么插入操作就需要阻塞等待,直到擁有 Gap Locks 的那個(gè)事務(wù)提交,同時(shí)執(zhí)行插入操作等待的事務(wù)也會(huì)在內(nèi)存中生成一個(gè)鎖結(jié)構(gòu),表明有事務(wù)想在某個(gè)間隙中插入新記錄,但目前處于阻塞狀態(tài),生成的鎖結(jié)構(gòu)就是插入意向鎖
實(shí)驗(yàn)?zāi)M如下:
session 1 | session 2 | session 3 | |
---|---|---|---|
T1 | begin; | ||
T2 | select * from test where id = 25 for update; | ||
T3 | insert into test values(26, 26); (blocked) | ||
T4 | insert into test values(26, 26); (blocked) |
對(duì)于語(yǔ)句 select * from test where id = 25 for update 因當(dāng)前表中不存在該記錄,在可重復(fù)讀隔離級(jí)別下,為了避免幻讀,會(huì)給 (20, 30] 間隙加上 Gap Locks
從鎖日志可以看出 session 1 給記錄 30 添加了間隙鎖(lock_mode X locks gap before rec)
RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table `test`.`test` trx id 38849 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 8000001e; asc 30 ;;
1: len 6; hex 00000000969c; asc ;;
2: len 7; hex a60000011a0128; asc (;;
3: len 4; hex 8000001e; asc ;;
當(dāng) session 2 插入記錄 26 時(shí),會(huì)在 B+ 樹(shù)中先定位到待插入位置,再判斷插入位置的間隙是否存在 Gap Locks,也就是判斷待插入位置的后一記錄 id = 30 是否存在 Gap Locks,如果存在需要在該記錄上生成插入意向鎖等待
RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table `test`.`test` trx id 38850 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 8000001e; asc 30 ;;
1: len 6; hex 00000000969c; asc ;;
2: len 7; hex a60000011a0128; asc (;;
3: len 4; hex 8000001e; asc ;;
此時(shí) session 2 和 session 3 都在 id = 30 的記錄上添加了插入意向鎖等待 session 1 上的 Gap Locks 釋放,生成的鎖記錄如下:
線上問(wèn)題分析
在對(duì) Mysql 中的各種鎖結(jié)構(gòu)有了一個(gè)清晰的了解之后,回過(guò)頭來(lái)再看看前面的線上問(wèn)題
@Transaction
public void service(Integer id) {
delete(id);
insert(id);
}
對(duì)于上面的業(yè)務(wù)代碼可能存在下面兩種情況:
- 傳入的參數(shù) id 在原數(shù)據(jù)庫(kù)中不存在
- 傳入的參數(shù) id 在原數(shù)據(jù)庫(kù)中存在
本次主要會(huì)針對(duì) id 記錄在原數(shù)據(jù)庫(kù)中不存在進(jìn)行分析
session 1 | session 2 | session 3 | |
---|---|---|---|
T1 | delete from test where id = 15; | ||
T2 | delete from test where id = 15; | delete from test where id = 15; | |
T3 | insert into test values(15, 15); | ||
T4 | insert into test values(15, 15); | ||
T5 | insert into test values(15, 15); |
因 id = 15 在數(shù)據(jù)庫(kù)中不存在,在 T1 時(shí)刻 session 1 會(huì)給其所在間隙的下一條記錄添加上 Gap Locks,又因 Gap Locks 不互斥, 在 T2 時(shí)刻 session 2 和session 3 都會(huì)同時(shí)獲取到 id = 20 的 Gap 鎖
下圖中 tx: T1、T2、T3 分別代表 session 1、session 2 和 session 3
當(dāng)在 T3 時(shí)刻 session 1 插入 id = 15 的記錄時(shí),會(huì)判斷其插入位置的后一條記錄是否存在 Gap Locks,如果存在,則需要在該記錄上生成 Insert Intention Locks 并等待持有 Gap Locks 的事務(wù)釋放鎖
在 T4 時(shí)刻 session 2 執(zhí)行插入語(yǔ)句,同樣會(huì)因插入位置的后一條記錄中存在 Gap Locks 而需要生成 Insert Intention Locks 等待。此時(shí)很明顯就形成了死鎖,session 1 生成插入意向鎖等待 session 2 和 session 3 上的 Gap 鎖釋放,而 session 2 同樣生成插入意向鎖等待 session 1 和 session 3 上的 Gap 鎖釋放
在 T4 時(shí)刻檢測(cè)到死鎖后,Mysql 會(huì)選擇其中一個(gè)事務(wù)進(jìn)行回滾,假設(shè)此時(shí) session 2 被回滾,釋放了其持有的所有鎖資源,session 1 可以繼續(xù)執(zhí)行嗎? 很明顯不可以,session 1 還同時(shí)在等待 session 3 上的 Gap 鎖釋放,繼續(xù)阻塞等待
在 T5 時(shí)刻 session 3 開(kāi)始執(zhí)行插入語(yǔ)句,此時(shí)同 T4 時(shí)刻,死鎖形成,session 1 生成的插入意向鎖正在等待 session 3 上的 Gap Locks 釋放,session 3 上生成的插入意向鎖正在等待 session 1 上的 Gap Locks 釋放,此時(shí) session 3 回滾釋放所有鎖資源后,session 1 才可以最終執(zhí)行成功
在完成了三個(gè)并發(fā)線程的死鎖分析后,可能有人會(huì)想雖然有死鎖,但通過(guò)死鎖檢測(cè)可以很快的檢測(cè)出,程序也可以正常的執(zhí)行,這有什么問(wèn)題呢? 其實(shí)上面沒(méi)有問(wèn)題主要是因?yàn)椴l(fā)量較小,死鎖檢測(cè)可以很快檢測(cè)出,如果此時(shí)將并發(fā)量擴(kuò)大 100 倍甚至 1000 倍后,還會(huì)沒(méi)有問(wèn)題嗎?
看看當(dāng)時(shí)出現(xiàn)線上問(wèn)題時(shí),接口的調(diào)用量情況,
進(jìn)一步在本地模擬 300 個(gè)線程并發(fā)執(zhí)行,因人腦并發(fā)分析所有事務(wù)的執(zhí)行情況的話會(huì)非常復(fù)雜,本次只以事務(wù) 1 為一個(gè)點(diǎn)來(lái)進(jìn)行分析
從圖中可以看到當(dāng) T1 在執(zhí)行插入語(yǔ)句時(shí),需要等待 T2- T101 上持有的 Gap Locks 釋放,之后 T2 - T6 可能同時(shí)執(zhí)行插入語(yǔ)句,然后進(jìn)行死鎖檢測(cè),事務(wù)回滾,看著似乎只要后續(xù)有事務(wù)執(zhí)行了插入語(yǔ)句就會(huì)執(zhí)行死鎖回滾,正常運(yùn)行,但在死鎖檢測(cè)的過(guò)程中還會(huì)有新事務(wù)(T101 - T 200 )獲取到 Gap Locks,造成鎖等待隊(duì)列中的事務(wù)越來(lái)越多,而 Mysql 的整體死鎖檢測(cè)時(shí)間復(fù)雜度為 O(n^2),鎖等待隊(duì)列中的事務(wù)較多時(shí),每一次有新事務(wù)進(jìn)行鎖等待,死鎖檢測(cè)都需要遍歷鎖等待隊(duì)列中在其之前等待的事務(wù),判斷是否會(huì)因自己的加入形成環(huán),此時(shí)檢測(cè)會(huì)非常消耗 CPU 資源,造成數(shù)據(jù)庫(kù)整體性能下降,死鎖檢測(cè)耗時(shí)增加,Mysql 活躍連接數(shù)大幅增加,并且因鎖等待而連接無(wú)法釋放,最終造成應(yīng)用層連接池被打滿
綜上分析,本次出現(xiàn)問(wèn)題的最主要原因是在短時(shí)間內(nèi)存在大并發(fā)的請(qǐng)求對(duì)同一行數(shù)據(jù)進(jìn)行先刪除再插入操作(先更新再插入同理),造成了死鎖等待,應(yīng)用層連接池被打滿,大量上游請(qǐng)求超時(shí)重試,進(jìn)一步導(dǎo)致鎖等待,最終影響了所有依賴該數(shù)據(jù)庫(kù)的業(yè)務(wù)
因此對(duì)于未來(lái)在業(yè)務(wù)代碼中存在相似邏輯的地方,一定要做好防重校驗(yàn),避免短時(shí)間內(nèi)存在對(duì)同一行數(shù)據(jù)的先更新再插入的并發(fā)操作。同時(shí)在可重復(fù)讀隔離別下,更新和刪除操作默認(rèn)都會(huì)添加 Next-Key Locks,間隙鎖的引入使得死鎖問(wèn)題在并發(fā)情況下很容易出現(xiàn),這也是在業(yè)務(wù)邏輯實(shí)現(xiàn)上需要考慮的問(wèn)題。
總結(jié)
本文以一個(gè)線上問(wèn)題為背景,對(duì) Mysql 中的各種鎖機(jī)制進(jìn)行了詳細(xì)的總結(jié),分析了各個(gè)鎖的加鎖時(shí)機(jī)和具體使用場(chǎng)景,其中特別要注意間隙鎖的使用,因間隙鎖和間隙鎖之間不互斥,當(dāng)多個(gè)事務(wù)之間并發(fā)執(zhí)行時(shí)很容易形成死鎖
作者:京東物流 張弓言文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-746171.html
來(lái)源:京東云開(kāi)發(fā)者社區(qū) 自猿其說(shuō)Tech 轉(zhuǎn)載請(qǐng)注明來(lái)源文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-746171.html
到了這里,關(guān)于記一次線上問(wèn)題引發(fā)的對(duì) Mysql 鎖機(jī)制分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!