關於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,是兼容的。
- /*********************************************************************//**
- Checks if locks of other transactions prevent an immediate insert of
- a record. If they do, first tests if the query thread should anyway
- be suspended for some reason; if not, then puts the transaction and
- the query thread to the lock wait state and inserts a waiting request
- for a gap x-lock to the lock queue.
- @return DB_SUCCESS, DB_LOCK_WAIT, DB_DEADLOCK, or DB_QUE_THR_SUSPENDED */
- UNIV_INTERN
- ulint
- lock_rec_insert_check_and_lock(
- /*===========================*/
- ulint flags, /*!< in: if BTR_NO_LOCKING_FLAG bit is
- set, does nothing */
- const rec_t* rec, /*!< in: record after which to insert */
- buf_block_t* block, /*!< in/out: buffer block of rec */
- dict_index_t* index, /*!< in: index */
- que_thr_t* thr, /*!< in: query thread */
- mtr_t* mtr, /*!< in/out: mini-transaction */
- ibool* inherit)/*!< out: set to TRUE if the new
- inserted record maybe should inherit
- LOCK_GAP type locks from the successor
- record */
- {
- const rec_t* next_rec;
- trx_t* trx;
- lock_t* lock;
- ulint err;
- ulint next_rec_heap_no;
- ut_ad(block->frame == page_align(rec));
- if (flags & BTR_NO_LOCKING_FLAG) {
- return(DB_SUCCESS);
- }
- trx = thr_get_trx(thr);
- next_rec = page_rec_get_next_const(rec);
- next_rec_heap_no = page_rec_get_heap_no(next_rec);
- lock_mutex_enter_kernel();
- /* When inserting a record into an index, the table must be at
- least IX-locked or we must be building an index, in which case
- the table must be at least S-locked. */
- ut_ad(lock_table_has(trx, index->table, LOCK_IX)
- || (*index->name == TEMP_INDEX_PREFIX
- && lock_table_has(trx, index->table, LOCK_S)));
- lock = lock_rec_get_first(block, next_rec_heap_no);
- if (UNIV_LIKELY(lock == NULL)) {
- /* We optimize CPU time usage in the simplest case */
- lock_mutex_exit_kernel();
- if (!dict_index_is_clust(index)) {
- /* Update the page max trx id field */
- page_update_max_trx_id(block,
- buf_block_get_page_zip(block),
- trx->id, mtr);
- }
- *inherit = FALSE;
- return(DB_SUCCESS);
- }
- *inherit = TRUE;
- /* If another transaction has an explicit lock request which locks
- the gap, waiting or granted, on the successor, the insert has to wait.
- An exception is the case where the lock by the another transaction
- is a gap type lock which it placed to wait for its turn to insert. We
- do not consider that kind of a lock conflicting with our insert. This
- eliminates an unnecessary deadlock which resulted when 2 transactions
- had to wait for their insert. Both had waiting gap type lock requests
- on the successor, which produced an unnecessary deadlock. */
- 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,
- NULL, index, thr);
- } else {
- err = DB_SUCCESS;
- }
- lock_mutex_exit_kernel();
- switch (err) {
- case DB_SUCCESS_LOCKED_REC:
- err = DB_SUCCESS;
- /* fall through */
- case DB_SUCCESS:
- if (dict_index_is_clust(index)) {
- break;
- }
- /* Update the page max trx id field */
- page_update_max_trx_id(block,
- buf_block_get_page_zip(block),
- trx->id, mtr);
- }
- #ifdef UNIV_DEBUG
- {
- mem_heap_t* heap = NULL;
- ulint offsets_[REC_OFFS_NORMAL_SIZE];
- const ulint* offsets;
- rec_offs_init(offsets_);
- offsets = rec_get_offsets(next_rec, index, offsets_,
- ULINT_UNDEFINED, &heap);
- ut_ad(lock_rec_queue_validate(block,
- next_rec, index, offsets));
- if (UNIV_LIKELY_NULL(heap)) {
- mem_heap_free(heap);
- }
- }
- #endif /* UNIV_DEBUG */
- return(err);
- }
下面通過幾個場景我們看看insert的具體加鎖的機制。
演示表如下:
- CREATE TABLE `tt` (
- `a` int(11) NOT NULL AUTO_INCREMENT,
- `b` int(11) DEFAULT NULL,
- PRIMARY KEY (`a`),
- KEY `idx_b` (`b`)
- ) ENGINE=InnoDB
tt表中插入一些數據insert into tt values(1,8),(2,3),(3,4),(4,1),(5,12);
場景一:
事務1 | 事務2 | |
---|---|---|
1 |
mysql> begin; |
mysql> begin; |
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; |
mysql> begin; |
2 |
mysql> insert into tt(b) values(5); |
|
3 |
mysql> insert into tt(b) values(6); |
|
4 | commit; | commit; |
這個場景證明,對於同一個gap,I鎖是不衝突的,事務1和事務2沒有鎖等待,都插入成功。
場景三:演示對插入的記錄加的排它鎖
事務1 | 事務2 | |
---|---|---|
1 |
mysql> begin; |
mysql> begin; |
2 |
mysql> insert into tt(b) values(5); |
|
3 |
mysql> select * from tt where b >4 and b <8 lock in share mode; 鎖等待 |
|
4 | commit; | |
+----+------+ 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; |
mysql> begin; |
mysql> begin; Query OK, 0 rows affected (0.00 sec) |
2 |
mysql> insert into tt(b) values(5); |
||
3 |
mysql> insert into tt(b) values(5); 鎖等待 |
mysql> insert into tt(b) values(5); 鎖等待 |
|
4 |
mysql> rollback; |
||
mysql> insert into tt(b) values(5); |
mysql> insert into tt(b) values(5); 死鎖發生了 |
先看看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形成死鎖。