Innodb锁系统 Insert/Delete 锁处理及死锁示例分析
A.INSERT
插入操作在函數btr_cur_optimistic_insert->btr_cur_ins_lock_and_undo->lock_rec_insert_check_and_lock這里進行鎖的判斷,我們簡單的看看這個函數的流程:
1.首先先看看欲插入記錄之后的數據上有沒有鎖,
? ?next_rec = page_rec_get_next_const(rec);
? ?next_rec_heap_no = page_rec_get_heap_no(next_rec);
? ?lock = lock_rec_get_first(block, next_rec_heap_no);
如果lock為空的話,對于非聚集索引,還需要更新page上的最大事務ID。
實際上這里是比較松散的檢查,大并發插入的時候,可以大大的降低創建鎖開銷。
那么其他事務如何發現這些新插入的記錄呢(重復插入同一條記錄顯然應該被阻塞),這里會有個判斷,其他事務去看看
新插入記錄的事務是否還是活躍的,如果還是活躍的,那么就為這個事務主動增加一個鎖記錄(所謂的隱式鎖就是么有鎖。。。。),這個判斷是在檢查是否存在沖突鍵的時候進行的(row_ins_duplicate_error_in_clust->row_ins_set_shared_rec_lock->lock_clust_rec_read_check_and_lock->lock_rec_convert_impl_to_expl
row_ins_set_shared_rec_lock的目的是為了向記錄上加一個LOCK_REC_NOT_GAP的LOCK_S鎖,也就是非GAP的記錄S鎖,如果發現記錄上有X鎖(隱式鎖轉換為LOCK_REC | LOCK_X | LOCK_REC_NOT_GAP),顯然是需要等待的(返回DB_LOCK_WAIT)
?
?
這里設置inherit為FALSE,然后返回DB_SUCCESS;
至于inherit的作用,稍后再議!
?
2.如果lock不為空,這意味著插入記錄的下一個記錄上存在鎖,設置inherit為TRUE.
檢查下一個記錄上的鎖是否和LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION相互沖突
? ? if (lock_rec_other_has_conflicting(
? ? ? ? ? ? LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,
? ? ? ? ? ? block, next_rec_heap_no, trx)) {
?
? ? ? ? /* Note that we may get DB_SUCCESS also here! */
? ? ? ? err = lock_rec_enqueue_waiting(LOCK_X | LOCK_GAP
? ? ? ? ? ? ? ? ? ? ? ? ? ?| LOCK_INSERT_INTENTION,
? ? ? ? ? ? ? ? ? ? ? ? ? ?block, next_rec_heap_no,
? ? ? ? ? ? ? ? ? ? ? ? ? ?index, the);
?
如果有別的事務在下一個記錄上存在顯式的鎖請求,并且和鎖模式( LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION) 沖突,那么
這時候當前事務就需要等待。
?
如果別的事務持有一個GAP類型的鎖以等待插入,我們認為這個鎖和當前插入不沖突。
如何判定鎖之間是否沖突,在上一篇博客(http://mysqllover.com/?p=425)已經介紹過,不再贅述.
?
當檢查到存在沖突的事務,我們就將一個鎖模式為LOCK_X | LOCK_GAP|LOCK_X | LOCK_GAP 加入到請求隊列中(調用函數lock_rec_enqueue_waiting),這里也會負責去檢查死鎖。
?
注意在加入等待隊列的時候可能會返回DB_SUCCESS,例如死鎖發生,但選擇另外一個事務為犧牲者。
?
我們上面提到變量inherit,在存在下一個記錄鎖時會設置為TRUE,在上層函數btr_cur_optimistic_insert,會據此進行判斷:
? ? if (!(flags & BTR_NO_LOCKING_FLAG) && inherit) {
? ? ? ? lock_update_insert(block, *rec);
? ? } ?
注意當我們執行到這部分邏輯時err為DB_SUCCESS,表示鎖檢查已經通過了。
BTR_NO_LOCKING_FLAG表示不做記錄鎖檢查
對于optimistic_insert, flags值為0
對于pessimistic_insert,flags值為BTR_NO_UNDO_LOG_FLAG | BTR_NO_LOCKING_FLAG | BTR_KEEP_SYS_FLAG
因此對于樂觀更新(無需修改BTREE結構),當inherit被設置為TRUE時,總會調用lock_update_insert
根據注釋,lock_update_insert用于繼承下一條記錄的GAP鎖,流程如下
1.首先獲取插入的記錄的heap no和下一條記錄的heap no
? ? ? ? receiver_heap_no = rec_get_heap_no_new(rec);
? ? ? ? donator_heap_no = rec_get_heap_no_new(
? ? ? ? ? ? page_rec_get_next_low(rec, TRUE));
?
其中receiver_heap_no是當前記錄,donator_heap_no是下一條記錄
?
2.調用lock_rec_inherit_to_gap_if_gap_lock函數,將donator_heap_no上所有非INSERT INTENTION且非LOCK_REC_NOT_GAP的記錄鎖
轉移給receiver_heap_no
遍歷donator_heap_no上的所有記錄鎖,繼承鎖的判定條件如下:
? ? ? ? if (!lock_rec_get_insert_intention(lock)
? ? ? ? ? ? && (heap_no == PAGE_HEAP_NO_SUPREMUM
? ? ? ? ? ? || !lock_rec_get_rec_not_gap(lock))) {
?
? ? ? ? ? ? lock_rec_add_to_queue(LOCK_REC | LOCK_GAP
? ? ? ? ? ? ? ? ? ? ? ? ? | lock_get_mode(lock),
? ? ? ? ? ? ? ? ? ? ? ? ? block, heir_heap_no,
? ? ? ? ? ? ? ? ? ? ? ? ? lock->index, lock->trx);
? ? ? ? }
?
注意這里有對SUPREMUM記錄的特殊處理。
也就是說,成功插入了一條記錄,其他持有該記錄的下一條記錄上鎖的事務也會持有新插入記錄上的GAP鎖。
?
說起INSERT,就不得不提到一個有趣的死鎖案例。也就是bug#43210(http://bugs.mysql.com/bug.php?id=43210)
DROP TABLE t1;
CREATE TABLE `t1` (
? `a` int(11) NOT NULL,
? `b` int(11) DEFAULT NULL,
? PRIMARY KEY (`a`),
? KEY `b` (`b`)
) ENGINE=InnoDB;
insert into t1 values (1,19),(8,12);
?
Session 1:
set autocommit = 0;
insert into t1 values (6,12);
?
Session 2:
set autocommit = 0;
insert into t1 values (6,12); ?//阻塞住,同時將session1的鎖轉換為顯示鎖。等待記錄上的S鎖 (查找dup key)
/****
session 1上的轉為顯式鎖:lock_mode X locks rec but not gap
session 2等待的鎖:lock mode S locks rec but not gap waiting
***/
Session 3:
set autocommit = 0;
insert into t1 values (6,12); ?//阻塞住,和session2 同樣等待S鎖,lock mode S locks rec but not gap waiting
?
Session 1:
ROLLBACK;
?
Session 2:
執行插入成功
這時候Session 2持有的鎖為主鍵記錄上的:
lock mode S locks rec but not gap
lock mode S locks gap before rec
lock_mode X locks gap before rec insert intention
?
Session3:
被選為犧牲者,回滾掉。
?
很容易重現,當session 1回滾時,session2和session3提示死鎖發生。
?
這里的關鍵是當ROLLBACK時,實際上是在做一次delete操作,backtrace如下:
trx_general_rollback_for_mysql->….->row_undo->row_undo_ins->row_undo_ins_remove_clust_rec->btr_cur_optimistic_delete->lock_update_delete->lock_rec_inherit_to_gap
?
我們來跟蹤一下創建鎖的軌跡
s1的事務0x7fdfd80265b8
s2的事務0x7fdfe0007c68
s3的事務0x7fdff00213f8
?
s1 , type_mode=1059 ? ? //s2為s1轉換隱式鎖為顯式鎖,
s2, ?type_mode=1282 ? ?//檢查重復鍵,需要加共享鎖,被s1 block住,等待S鎖
s3, ?type_mode=1282 ? ?// 被s1 block住,等待S鎖
?
s1, type_mode=547 ? ? ? //s1回滾,刪除記錄,lock_update_delete鎖繼承,
s2, type_mode=546 ? ? ? ?//創建s鎖 ?LOCK_GAP | LOCK_REC | LOCK_S
s3, type_mode=546 ? ? ? ?//創建s鎖 ? LOCK_GAP | LOCK_REC | LOCK_S
s2, type_mode=2819 ? // LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION
s3, type_mode=2819 ? // ?LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION
?
看看show engine innodb status打印的死鎖信息:
insert into t1 values (6,12)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 137 page no 3 n bits 72 index `PRIMARY` of table `test`.`t1` trx id FE3BFA70 lock_mode X locks gap before rec insert intention waiting
*** (2) TRANSACTION:
TRANSACTION FE3BFA6F, ACTIVE 143 sec inserting, thread declared inside InnoDB 1
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1248, 2 row lock(s)
MySQL thread id 791, OS thread handle 0x7fe2d4ea1700, query id 2613 localhost root update
insert into t1 values (6,12)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 137 page no 3 n bits 72 index `PRIMARY` of table `test`.`t1` trx id FE3BFA6F lock mode S locks gap before rec
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 137 page no 3 n bits 72 index `PRIMARY` of table `test`.`t1` trx id FE3BFA6F lock_mode X locks gap before rec insert intention waiting
*** WE ROLL BACK TRANSACTION (2)
?
從上面的分析,我們可以很容易理解死鎖為何發生。s1插入記錄,s2插入同一條記錄,主鍵沖突,s2將s1的隱式鎖轉為顯式鎖,同時s2向隊列中加入一個s鎖請求;
s3同樣也加入一個s鎖請求;
當s1回滾后,s2和s3獲得s鎖,但隨后s2和s3又先后請求插入意向鎖,因此鎖隊列為:
s2(S GAP)<—s3(S GAP)<—s2(插入意向鎖)<–s3(插入意向鎖) ? s3,s2,s3形成死鎖。
B.DELETE
Innodb的delete操作實際上只是做標記刪除,而不是真正的刪除記錄;真正的刪除是由Purge線程來完成的。
DELETE操作的記錄加鎖,是在查找記錄時完成的。這一點,我們在上一節已經提到了。
上面我們有提到,對插入一條記錄做回滾時,實際上是通過undo來做delete操作。這時候有一個lock_update_insert操作,我們來看看這個函數干了什么:
1.首先獲取將被移除的記錄HEAP NO和下一條記錄的HEAP NO
? ? ? ? heap_no = rec_get_heap_no_new(rec);
? ? ? ? next_heap_no = rec_get_heap_no_new(page
? ? ? ? ? ? ? ? ? ? ? ? ? ?+ rec_get_next_offs(rec,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?TRUE));
2.然后獲取kernel mutex鎖,執行:
將被刪除記錄上的GAP鎖轉移到下一條記錄上:
lock_rec_inherit_to_gap(block, block, next_heap_no, heap_no);
遍歷heao_no上的鎖對象,滿足如下條件時為下一個記錄上的事務創建新的鎖對象:
? ? ? ? if (!lock_rec_get_insert_intention(lock)
? ? ? ? ? ? && !((srv_locks_unsafe_for_binlog
? ? ? ? ? ? ? || lock->trx->isolation_level
? ? ? ? ? ? ? <= TRX_ISO_READ_COMMITTED)
? ? ? ? ? ? ?&& lock_get_mode(lock) == LOCK_X)) {
?
? ? ? ? ? ? lock_rec_add_to_queue(LOCK_REC | LOCK_GAP
? ? ? ? ? ? ? ? ? ? ? ? ? | lock_get_mode(lock),
? ? ? ? ? ? ? ? ? ? ? ? ? heir_block, heir_heap_no,
? ? ? ? ? ? ? ? ? ? ? ? ? lock->index, lock->trx);
? ? ? ? }
?
?
條件1:鎖對象不是插入意向鎖(INSERT INTENTION LOCK)
條件2:srv_locks_unsafe_for_binlog被設置為FALSE且隔離級別大于READ COMMITTED, 或者鎖類型為LOCK_S ? ??
?
和lock_update_insert類似,這里也會創建新的GAP鎖對象
?
當完成鎖表更新操作后,重置鎖bit并釋放等待的事務lock_rec_reset_and_release_wait(block, heap_no):
>>正在等待當前記錄鎖的(lock_get_wait(lock)),取消等待(lock_rec_cancel(lock))
>>已經獲得當前記錄鎖的,重置對應bit位(lock_rec_reset_nth_bit(lock, heap_no);)
?
lock_update_delete主要在INSERT回滾及Purge線程中被調用到。
?
在查找數據時,DELETE會給記錄加鎖,在進行標記刪除時,也會調用到鎖檢查函數:
聚集索引:
row_upd->row_upd_clust_step->row_upd_del_mark_clust_rec->btr_cur_del_mark_set_clust_rec->lock_clust_rec_modify_check_and_lock
這個backtrace,會從lock_clust_rec_modify_check_and_lock直接返回DB_SUCCESS,因為函數btr_cur_del_mark_set_clust_rec的參數flags總是
值為BTR_NO_LOCKING_FLAG
用戶線程不做調用,但在btr_cur_upd_lock_and_undo則會繼續走lock_clust_rec_modify_check_and_lock的流程。
?
二級索引:
row_upd->row_upd_sec_step->row_upd_sec_index_entry->btr_cur_del_mark_set_sec_rec->lock_sec_rec_modify_check_and_lock
用戶線程里lock_sec_rec_modify_check_and_lock的flags參數為0,而在row_undo_mod_del_unmark_sec_and_undo_update、row_undo_mod_del_mark_or_remove_sec_low函數里則設置為BTR_NO_LOCKING_FLAG,表示不做檢查。
?
lock_sec_rec_modify_check_and_lock用于檢查是否有其他事務阻止當前修改一條二級索引記錄(delete mark or delete unmark),
?
如果開始修改二級索引,則表示我們已經成功修改了聚集索引,因此不應該有其他事務在該記錄上的隱式鎖,也不應該有其他活躍事務修改了二級索引記錄。該函數會調用:
? ? err = lock_rec_lock(TRUE, LOCK_X | LOCK_REC_NOT_GAP,
? ? ? ? ? ? ? ? block, heap_no, index, the);
第一個函數為TRUE,則當無需等待時,不會創建新的鎖對象。
?
如果err返回值為DB_SUCCESS或者DB_SUCCESS_LOCKED_REC,就更新當前二級索引Page上的最大事務ID。
如果當前存在和LOCK_X|LOCK_REC_NOT_GAP相沖突的鎖對象,則可能需要等待。
?
回到在之前博文提到的死鎖,信息如下:
*** (1) TRANSACTION:
TRANSACTION 1E7D49CDD, ACTIVE 69 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1248, 4 row lock(s), undo log entries 1
MySQL thread id 1385867, OS thread handle 0x7fcebd956700, query id 837909262 10.246.145.78 im updating
delete??? from??????? msg ? ?WHERE???? target_id = ‘Y25oaHVwYW7mmZbmmZblpKnkvb8=’????? and???????? gmt_modified <= ’2012-12-14 15:07:14′
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS?space id 203 page no 475912 n bits 88?index `PRIMARY` of table `im`.`msg` trx id 1E7D49CDD lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 1E7CE0399, ACTIVE 1222 sec fetching rows, thread declared inside InnoDB 272
mysql tables in use 1, locked 1
1346429 lock struct(s), heap size 119896504, 11973543 row lock(s), undo log entries 1
MySQL thread id 1090268, OS thread handle 0x7fcebf48c700, query id 837483530 10.246.145.78 im updating
delete??? from??????? msg ? ?WHERE???? target_id = ‘Y25oaHVwYW7niLHkuZ3kuYU5OQ==’????? and???????? gmt_modified <= ’2012-12-14 14:13:28′
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 203 page no 475912 n bits 88 index `PRIMARY` of table `im`.`msg` trx id 1E7CE0399 lock_mode X
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 203 page no 1611099?n bits 88?index `PRIMARY` of table `im`.`msg` trx id 1E7CE0399 lock_mode X waiting
?
表結構為:
CREATE?TABLE?`msg`?(
??`id`?bigint(20)?NOT?NULL?AUTO_INCREMENT,
??`target_id`?varchar(100)?COLLATE?utf8_bin?NOT?NULL?,
? ? ? ?……
? ? ? ?……
??`flag`?tinyint(4)?NOT?NULL ,
??`gmt_create`?datetime?NOT?NULL,
??`gmt_modified`?datetime?NOT?NULL,
? `datablob`?blob,
??`nickname`?varchar(64)?COLLATE?utf8_bin?DEFAULT?NULL ,
??`source`?tinyint(4)?DEFAULT?NULL ,
??PRIMARY?KEY?(`id`),
??KEY?`idx_o_tid`?(`target_id`,`gmt_modified`,`source`,`flag`)
)?ENGINE=InnoDB?
首先我們從死鎖信息里來看,發生死鎖的是兩個delete語句,
delete??? from??????? offmsg_0007??? WHERE???? target_id = ‘Y25oaHVwYW7mmZbmmZblpKnkvb8=’????? and???????? gmt_modified <= ’2012-12-14 15:07:14′
delete??? from??????? offmsg_0007??? WHERE???? target_id = ‘Y25oaHVwYW7niLHkuZ3kuYU5OQ==’????? and???????? gmt_modified <= ’2012-12-14 14:13:28′
?
?
我們再看看這個表上的索引,一個主鍵索引(target_id),一個二級索引(`target_id`,`gmt_modified`,`source`,`flag`)
根據前綴索引的原則,理論上我們應該可以通過二級索引來查找數據,從上一節的分析,我們知道,如果根據二級索引查找數據:
>>二級索引上加X 鎖,記錄及GAP
>>聚集索引上加記錄X鎖
?
我們再看死鎖信息:
第一條SQL等待聚集索引Page 475912上的lock_mode X locks rec but not gap, 這說明該鎖請求等待是走二級索引的
第二條SQL持有聚集索引Page 475912上的lock_mode X鎖,等待聚集索引Page 1611099上的 lock_mode X
因此我們大致可以認為第二條SQL總是在請求聚集索引上的LOCK_ORDINARY類型的鎖,簡單的gdb我們可以知道走聚集索引做范圍刪除,鎖模式值為3,也就是LOCK_X
?
因此,可以推測delete操作走錯了索引,導致出現資源的互相占用。從而死鎖;至于為什么走錯索引,這就是優化器的問題了,暫不明;
?
C.釋放鎖
在事務提交或回滾時,會釋放記錄鎖,調用函數為lock_release_off_kernel
函數的邏輯很簡單,遍歷trx->trx_locks。
對于記錄鎖,調用lock_rec_dequeue_from_page(lock)
–>從lock_sys中刪除
–>檢查lock所在page上的等待的鎖對象是否能被grant(lock_grant),如果可以,則喚醒等待的事務。
對于表鎖,調用lock_table_dequeue(lock)
轉載自:?[MySQL學習] Innodb鎖系統(4) Insert/Delete 鎖處理及死鎖示例分析
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Innodb锁系统 Insert/Delete 锁处理及死锁示例分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IT架构的本质:工作12年,我的五点感悟
- 下一篇: mysql insert锁机制