insert into 加鎖機制

關於MySQL innodb Insert into 加鎖的機制的文章網上很少,個人對於insert 的加鎖機制比較感興趣,所以通過此wiki對研究的過程做個總結,如有不對的地方,歡迎指正。

我先把官方文檔對於insert 加鎖的描述貼出來

INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.Prior to inserting the row, a type of gap lock called an insertion intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock. 

大體的意思是:insert會對插入成功的行加上排它鎖,這個排它鎖是個記錄鎖,而非next-key鎖(當然更不是gap鎖了),不會阻止其他併發的事務往這條記錄之前插入記錄。在插入之前,會先在插入記錄所在的間隙加上一個插入意向gap鎖(簡稱I鎖吧),併發的事務可以對同一個gap加I鎖。如果insert 的事務出現了duplicate-key error ,事務會對duplicate index record加共享鎖。這個共享鎖在併發的情況下是會產生死鎖的,比如有兩個併發的insert都對要對同一條記錄加共享鎖,而此時這條記錄又被其他事務加上了排它鎖,排它鎖的事務提交或者回滾後,兩個併發的insert操作是會發生死鎖的。

 


關於插入意向鎖:

從mysql的insert 加鎖的源碼可以看出,insert 插入的時候是用的是LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION (這就是插入意向鎖)去檢查插入的gap,這個鎖模式是與LOCK_S | LOCK_GAP,LOCK_X | LOCK_GAP鎖模式衝突的,但對於相同的gap,兩個鎖模式爲LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,是兼容的。

  1. /*********************************************************************//** 
  2. Checks if locks of other transactions prevent an immediate insert of 
  3. a record. If they do, first tests if the query thread should anyway 
  4. be suspended for some reason; if not, then puts the transaction and 
  5. the query thread to the lock wait state and inserts a waiting request 
  6. for a gap x-lock to the lock queue. 
  7. @return DB_SUCCESS, DB_LOCK_WAIT, DB_DEADLOCK, or DB_QUE_THR_SUSPENDED */  
  8. UNIV_INTERN  
  9. ulint  
  10. lock_rec_insert_check_and_lock(  
  11. /*===========================*/  
  12.     ulint       flags,  /*!< in: if BTR_NO_LOCKING_FLAG bit is 
  13.                 set, does nothing */  
  14.     const rec_t*    rec,    /*!< in: record after which to insert */  
  15.     buf_block_t*    block,  /*!< in/out: buffer block of rec */  
  16.     dict_index_t*   index,  /*!< in: index */  
  17.     que_thr_t*  thr,    /*!< in: query thread */  
  18.     mtr_t*      mtr,    /*!< in/out: mini-transaction */  
  19.     ibool*      inherit)/*!< out: set to TRUE if the new 
  20.                 inserted record maybe should inherit 
  21.                 LOCK_GAP type locks from the successor 
  22.                 record */  
  23. {  
  24.     const rec_t*    next_rec;  
  25.     trx_t*      trx;  
  26.     lock_t*     lock;  
  27.     ulint       err;  
  28.     ulint       next_rec_heap_no;  
  29.     ut_ad(block->frame == page_align(rec));  
  30.     if (flags & BTR_NO_LOCKING_FLAG) {  
  31.         return(DB_SUCCESS);  
  32.     }  
  33.     trx = thr_get_trx(thr);  
  34.     next_rec = page_rec_get_next_const(rec);  
  35.     next_rec_heap_no = page_rec_get_heap_no(next_rec);  
  36.     lock_mutex_enter_kernel();  
  37.     /* When inserting a record into an index, the table must be at 
  38.     least IX-locked or we must be building an index, in which case 
  39.     the table must be at least S-locked. */  
  40.     ut_ad(lock_table_has(trx, index->table, LOCK_IX)  
  41.           || (*index->name == TEMP_INDEX_PREFIX  
  42.           && lock_table_has(trx, index->table, LOCK_S)));  
  43.     lock = lock_rec_get_first(block, next_rec_heap_no);  
  44.     if (UNIV_LIKELY(lock == NULL)) {  
  45.         /* We optimize CPU time usage in the simplest case */  
  46.         lock_mutex_exit_kernel();  
  47.         if (!dict_index_is_clust(index)) {  
  48.             /* Update the page max trx id field */  
  49.             page_update_max_trx_id(block,  
  50.                            buf_block_get_page_zip(block),  
  51.                            trx->id, mtr);  
  52.         }  
  53.         *inherit = FALSE;  
  54.         return(DB_SUCCESS);  
  55.     }  
  56.     *inherit = TRUE;  
  57.     /* If another transaction has an explicit lock request which locks 
  58.     the gap, waiting or granted, on the successor, the insert has to wait. 
  59.     An exception is the case where the lock by the another transaction 
  60.     is a gap type lock which it placed to wait for its turn to insert. We 
  61.     do not consider that kind of a lock conflicting with our insert. This 
  62.     eliminates an unnecessary deadlock which resulted when 2 transactions 
  63.     had to wait for their insert. Both had waiting gap type lock requests 
  64.     on the successor, which produced an unnecessary deadlock. */  
  65.     if (lock_rec_other_has_conflicting(  
  66.             LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,  
  67.             block, next_rec_heap_no, trx)) {  
  68.         /* Note that we may get DB_SUCCESS also here! */  
  69.         err = lock_rec_enqueue_waiting(LOCK_X | LOCK_GAP  
  70.                            | LOCK_INSERT_INTENTION,  
  71.                            block, next_rec_heap_no,  
  72.                            NULL, index, thr);  
  73.     } else {  
  74.         err = DB_SUCCESS;  
  75.     }  
  76.     lock_mutex_exit_kernel();  
  77.     switch (err) {  
  78.     case DB_SUCCESS_LOCKED_REC:  
  79.         err = DB_SUCCESS;  
  80.         /* fall through */  
  81.     case DB_SUCCESS:  
  82.         if (dict_index_is_clust(index)) {  
  83.             break;  
  84.         }  
  85.         /* Update the page max trx id field */  
  86.         page_update_max_trx_id(block,  
  87.                        buf_block_get_page_zip(block),  
  88.                        trx->id, mtr);  
  89.     }  
  90. #ifdef UNIV_DEBUG  
  91.     {  
  92.         mem_heap_t* heap        = NULL;  
  93.         ulint       offsets_[REC_OFFS_NORMAL_SIZE];  
  94.         const ulint*    offsets;  
  95.         rec_offs_init(offsets_);  
  96.         offsets = rec_get_offsets(next_rec, index, offsets_,  
  97.                       ULINT_UNDEFINED, &heap);  
  98.         ut_ad(lock_rec_queue_validate(block,  
  99.                           next_rec, index, offsets));  
  100.         if (UNIV_LIKELY_NULL(heap)) {  
  101.             mem_heap_free(heap);  
  102.         }  
  103.     }  
  104. #endif /* UNIV_DEBUG */  
  105.     return(err);  
  106. }  


下面通過幾個場景我們看看insert的具體加鎖的機制。

演示表如下:

  1. CREATE TABLE `tt` (  
  2.   `a` int(11) NOT NULL AUTO_INCREMENT,  
  3.   `b` int(11) DEFAULT NULL,  
  4.   PRIMARY KEY (`a`),  
  5.   KEY `idx_b` (`b`)  
  6. ) ENGINE=InnoDB  

tt表中插入一些數據insert into tt values(1,8),(2,3),(3,4),(4,1),(5,12);


場景一:

  事務1 事務2
1

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

2 mysql> select * from tt where b = 5 for update;
Empty set (0.00 sec)
 
3  

 mysql> insert into tt(b) values(6);

鎖等待。。

4  commit;  
   

Query OK, 1 row affected (14.92 sec)


我們看看第3步innodb鎖狀態,9D53488C是事務1,9D5348C0是事務2,事務1的鎖很容易理解,select for update,數據表中沒有b=5的記錄,所以加的是X gap鎖,鎖住的間隙是(4,8),事務2發生了鎖等待。事務2鎖等待是因爲事務1對(4,8)加上了排它鎖(鎖模式爲X,GAP),是會阻塞事務2的I鎖,事務2發生等待,因此INNODB_LOCKS表中顯示的lock_mode是X和GAP,lock_type是行級鎖。

(關於INNODB_LOCKS和INNODB_LOCK_WAITS字段意義可以參考information_schema中Innodb相關表用於分析sql查詢鎖的使用情況介紹),



場景二:兩個併發插入到相同gap不同的記錄

  事務1 事務2
1

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

2

mysql> insert into tt(b) values(5);
Query OK, 1 row affected (0.04 sec)

 
3  

mysql> insert into tt(b) values(6);
Query OK, 1 row affected (0.04 sec)

4  commit; commit; 

這個場景證明,對於同一個gap,I鎖是不衝突的,事務1和事務2沒有鎖等待,都插入成功。

場景三:演示對插入的記錄加的排它鎖

  事務1 事務2
1

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

2

mysql> insert into tt(b) values(5);
Query OK, 1 row affected (0.04 sec)

 
3  

mysql> select * from tt where b >4 and b <8 lock in share mode;

鎖等待

4  commit;  
   

+----+------+
| a | b |
+----+------+
| 12 | 5 |
+----+------+
1 row in set (6.90 sec)

commit;



事務1對應的是9D5CD9A9 事務2對應的是9D5CD9F8,事務2發生了鎖等待,通過innodb_locks,可以看出事務2要等待的鎖的類型是S gap鎖(沒弄明白這裏爲什麼不是顯示的gap鎖),加鎖的間隙是(4,8),這個鎖被事務1的X鎖組塞,所以可以確認insert插入後是會加排它鎖,這裏可以通過修改事務2的語句,確定出insert 插入後加的是記錄鎖(這裏就不列出具體的演示場景了)。

場景四:演示下insert 的事務出現了duplicate-key error的情況

演示前先tt表的b字段改成unique key。

  事務1 事務2 事務3
1

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
2

mysql> insert into tt(b) values(5);
Query OK, 1 row affected (0.04 sec)

   
3  

mysql>  insert into tt(b) values(5);

鎖等待

mysql>  insert into tt(b) values(5);

鎖等待

4

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

   
   

mysql> insert into tt(b) values(5);
Query OK, 1 row affected (37.17 sec)

mysql> insert into tt(b) values(5);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

死鎖發生了

先看看3的時候,鎖的狀態:

9D961CAD是事務1 ,9D960FC9和9D960FD9分別是事務2和3,從innodb_locks表中可以看出事務1是X記錄鎖,事務2和3是S記錄鎖,且這三個鎖對應的是同樣的記錄,從innodb_lock_waits表可以看出事務2和事務3 的S鎖被事務1 的X鎖阻塞了。



當事務1 rollback後,事務2和事務3發生死鎖。通過show engine innodb status查看死鎖日誌如下:

------------------------
LATEST DETECTED DEADLOCK
------------------------
150109 9:59:59
*** (1) TRANSACTION:
TRANSACTION 9D96295F, ACTIVE 19 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 1675150, OS thread handle 0x7f5181977700, query id 1001786133 192.168.148.68 q3boy update
insert into tt(b) values(5)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 48562 page no 4 n bits 80 index `ux_b` of table `testdg`.`tt` trx id 9D960FD9 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000008; asc ;;
1: len 4; hex 80000001; asc ;;

*** (2) TRANSACTION:
TRANSACTION 9D962A68, ACTIVE 9 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 1675251, OS thread handle 0x7f518055e700, query id 1001790623 192.168.148.68 q3boy update
insert into tt(b) values(5)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 48562 page no 4 n bits 80 index `ux_b` of table `testdg`.`tt` trx id 9D960FC9 lock mode S locks gap before rec
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000008; asc ;;
1: len 4; hex 80000001; asc ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 48562 page no 4 n bits 80 index `ux_b` of table `testdg`.`tt` trx id 9D962A68 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000008; asc ;;
1: len 4; hex 80000001; asc ;;


從上面死鎖日誌,我們可以很容易理解死鎖爲何發生。事務1插入記錄,事務2插入同一條記錄,主鍵衝突,事務2將事務1的隱式鎖轉爲顯式鎖,同時事務2向隊列中加入一個s鎖請求;

事務3同樣也加入一個s鎖請求;

當事務1回滾後,事務2和事務3獲得s鎖,但隨後事務2和事務3又先後請求插入意向鎖,因此鎖隊列爲:

事務2(S GAP)<—事務3(S GAP)<—事務2(插入意向鎖)<–事務3(插入意向鎖)   事務3,事務2,事務3形成死鎖。

發佈了193 篇原創文章 · 獲贊 30 · 訪問量 49萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章