死鎖場景1:并發(fā)插入重復(fù)key
場景重現(xiàn)
表結(jié)構(gòu)如下:
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
三個(gè)session按順序執(zhí)行下面的操作
Session 1:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 2:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 3:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 1:
ROLLBACK;
然后Session2 和 Session3發(fā)生死鎖
原因分析
分析之前,我們要知道:
- 隱式鎖:事務(wù)發(fā)現(xiàn)當(dāng)前記錄沒有鎖競爭,則暫時(shí)不加鎖,直接操作
- 隱式鎖轉(zhuǎn)換為顯示鎖:后續(xù)發(fā)現(xiàn)鎖競爭的事務(wù),會(huì)為之前的事務(wù)加鎖
- 發(fā)生"duplicate-key"錯(cuò)誤,則會(huì)在"duplicate index record"上設(shè)置共享GAP鎖
- 刪除記錄會(huì)導(dǎo)致鎖繼承
- mysql表有2個(gè)偽記錄:infimum表示最小記錄,supremum表示最大記錄
- Session1,Session2,Session3簡稱:S1,S2,S3。主鍵為1的記錄,簡稱:row1
基于以上事實(shí),我們梳理下發(fā)生死鎖的原因
-
S1插入row1時(shí),由于沒有鎖競爭,不加鎖直接插入
image.png
- S2/S3插入row1時(shí),發(fā)現(xiàn)row1上有活動(dòng)的事務(wù)S1,幫S1在row1上加記錄鎖

image.png
- S2/S3插入row1時(shí),發(fā)現(xiàn)主鍵重復(fù), 對(duì)row1請(qǐng)求"共享next-key鎖"

image.png
- "共享next-key鎖"與"記錄鎖"沖突,S2/S2進(jìn)入等待隊(duì)列

image.png
- S1回滾:刪除row1,將row1上的鎖繼承給supremum,然后授予給S2/S3

image.png
- row1被刪除后,S2/S3重新定位到infinum,對(duì)其下一條記錄supremum加"插入意向鎖"
? S2對(duì)supremum加"插入意向鎖",而S3持有supremum的共享GAP鎖
? S3對(duì)supremum加"插入意向鎖",而S2持有supremum的共享GAP鎖
? 于是S2與S3發(fā)生死鎖

image.png
關(guān)鍵代碼分析
"............"表示省略部分代碼
1 S2/S3 插入流程代碼分析
1.1 分析入口:row0ins.cc類的row_ins_clust_index_entry_low方法
/**
調(diào)用路徑是:row_insert_for_mysql -> row_insert_for_mysql_using_ins_graph
-> row_ins_step -> row_ins -> row_ins_index_entry_step
-> row_ins_index_entry -> row_ins_clust_index_entry
-> row_ins_clust_index_entry_low
*/
dberr_t row_ins_clust_index_entry_low(...........){
//如果發(fā)生"duplicate-key"錯(cuò)誤
if (!index->allow_duplicates && n_uniq &&
(cursor->up_match >= n_uniq || cursor->low_match >= n_uniq)) {
............
// 此方法會(huì)請(qǐng)求共享鎖,并加入等待鎖的隊(duì)列
err = row_ins_duplicate_error_in_clust(flags, cursor, entry, thr, &mtr);
if (err != DB_SUCCESS) {
err_exit:
mtr.commit();
//如果共享鎖無法立即獲取,直接返回,上層方法會(huì)將此線程休眠
//Session2和Session3無法立即獲取共享鎖后,會(huì)從這里返回后休眠
goto func_exit;
}
............
}
............
/**
處理完"duplicate-key"的情況后,開始執(zhí)行插入流程
獲取了共享鎖后的Session2和Session3,會(huì)在這個(gè)方法里面申請(qǐng)“插入意向鎖”,從而發(fā)生死鎖
后續(xù)調(diào)用路徑:-> btr_cur_ins_lock_and_undo -> lock_rec_insert_check_and_lock
-> lock_rec_insert_check_and_lock
-> rec_lock.add_to_waitq(此方法會(huì)檢測死鎖)
*/
err = btr_cur_optimistic_insert(flags, cursor, &offsets, &offsets_heap,
entry, &insert_rec, &big_rec, n_ext, thr,
&mtr);
............
}
1.2 請(qǐng)求"共享GAP鎖"的相關(guān)代碼分析
/**
為重復(fù)記錄,設(shè)置共享鎖,如果鎖沖突,則加入等待隊(duì)列
Checks if a unique key violation error would occur at an index entry
insert. Sets shared locks on possible duplicate records. Works only
for a clustered index!
*/
static row_ins_duplicate_error_in_clust(...........)
{
...........
...........
//隔離級(jí)別>RC 且 不是特殊表,則加LOCK_ORDINARY鎖(LOCK_ORDINARY就是next-key鎖)
lock_type = ((trx->isolation_level <= TRX_ISO_READ_COMMITTED) ||
(cursor->index->table->skip_gap_locks()))
? LOCK_REC_NOT_GAP
: LOCK_ORDINARY;
/* We set a lock on the possible duplicate: this
is needed in logical logging of MySQL to make
sure that in roll-forward we get the same duplicate
errors as in original execution */
if (flags & BTR_NO_LOCKING_FLAG) {
/* Do nothing if no-locking is set */
err = DB_SUCCESS;
} else if (trx->duplicates) {
/* If the SQL-query will update or replace
duplicate key we will take X-lock for
duplicates ( REPLACE, LOAD DATAFILE REPLACE,
INSERT ON DUPLICATE KEY UPDATE). */
// INSERT ON DUPLICATE KEY UPDATE 等語句需要加”排他鎖“
err =
row_ins_set_exclusive_rec_lock(lock_type, btr_cur_get_block(cursor),
rec, cursor->index, offsets, thr);
} else {
// 普通insert 加”共享鎖“
// 后續(xù)調(diào)用路徑 -> lock_clust_rec_read_check_and_lock
// -> lock_rec_lock -> lock_rec_lock_slow -> rec_lock.add_to_waitq
err = row_ins_set_shared_rec_lock(lock_type, btr_cur_get_block(cursor),
rec, cursor->index, offsets, thr);
}
...........
...........
}
// S2/S3通過此方法請(qǐng)求"共享GAP鎖"
static dberr_t lock_rec_lock_slow(...........) {
...........
if (lock_rec_has_expl(mode, block, heap_no, trx)) {
/* The trx already has a strong enough lock on rec: do
nothing */
err = DB_SUCCESS;
} else {
//判斷“mysql行”上是否有和當(dāng)前鎖模式?jīng)_突的鎖
const lock_t *wait_for =
lock_rec_other_has_conflicting(mode, block, heap_no, trx);
if (wait_for != NULL) {
//Session1已經(jīng)獲取了row1的記錄鎖,因此與當(dāng)前鎖模式?jīng)_突
switch (sel_mode) {
...........
case SELECT_ORDINARY:
//創(chuàng)建鎖對(duì)象
RecLock rec_lock(thr, index, block, heap_no, mode);
//進(jìn)入等待隊(duì)列
err = rec_lock.add_to_waitq(wait_for);
break;
}
}else if (!impl) {
//顯示鎖
lock_rec_add_to_queue(LOCK_REC | mode, block, heap_no, index, trx);
err = DB_SUCCESS_LOCKED_REC;
} else {
//隱式鎖
err = DB_SUCCESS;
}
............
............
}
1.3 請(qǐng)求"插入意向鎖"的相關(guān)代碼分析
dberr_t lock_rec_insert_check_and_lock(..........)
{
//獲取當(dāng)前定位記錄的下一條記錄
//對(duì)應(yīng)我們的例子,回滾后row1已經(jīng)被刪除
//因此s2/s3重新定位獲取的:rec就是infimum, next_rec就是supremum
const rec_t *next_rec = page_rec_get_next_const(rec);
//next_rec的heap_no
ulint heap_no = page_rec_get_heap_no(next_rec);
...........
//獲取的鎖模式:插入意向鎖
const ulint type_mode = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION;
//判斷next_rec上是否有和“插入意向鎖”沖突的鎖
//對(duì)應(yīng)我們的例子:S1回滾后,supremum列繼承了row1的“共享GAP鎖”
//此時(shí)S2/S3定位到的next_rec就是supremum,因此發(fā)生“鎖沖突”
const lock_t *wait_for =
lock_rec_other_has_conflicting(type_mode, block, heap_no, trx);
if (wait_for != NULL) {
RecLock rec_lock(thr, index, block, heap_no, type_mode);
trx_mutex_enter(trx);
trx->owns_mutex = true;
//創(chuàng)建鎖,進(jìn)入等待隊(duì)列,此方法內(nèi)部會(huì)進(jìn)行死鎖檢測
err = rec_lock.add_to_waitq(wait_for);
trx->owns_mutex = false;
trx_mutex_exit(trx);
} else {
//沒有沖突,不加鎖(隱式鎖)
err = DB_SUCCESS;
}
...........
}
2 S1 回滾流程代碼分析
調(diào)用路徑:trx_rollback_for_mysql —> trx_rollback_to_savepoint_low
static void trx_rollback_to_savepoint_low(...........)
{
............
//此方法會(huì)執(zhí)行”鎖繼承“
//調(diào)用路徑:btr_cur_optimistic_delete_func -> lock_rec_inherit_to_gap
//對(duì)應(yīng)我們的例子:row1刪除后,row1的gap鎖繼承給了supremum
//由(infimum,row1] 變成了 (infimum,supremum]
que_run_threads(thr);
.............
}
if (savept == NULL) {
//此方法會(huì)執(zhí)行”鎖授予“
//調(diào)用路徑:lock_rec_dequeue_from_page —> lock_rec_grant
//對(duì)應(yīng)我們的例子:將共享GAP鎖(infimum,supremum],授予給了S2/S3
trx_rollback_finish(trx);
MONITOR_INC(MONITOR_TRX_ROLLBACK);
} else {
trx->lock.que_state = TRX_QUE_RUNNING;
MONITOR_INC(MONITOR_TRX_ROLLBACK_SAVEPOINT);
}
............
}
