天天看點

[MySQL學習] Innodb鎖系統(4) 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:

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)