Innodb锁系统 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:
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)
转载自: [MySQL学习] Innodb锁系统(4) Insert/Delete 锁处理及死锁示例分析
Innodb锁系统 Insert/Delete 锁处理及死锁示例分析相关推荐
- MySQL锁系统总结
1 innoDB锁简介 innoDb支持多种粒度的锁,按照粒度来分,可分为表锁(LOCK_TABLE)和行锁(LOCK_REC) 一般的锁系统都会有共享锁和排他锁的分类,共享锁也叫读锁,排他锁也叫写锁 ...
- mysql 事务 innodb 锁表_MySQL性能优化之Innodb事务系统,值得收藏
概述 今天主要分享下Innodb事务系统的一些优化相关,以下基于mysql 5.7. Innodb中的事务.视图.多版本 1.事务 在Innodb中,每次开启一个事务时,都会为该session分配一个 ...
- mysql 开发进阶篇系列 7 锁问题(innodb锁争用情况及锁模式)
1 .获取innodb行锁争用情况 1.1 通过检查innodb_row_lock状态变量来分析系统上的行锁的争夺情况 SHOW STATUS LIKE 'innodb_row_lock%' 通过in ...
- MySQL怎么运行的系列(十)Innodb中的锁:记录锁、临键锁、间隙锁、意向锁
本系列文章目录 展开/收起 MySQL怎么运行的系列(一)mysql体系结构和存储引擎 MySQL怎么运行的系列(二)Innodb缓冲池 buffer pool 和 改良版LRU算法 Mysql怎么运 ...
- InnoDB锁-共享锁、排他锁与意向锁
InnoDB锁-共享锁.排他锁与意向锁 1.锁的分类 锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制.之前MyISAM锁章节已经讲过锁分类,而InnoDB锁按照粒度分为锁定整 ...
- mysql insert 乐观锁_【mysql】关于乐观锁
一.乐观锁介绍 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检,乐观锁适用于 ...
- MySQL数据库读现象 数据库锁机制 Innodb存储引擎行级锁
数据库读现象 数据库管理软件的"读现象"指的是当多个事务并发执行时,在读取数据方面可能碰到的问题,包括有脏读.不可重复读和幻读. 创建数据表 # 创建数据表 create tabl ...
- MySQL数据库锁构建_MySQL数据库InnoDB存储引擎中的锁机制
00 – 基本概念 当并发事务同时访问一个资源的时候,有可能导致数据不一致.因此需要一种致机制来将访问顺序化. 锁就是其中的一种机制.我们用商场的试衣间来做一个比喻.试衣间供许多消费者使用.因此可能有 ...
- mysql innodb 排他锁_MySQL 针对 InnoDB 引擎锁的种类:行锁(共享锁和排他锁)和表锁(意向共享锁和意向排他锁)...
InnoDB 锁快速到底 行锁:共享锁(S).排他锁(X) 表锁:意向共享锁(IS).意向排他锁(IX) 下面主要针对 MySQL 中行级锁中的共享锁(S)与排他锁(X)进行分析 共享锁又称为读锁,简 ...
最新文章
- uni-app编译配置
- querystring java_java – 自定义枚举的QueryStringBindable
- 安装rocketmq并配置管理界面
- 重物码垛搬运机器人_搬运码垛机器人的特点及应用
- Android 中MVC实例之Activity,Window和View
- Python:使用SWIG编写C语言扩展
- 全国计算机等级考试题库二级C操作题100套(第46套)
- AcWing 126. 最大的和
- 5g理论速度_华为5G随行WiFi Pro 有多强 我们试一下NSA网络下的5G速度
- [转]MegCup2015初赛题
- [论文阅读] Self-supervised Correction Learning for Semi-supervised Biomedical Image Segmentation
- GitHub 标星 1.6w+,前方宝藏项目出没!| 原力计划
- sqlite 按拼音排序
- 地理信息系统GIS在城市生活垃圾管理中的应用时间
- 电子计算机为什么123安不出来,方正软件常见问题及其解决办法-精.doc
- TP-Link 886nV6 刷第三方系统回忆
- java md5 16位解密_Java md5加密解密数据
- python开源报表工具_12个最好的开源报表工具
- PHP-SDK实现微信付款码支付
- 劳务派遣能解决哪些用工难题?企业关心的都在这里!
热门文章
- java和net共同点,Java和.NET中的垃圾回收机制比较
- python统计学书籍推荐_一位90后统计学硕士的深悟:统计其实有门道!AI还能这样学!(精荐40本书+20视频资源...
- html5调盒子边框大小,CSS3 - 盒子大小(CSS3 - Box Sizing)
- switch语句可以被代替吗_大空间建筑内的消防水炮可以代替喷淋装置吗
- java怎么读取文件夹下的_java怎么读取读取文件夹下的所有文件夹和文件?
- mysql数据被截断_有关Mysql数据截断问题的处理方法
- linux如何判断网线插入_斜口钳和网线钳制作网线!
- IDEA安装class文件分析工具(binEd 和 JClassLib)
- C#三层架构之第三次课 业务逻辑层
- 重磅:JDK11正式发布!史上最全所有特性完整解读!