关于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形成死锁。