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:
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:
insert into t1 values (6,12); //阻塞住,和session2 同樣等待s鎖,lock mode s locks rec but not gap waiting
rollback;
執行插入成功
這時候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
*** (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上的鎖對象,滿足如下條件時為下一個記錄上的事務建立新的鎖對象:
&& !((srv_locks_unsafe_for_binlog
|| lock->trx->isolation_level
<= trx_iso_read_committed)
&& lock_get_mode(lock) == lock_x)) {
heir_block, heir_heap_no,
條件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
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′
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
transaction 1e7ce0399, active 1222 sec fetching rows, thread declared inside innodb 272
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′
record locks space id 203 page no 475912 n bits 88 index `primary` of table `im`.`msg` trx id 1e7ce0399 lock_mode x
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)