MySQL 并發(fā) replace into 導(dǎo)致 insert intention 與 gap lock 形成死鎖
引言
本文介紹一個(gè)在 pt-osc 執(zhí)行期間發(fā)生的死鎖案例,其實(shí)之前的文章 并發(fā) replace into 導(dǎo)致 supremum X 鎖與插入意向鎖形成死鎖 中也分析過(guò)相關(guān)案例,但由于理解不到位導(dǎo)致根因分析并不全面,因此本文進(jìn)一步分析該類(lèi)型的死鎖,包括死鎖發(fā)生的原因與優(yōu)化方法。
現(xiàn)象
時(shí)間:2024-03-13 20:48:29
數(shù)據(jù)庫(kù)版本:MySQL 5.7.21
現(xiàn)象:pt-osc 執(zhí)行 DDL 期間多次發(fā)生死鎖
分析
死鎖日志
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-03-13 20:48:29 0x7ff6fb2f8700
*** (1) TRANSACTION:
TRANSACTION 385752159, ACTIVE 0 sec inserting
mysql tables in use 2, locked 2
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 17811400, OS thread handle 140698772580096, query id 25019811085 x.x.x.x eclp_po1_rw update
REPLACE INTO `eclp_po1`.`_po_main_new` (`id`, `parent_no`, `po_no`, `unit_flag`, `unit_rule`, `bill_of_lading`, `dept_no`, `dept_id`, `dept_name`, `seller_id`, `seller_no`, `seller_name`, `org_id`, `org_no`, `org_name`, `distribution_id`, `distribution_no`, `distribution_name`, `warehouse_id`, `warehouse_no`, `warehouse_name`, `out_warehouse_no`, `out_warehouse_name`, `target_warehouse_id`, `expect_arrival_time`, `po_type`, `po_status`, `po_cancel_status`, `po_sign`, `out_source_no`, `out_po_no`, `out_seller_no`, `difference_remark`, `contacts`, `contacts_address`, `supplier_id`, `supplier_no`, `supplier_name`, `supplier_contacts`, `approval_time`, `approval_user`, `po_dl_result`, `po_dl_message`, `receive_level`, `isv_replenish_type`, `temperature`, `temperature_value`, `create_time`, `products_code`, `receive_box_number`, `products_name`, `update_time`, `create_user`, `update_user`, `yn`, `ts`,
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 245 page no 234753 n bits 384 index po_no of table `eclp_po1`.`_po_main_new` trx id 385752159 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 313 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 16; hex 45504c34343138303834393836363939; asc xxx;;
1: len 8; hex 80000404aa62df4b; asc b K;;
*** (2) TRANSACTION:
TRANSACTION 385752158, ACTIVE 0 sec inserting
mysql tables in use 2, locked 2
5 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 2
MySQL thread id 17811470, OS thread handle 140698752878336, query id 25019811042 x.x.x.x eclp_po1_rw update
REPLACE INTO `eclp_po1`.`_po_main_new` (`id`, `parent_no`, `po_no`, `unit_flag`, `unit_rule`, `bill_of_lading`, `dept_no`, `dept_id`, `dept_name`, `seller_id`, `seller_no`, `seller_name`, `org_id`, `org_no`, `org_name`, `distribution_id`, `distribution_no`, `distribution_name`, `warehouse_id`, `warehouse_no`, `warehouse_name`, `out_warehouse_no`, `out_warehouse_name`, `target_warehouse_id`, `expect_arrival_time`, `po_type`, `po_status`, `po_cancel_status`, `po_sign`, `out_source_no`, `out_po_no`, `out_seller_no`, `difference_remark`, `contacts`, `contacts_address`, `supplier_id`, `supplier_no`, `supplier_name`, `supplier_contacts`, `approval_time`, `approval_user`, `po_dl_result`, `po_dl_message`, `receive_level`, `isv_replenish_type`, `temperature`, `temperature_value`, `create_time`, `products_code`, `receive_box_number`, `products_name`, `update_time`, `create_user`, `update_user`, `yn`, `ts`,
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 245 page no 234753 n bits 384 index po_no of table `eclp_po1`.`_po_main_new` trx id 385752158 lock_mode X locks gap before rec
Record lock, heap no 313 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 16; hex 45504c34343138303834393836363939; asc xxx;;
1: len 8; hex 80000404aa62df4b; asc b K;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 245 page no 234753 n bits 384 index po_no of table `eclp_po1`.`_po_main_new` trx id 385752158 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 313 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 16; hex 45504c34343138303834393836363939; asc xxx;;
1: len 8; hex 80000404aa62df4b; asc b K;;
*** WE ROLL BACK TRANSACTION (1)
其中:
- REPLACE INTO _po_main_new,表明是 pt-osc 的 SQL,其中 SQL 截?cái)啵?/li>
- eclp_po1_rw,顯示是業(yè)務(wù)賬號(hào),表明對(duì)應(yīng) pt-osc 增量數(shù)據(jù)同步階段;
- index po_no,加鎖索引;
- info bits 0,表明沒(méi)有發(fā)生標(biāo)記刪除。結(jié)合顯示 ACTIVE 0 sec inserting,因此判斷對(duì)應(yīng) insert,不是 update;
- 日志顯示間隙鎖與插入意向鎖沖突導(dǎo)致死鎖。
表結(jié)構(gòu)
mysql> show create table eclp_po1.po_main \G
*************************** 1. row ***************************
Table: po_main
Create Table: CREATE TABLE `po_main` (
`id` bigint(20) NOT NULL COMMENT '主鍵',
`parent_no` varchar(50) DEFAULT NULL COMMENT '采購(gòu)父單號(hào)',
`po_no` varchar(50) NOT NULL COMMENT '采購(gòu)單號(hào)(ECLP采購(gòu)單號(hào))',
...
PRIMARY KEY (`id`),
UNIQUE KEY `po_no` (`po_no`) USING BTREE,
...
其中:
- index po_no,二級(jí)唯一索引
復(fù)現(xiàn)
測(cè)試數(shù)據(jù)
數(shù)據(jù)庫(kù)版本版本:5.7.24
事務(wù)隔離級(jí)別:RR
測(cè)試表結(jié)構(gòu)
mysql> show create table t_lock \G
*************************** 1. row ***************************
Table: t_lock
Create Table: CREATE TABLE `t_lock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT '0',
`b` int(11) DEFAULT '0',
`c` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_a` (`a`),
KEY `idx_b` (`b`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> select * from t_lock;
+----+------+------+------+
| id | a | b | c |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
| 5 | 5 | 5 | 5 |
| 9 | 9 | 9 | 9 |
+----+------+------+------+
3 rows in set (0.00 sec)
流程
三個(gè)事務(wù)并發(fā)向同一個(gè)間隙插入數(shù)據(jù)。
時(shí)刻 | session 1 | session 2 | session 3 |
1 | begin; replace into t_lock values(2,2,2,2); | ||
2 | begin; replace into t_lock values(3,3,3,3); blocked | ||
3 | begin; replace into t_lock values(4,4,4,4); blocked | ||
4 | rollback; / commit; | ||
5 | Query OK, 1 row affected | Deadlock found |
其中:
- replace 對(duì)應(yīng) insert;
- 與 insert 不同之處在于事務(wù) 1 提交以后也會(huì)觸發(fā)死鎖,原因是事務(wù) 2 與 3 持有的間隙鎖與事務(wù) 1 的回滾操作無(wú)關(guān);
- 研發(fā)反饋不存在回滾操作,因此判斷提交操作觸發(fā)死鎖。
由于死鎖日志與前文相同,因此這里不再展示,但是為了分析死鎖,下面分析鎖等待的原因。
鎖信息
時(shí)刻 3 查看事務(wù) 1 與 2 的鎖信息
圖片
其中:
- 事務(wù) 2 與 3 的鎖信息相同,因此僅展示事務(wù) 1 與 2;
- 事務(wù) 1 與 2 均已持有間隙鎖,但是事務(wù) 2 發(fā)生插入意向鎖等待;
時(shí)刻 3 查看事務(wù) 2 與 3 的鎖等待信息
圖片
其中:
- 顯示有三組鎖等待,右圖中展示事務(wù)間鎖等待的關(guān)系,其中箭頭指向表示事務(wù)等待的鎖;
- 事務(wù) 1 同時(shí)阻塞事務(wù) 2 與 3;
- 事務(wù) 3 同時(shí)等待事務(wù) 1 與 2,顯然事務(wù) 3 指向事務(wù) 2 是死鎖檢測(cè)時(shí)環(huán)中的第一條邊,下一條邊將從事務(wù) 2 指回事務(wù) 3;
- 事務(wù) 1 提交或回滾后,事務(wù) 2 與 3 同時(shí)持有間隙鎖,且均等待插入意向鎖,因此導(dǎo)致死鎖。
原理
pt-osc
online ddl 不會(huì)導(dǎo)致死鎖,兩者最大的區(qū)別是 pt-osc 執(zhí)行 DDL 時(shí)產(chǎn)生的主從延遲小,原因是 online ddl 是在主庫(kù)執(zhí)行完成后從庫(kù)開(kāi)始執(zhí)行,因此天然存在延遲。
為解決這個(gè)問(wèn)題,pt-osc 支持主從“同時(shí)”執(zhí)行,缺點(diǎn)是全量數(shù)據(jù)和增量數(shù)據(jù)可能發(fā)生亂序。為解決這個(gè)問(wèn)題,將全量數(shù)據(jù)中 insert 轉(zhuǎn)換成 insert ignore,將增量數(shù)據(jù)中 insert / update 轉(zhuǎn)換成 replace into,從而實(shí)現(xiàn)數(shù)據(jù)的最終一致性,缺點(diǎn)是改寫(xiě) SQL 后加鎖變復(fù)雜,可能導(dǎo)致死鎖。
replace 語(yǔ)句加鎖流程
參考【許海波】大佬的文章 REPLACE語(yǔ)句死鎖與官方修復(fù)剖析,5.7.19 版本中 replace 語(yǔ)句加鎖流程見(jiàn)下圖。
圖片
整理加鎖規(guī)則見(jiàn)下表。
索引類(lèi)型 | 是否沖突 | 加鎖類(lèi)型 |
主鍵索引 | 否 | 不加鎖 |
是 | next-key lock | |
二級(jí)唯一鍵 | 否 | gap lock |
是 | next-key lock |
其中:
- 二級(jí)唯一鍵沒(méi)有沖突時(shí)加 gap lock 最不合理;
- 5.7.26 中針對(duì)該問(wèn)題進(jìn)行了修復(fù)。
在分析修復(fù)實(shí)現(xiàn)前首先介紹該修復(fù)帶來(lái)的新問(wèn)題,具體是 5.7.26 中新增的鎖。
不同版本 insert 加鎖對(duì)比
參考以下兩篇文章:
- 【操盛春】大佬的文章 答讀者問(wèn):唯一索引沖突,為什么主鍵的 supremum 記錄會(huì)加 next-key 鎖?
- 【高鵬】大佬的文章 MySQL:新版本RR模式下特殊的鎖行為一列
其中都提到了一個(gè)現(xiàn)象,插入二級(jí)唯一鍵時(shí)如果發(fā)生唯一鍵沖突,內(nèi)部回滾刪除主鍵時(shí)會(huì)給回滾記錄加鎖,并在回滾完成后將鎖繼承到下一行,加鎖類(lèi)型是 gap lock。注意如果下一行是 sup 偽列,加鎖類(lèi)型是 next-key lock,從而導(dǎo)致大于當(dāng)前最大主鍵的新插入的主鍵值均無(wú)法插入,出現(xiàn)大面積堵塞。
其中的重點(diǎn)是主鍵刪除過(guò)程中的以下兩步操作:
- 將隱式鎖轉(zhuǎn)換成顯式鎖
- 發(fā)生鎖繼承
文中提到這個(gè)現(xiàn)象是在 5.7.29+ 版本中發(fā)現(xiàn),5.7.22 中沒(méi)有該現(xiàn)象。
因此下面測(cè)試對(duì)比 5.7.24 與 5.7.33 兩個(gè)版本中 insert 發(fā)生唯一鍵沖突時(shí)的加鎖規(guī)則,其中使用兩個(gè)事務(wù)依次模擬主鍵沖突與二級(jí)唯一鍵沖突。
由于 insert 沒(méi)有唯一鍵沖突時(shí)使用隱式鎖,可以認(rèn)為不加鎖,因此僅測(cè)試唯一鍵沖突的場(chǎng)景。
測(cè)試數(shù)據(jù)
drop table `e`;
CREATE TABLE `e` (
`id` int NOT NULL AUTO_INCREMENT,
`c` int DEFAULT NULL,
`d` int DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;
insert into e(c,d) values(10,10),(20,20);
主鍵沖突
insert into e(id,c,d) values(11,11,11);
結(jié)果
圖片
其中:
- 主鍵沖突時(shí),5.7.24 next-key lock,5.7.33 record lock。
二級(jí)唯一鍵沖突
inser into e(c,d) values(10,10);
結(jié)果
圖片
其中:
- 二級(jí)唯一鍵沖突時(shí),5.7.24 next-key lock,5.7.33 也是 next-key lock;
- 5.7.33 新增主鍵索引上的鎖,具體是 X 型 next-key lock,且加鎖到右邊界 supremum,因此大于等于 12 的主鍵值都無(wú)法插入;
- 5.7.33 主鍵索引上新增鎖的原因是主鍵回滾,期間先將隱式鎖轉(zhuǎn)換成顯式鎖,然后發(fā)生鎖繼承;
- 5.7.24 同樣主鍵回滾,但是主鍵索引上沒(méi)有鎖的原因是使用隱式鎖,沒(méi)有轉(zhuǎn)換成顯式鎖,因此鎖繼承時(shí)不加鎖;
- 因此判斷高版本中針對(duì)二級(jí)唯一鍵沖突檢查加鎖沒(méi)有優(yōu)化,針對(duì)主鍵沖突檢查加鎖粒度減小,針對(duì)主鍵回滾加鎖粒度增大。
但是通常主鍵值自增,因此二級(jí)索引加鎖粒度變大可能導(dǎo)致死鎖增多。
代碼分析
下面分析 5.7.26 中修改唯一鍵沖突時(shí)加鎖規(guī)則對(duì)應(yīng)的 commit,具體是 Bug #25966845 INSERT ON DUPLICATE KEY GENERATE A DEADLOCK。
相關(guān)描述如下所示。
Bug #25966845 INSERT ON DUPLICATE KEY GENERATE A DEADLOCK
PROBLEM
-------
When we do a partial rollback of the tuple due to "on duplicate
key update" clause we were not maintaining serilizability,
so another connection waiting on the row could update it and
cause wrong results.
FIX
---
1) During partial rollback ,while deleting the tuple convert implicit
lock on the tuple to explicit lock ,so that no connection can
get hold of the tuple during delete. This lock is later inherited
by the next record.
...
其中:
- 問(wèn)題,insert duplicate 語(yǔ)句在死鎖回滾事務(wù)時(shí)沒(méi)有保證串行,導(dǎo)致回滾期間其他鎖等待事務(wù)可以更新數(shù)據(jù);
- 修復(fù),將 delete 持有的隱式鎖轉(zhuǎn)換成顯式鎖,并隨后發(fā)生鎖繼承,從而阻塞其他事務(wù)在回滾期間更新數(shù)據(jù)。
release notes 中顯示 5.7.26 中該 commit 用于修復(fù) bug,但是暫未根據(jù) commit 找到對(duì)應(yīng)要修復(fù)的 bug。
Two sessions concurrently executing an INSERT ... ON DUPLICATE KEY UPDATE operation generated a deadlock. During partial rollback of a tuple, another session could update it. The fix for this bug reverts fixes for Bug #11758237, Bug #17604730, and Bug #20040791. (Bug #25966845)
下面分析該 commit 中的代碼實(shí)現(xiàn)。
最明顯的是新增row_convert_impl_to_expl_if_needed函數(shù),相關(guān)代碼如下所示。
首先是row_convert_impl_to_expl_if_needed函數(shù)的定義。
void
row_convert_impl_to_expl_if_needed(
/*===============================*/
btr_cur_t* cursor, /*!< in: cursor to record */
undo_node_t* node) /*!< in: undo node */
{
ulint* offsets = NULL;
// IODKU 表示 insert duplicate
/* In case of partial rollback implicit lock on the
record is released in the middle of transaction, which
can break the serializability of IODKU and REPLACE
statements. Normal rollback is not affected by this
becasue we release the locks after the rollback. So
to prevent any other transaction modifying the record
in between the partial rollback we convert the implicit
lock on the record to explict. When the record is actually
deleted this lock be inherited by the next record. */
// 判讀事務(wù)隔離級(jí)別
if (!node->partial
|| (node->trx == NULL)
|| node->trx->isolation_level < TRX_ISO_REPEATABLE_READ){
return;
}
...
// 將隱式鎖轉(zhuǎn)換成顯式鎖
lock_rec_convert_active_impl_to_expl(block, rec, index,
offsets,node->trx,heap_no);
}
其中:
- RR 事務(wù)隔離級(jí)別下調(diào)用lock_rec_convert_active_impl_to_expl函數(shù)將隱式鎖轉(zhuǎn)換成顯式鎖;
- RC 事務(wù)隔離級(jí)別下不轉(zhuǎn)換。
然后是row_convert_impl_to_expl_if_needed函數(shù)的調(diào)用。
圖片
其中:
- 刪除主鍵與二級(jí)索引時(shí)都調(diào)用row_convert_impl_to_expl_if_needed函數(shù),包括 insert、insert duplicate、replace。
到目前為止,鎖的粒度在增大,但實(shí)際上并非始終如此,具體在唯一性檢查時(shí)鎖的粒度有減小。
首先是主鍵唯一性檢查前加鎖,對(duì)比 5.7.24 與 5.7.26 代碼中row_ins_duplicate_error_in_clust函數(shù)。
圖片
其中:
- 對(duì)于 RR,5.7.24,lock_type = next-key lock;
- 5.7.26,lock_type = record lock,包括 insert、insert duplicate、replace。
因此主鍵索引加鎖粒度減小,具體是從 next-key lock 減小為 record lock,也就是移除了 gap lock。
然后是二級(jí)唯一鍵唯一性檢查前加鎖,對(duì)比 5.7.24 與 5.7.26 代碼中row_ins_sec_index_entry_low函數(shù)。
圖片
其中:
- 唯一鍵沖突時(shí),加鎖類(lèi)型不變,保持 next-key lock,包括 insert、insert duplicate、replace;
- 唯一鍵不沖突時(shí),5.7.24 中 insert 不加鎖,insert duplicate / replace 加 gap lock,5.7.26 中統(tǒng)一不加鎖。
因此對(duì)于 insert 語(yǔ)句,唯一鍵沖突時(shí),5.7.26 中未必鎖減少,下面測(cè)試對(duì)于 replace 語(yǔ)句,加鎖規(guī)則的變化。
不同版本 replace 加鎖對(duì)比
下表中對(duì)比 insert 與 insert on duplicate 的加鎖規(guī)則。
場(chǎng)景 | 唯一鍵不沖突 | 唯一鍵沖突 |
insert | 隱式鎖 | S 型 next-key lock |
insert on duplicate(5.7.26-) | X 型 gap lock | X 型 next-key lock |
insert on duplicate(5.7.26+) | 不加鎖 | X 型 next-key lock |
其中:
- 主要改動(dòng)點(diǎn)是將唯一鍵不沖突時(shí)的間隙鎖移除,下面測(cè)試驗(yàn)證。
操作流程
session 1 | session 2 |
begin; replace into t_lock values(2,2,2,2); | |
begin; replace into t_lock values(3,3,3,3); |
5.7.33 二級(jí)唯一鍵不沖突時(shí)不加鎖。
圖片
因此,5.7.33 沒(méi)有沖突時(shí)不加鎖,有沖突時(shí)加鎖不變,且主鍵回滾后鎖粒度增大。
總結(jié)
回顧 insert 二級(jí)唯一鍵沖突后的異常處理,堆棧如下所示。
// 判斷是否報(bào)錯(cuò),唯一鍵沖突時(shí) err = DB_DUPLICATE_KEY
if (err != DB_SUCCESS)
// 處理異常
row_mysql_handle_errors
// 插入記錄導(dǎo)致唯一索引沖突,需要回滾
trx_rollback_to_savepoint
// 回滾 insert 操作
row_undo_ins
// 刪除主鍵索引
row_undo_ins_remove_clust_rec
// 5.7.24 中沒(méi)有該函數(shù),5.7.26 中新函數(shù)
row_convert_impl_to_expl_if_needed
// 把主鍵索引記錄上的隱式鎖轉(zhuǎn)換為顯式鎖
lock_rec_convert_impl_to_expl
// 先進(jìn)行樂(lè)觀(guān)刪除,如果樂(lè)觀(guān)刪除失敗,后面會(huì)進(jìn)行悲觀(guān)刪除
btr_cur_optimistic_delete
// 鎖繼承
lock_rec_inherit_to_gap
// 真正刪除
page_cur_delete_rec
其中:
- 刪除主鍵前先將要?jiǎng)h除記錄上的隱式鎖轉(zhuǎn)換成顯式鎖,作為過(guò)渡,用于下一步的鎖轉(zhuǎn)移;
- 然后在真正刪除前將要?jiǎng)h除記錄上的鎖轉(zhuǎn)移到下一行記錄,加鎖類(lèi)型為 gap lock,其中如果下一行是 sup 偽列,加鎖類(lèi)型修改為 next-key lock。
總結(jié)下 5.7.26 中唯一性檢查加鎖規(guī)則的調(diào)整:
- 主鍵索引,沒(méi)有唯一鍵沖突時(shí)不加鎖,有唯一鍵沖突時(shí)移除了 gap lock,不論隔離級(jí)別,全部加 record lock,而不是 next-key lock;
- 二級(jí)唯一索引,沒(méi)有唯一鍵沖突時(shí)不加鎖,不論隔離級(jí)別,移除了 gap lock,有唯一鍵沖突時(shí)保持不變,不論隔離級(jí)別,依然是 next-key lock;
- 刪除主鍵索引與二級(jí)索引時(shí)都需要先將插入時(shí)的隱式鎖轉(zhuǎn)換成顯式鎖,因此在刪除前鎖繼承時(shí)將產(chǎn)生 gap lock,從而阻塞并發(fā)插入。
到這里,可以提出一個(gè)問(wèn)題,那就是加鎖規(guī)則的調(diào)整可以保證數(shù)據(jù)唯一嗎?
參考 #issue 68021 MySQL unique check 的問(wèn)題,首先解釋低版本中加 next-key lock 的原因。
原因是二級(jí)唯一索引插入記錄時(shí),分為兩個(gè)階段,包括唯一性檢查與插入操作,因此通過(guò)在兩階段之間加鎖保證操作的原子性,禁止其他事務(wù)在同一間隙中插入數(shù)據(jù),從而避免檢查沒(méi)有沖突,但是插入時(shí)出現(xiàn)沖突。
文中介紹針對(duì)二級(jí)索引加 next-key lock 的問(wèn)題,官方之前做過(guò)改動(dòng),具體將 next-key lock 改成 record lock,但是導(dǎo)致唯一性約束失效的嚴(yán)重 bug#73170,因此后來(lái)又將該 fix revert。
具體如下圖所示,其中紅色表示 record 已經(jīng)被刪除,藍(lán)色表示未被刪除。tuple 中第一個(gè)元素表示二級(jí)唯一鍵,第二個(gè)元素表示主鍵。
圖片
其中:
- 假如同時(shí)插入兩個(gè) record (13000, 99)、( 13000, 120),如果將 next-key lock 改成 record lock;
- 唯一性檢查時(shí)分別對(duì) (13000, 100)、(13000, 102)、(13000, 108) ... (13000, 112) 所有的二級(jí)索引加 S record lock,insert 時(shí)對(duì) (13000, 100) 加 GAP | insert_intention lock;
- 由于鎖不沖突,因此這兩個(gè) record 都可以同時(shí)插入成功,就造成了unique key 約束失效了。
因此,5.7.26 中針對(duì)二級(jí)唯一鍵沖突的場(chǎng)景,加鎖規(guī)則不變,依然是 next-key lock,因此不會(huì)導(dǎo)致唯一性約束失效。
此外,主鍵唯一性檢查時(shí)不需要 gap lock 的原因是理論上同一時(shí)間二級(jí)唯一鍵可能有重復(fù),而主鍵不會(huì)有重復(fù)。
原因是二級(jí)索引與主鍵索引唯一性檢查與記錄復(fù)用判斷的標(biāo)準(zhǔn)不同:
- 二級(jí)索引刪除后,再次插入相同唯一鍵時(shí)如果主鍵不同,原記錄不可以復(fù)用。為了滿(mǎn)足 MVCC 的需求,delete-marked record 不能馬上刪除,因此理論上可能有重復(fù)的唯一鍵;
- 主鍵索引刪除后,再次插入相同主鍵時(shí)可以直接服用原記錄。為了滿(mǎn)足 MVCC 的需求,delete-marked record 可以從 undo log 中查到,因此理論上不會(huì)有重復(fù)的主鍵。
結(jié)論
死鎖的根本原因是 replace 語(yǔ)句在二級(jí)唯一鍵不沖突時(shí)申請(qǐng) gap lock。
針對(duì)該問(wèn)題,5.7.26 進(jìn)行了優(yōu)化,具體為:
- 主鍵索引,沒(méi)有唯一鍵沖突時(shí)不加鎖,有唯一鍵沖突時(shí)移除了 gap lock,不論隔離級(jí)別,全部加 record lock,而不是 next-key lock;
- 二級(jí)唯一索引,沒(méi)有唯一鍵沖突時(shí)不加鎖,不論隔離級(jí)別,移除了 gap lock,有唯一鍵沖突時(shí)保持不變,不論隔離級(jí)別,依然是 next-key lock;
- 刪除主鍵索引與二級(jí)索引時(shí)都需要先將插入時(shí)的隱式鎖轉(zhuǎn)換成顯式鎖,因此在刪除前鎖繼承時(shí)將產(chǎn)生 gap lock,從而阻塞并發(fā)插入。
其中前兩條減小了加鎖粒度,最后一條增大了加鎖粒度,commit 顯示是為了修復(fù) bug,但是具體 bug 暫未找到。
低版本中二級(jí)索引唯一性檢查加 next-key lock 的原因是為了保證唯一性檢查與插入操作兩個(gè)階段之間操作的原子性。
5.7.26 版本中同樣唯一鍵沖突時(shí),主鍵索引與二級(jí)索引加鎖類(lèi)型不同的原因是:
- 主鍵索引,record lock,原因是理論上不會(huì)有重復(fù)的主鍵;
- 二級(jí)索引,next-key lock,理論上可能有重復(fù)的二級(jí)唯一鍵。
參考教程
- REPLACE語(yǔ)句死鎖與官方修復(fù)剖析
https://zhuanlan.zhihu.com/p/527813412
- 答讀者問(wèn):唯一索引沖突,為什么主鍵的 supremum 記錄會(huì)加 next-key 鎖?
- MySQL:新版本RR模式下特殊的鎖行為一列
- Bug #25966845 INSERT ON DUPLICATE KEY GENERATE A DEADLOCK
https://github.com/mysql/mysql-server/commit/066b6fdd433aa6673622341f1a2f0a3a20018043
- #issue 68021 MySQL unique check 的問(wèn)題
- Replace into加鎖的探究
https://www.jianshu.com/p/497fd78f0b91
- 并發(fā) replace into 導(dǎo)致 supremum X 鎖與插入意向鎖形成死鎖
- pt-online-schema-change的原理解析與應(yīng)用說(shuō)明
https://www.cnblogs.com/xinysu/p/6758170.html