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:
set autocommit = 0;
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:
set autocommit = 0;
insert into t1 values (6,12); //阻塞住,和session2 同样等待S锁,lock mode S locks rec but not gap waiting
Session 1:
ROLLBACK;
Session 2:
执行插入成功
这时候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
insert into t1 values (6,12)
*** (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上的锁对象,满足如下条件时为下一个记录上的事务创建新的锁对象:
if (!lock_rec_get_insert_intention(lock)
&& !((srv_locks_unsafe_for_binlog
|| lock->trx->isolation_level
<= TRX_ISO_READ_COMMITTED)
&& lock_get_mode(lock) == LOCK_X)) {
lock_rec_add_to_queue(LOCK_REC | LOCK_GAP
| lock_get_mode(lock),
heir_block, heir_heap_no,
lock->index, lock->trx);
}
条件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
mysql tables in use 1, locked 1
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′
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
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
*** (2) TRANSACTION:
TRANSACTION 1E7CE0399, ACTIVE 1222 sec fetching rows, thread declared inside InnoDB 272
mysql tables in use 1, locked 1
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′
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 203 page no 475912 n bits 88 index `PRIMARY` of table `im`.`msg` trx id 1E7CE0399 lock_mode X
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
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)