一、场景还原

当时同事A在线上代码中使用了Mybatis-plus的如下方法

com.baomidou.mybatisplus.extension.service.IServicesaveOrUpdate(T, com.baomidou.mybatisplus.core.conditions.Wrapper<T>)

该方法先执行了update操作,如果更新到就不再执行后续操作,如果没有更新到,才进行主键查询,查询到了就修改,未查询到就新增。具体方法如下

/*** <p>* 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法* 此次修改主要是减少了此项业务代码的代码量(存在性验证之后的saveOrUpdate操作)* </p>** @param entity 实体对象*/default boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper) {return update(entity, updateWrapper) || saveOrUpdate(entity);}

那么这个方法的做法,为什么会导致间隙锁死锁呢?咱们一起来分析并还原间隙锁死锁的场景。

二、什么是间隙锁

间隙锁是MySQL行锁的一种,与行锁不同的是间隙锁可能锁定的是一行数据,也可能锁住一个间隙。锁定规则如下:

  • 当修改的数据存在时,间隙锁只会锁定当前行。

  • 当修改的数据不存在时,间隙锁会向左找第一个比当前索引值小的值,向右找第一个比当前索引值大 的值(没有则为正无穷),将此区间锁住,从而阻止其他事务在此区间插入数据。

三、间隙锁的作用

与行锁(例如乐观锁高级实现,MVCC)组合成Next-key lock,在可重复读这种隔离级别下一起工作避免幻读。

四、如何关闭间隙锁(强烈不建议关闭)

1、降低隔离级别,例如降为提交读。

2、直接修改my.cnf,将开关,innodb_locks_unsafe_for_binlog改为1,默认为0即开启

五、还原线上间隙锁死锁的场景

5.1 复现间隙锁死锁

5.1.1 我们先准备一个表

mysql> select * from t_gap_lock;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   21 |
|  5 | 李五   |   25 |
|  6 | 赵六   |   26 |
|  9 | 王九   |   29 |
| 12 | 十二   |   12 |
+----+--------+------+

表中的id数据咱们准备了三个间隙:

  • 间隙一:1-5

  • 间隙二:6-9

  • 间隙三:12-正无穷

5.1.2 操作

1、此时我们开启事务一,然后执行更新id=3的数据,按照咱们的理论,id=3这个数据不存在,说明它会在1-5之间加间隙锁。

#开启事务一
begin;#事务一在1-5之间加间隙锁
update t_gap_lock t set t.age = 23 where t.id = 3;

2、然后我们开启事务二,然后执行更新id=7的数据,按照咱们的理论,id=7这个数据不存在,说明它会在6-9之间加间隙锁

#开启事务二
begin;#事务二在6-9之间加间隙锁
update t_gap_lock t set t.age = 27 where t.id = 7;

3、那么重点来了,此时我们需要做的操作就是让事务一在6-9之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。

#事务一在6-9之间插入数据
insert into t_gap_lock(id, name, age) values(8,'李八',28);

4、在事务一等待锁的同时,咱们让事务二同时在1-5之间插入数据,这个时候会发现,只要事务二一执行插入。MySQL立即报了死锁,我们就会见到如下提示: [40001][1213] Deadlock found when trying to get lock; try restarting transaction 。

# 同时事务二在1-5之间插入数据
insert into t_gap_lock(id, name, age) values(3,'李三',23);

5.1.3 整个死锁过程进行原理分析

1、首先事务一开启事务后,更新id=3的数据,此数据不存在,所以事务一会锁住1-5这个间隙,即为1-5这个间隙添加间隙锁,同理,事务二会为6-9这个间隙添加间隙锁;

2、然后我们让事务一在6-9这个间隙插入数据,因为事务二已经加了间隙锁,所以事务一需要等待事务二释放间 隙锁才能进行插入操作,此时事务一等待事务二释放间隙锁;

3、同理,事务二在1-5间隙插入时需要等待事务一释放间隙锁,两个事务相互等待,死锁产生。

那么咱们此时就能大概明白最初那个Mybatis-plus的saveOrUpdate方法为什么会造成间隙锁死锁的问题,也就是线上存在两个并发事务,然后更新的时候都没有更新到,此时都在自己的间隙加了间隙锁,然后再到彼此的区间进行数据插入,此时就会造成两个事务互相等待对方的释放间隙锁,从而导致死锁。也许有同学会想,线上的数据几乎不可能刚好会存在1-5,6-9这种间隙,来给并发事务各自加锁,又刚好到彼此区间插入数据的场景,所以我们就会有接下来验证间隙锁加锁是非互斥的,再一次深度还原间隙锁死锁的场景。

5.2 验证间隙锁加锁非互斥

5.2.1 依然以t_gap_lock为例

mysql> select * from t_gap_lock;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   21 |
|  5 | 李五   |   25 |
|  6 | 赵六   |   26 |
|  9 | 王九   |   29 |
| 12 | 十二   |   12 |
+----+--------+------+

5.2.2 操作

1、此时咱们开启事务一,然后执行更新id=13的数据,按照咱们的理论,id=13这个数据不存在,说明它会在13-正无穷(因为当前索引树上没有比13更大的值)之间加间隙锁。

#开启事务一
begin;
#事务一在13-正无穷添加间隙锁
update t_gap_lock t set t.age = 13 where t.id = 13;

2、然后我们开启事务二,然后也执行更新id=13的数据,按照咱们的理论,事务二也会对13-正无穷之间加间隙锁

#开启事务二
begin;
#在13-正无穷添加间隙锁
update t_gap_lock t set t.age = 13 where t.id = 13;

3、那么重点来了,此时我们需要做的操作就是让事务一在13-正无穷之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。

#事务一在13-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (13,'十六',16);

4、在事务一等待锁的同时,咱们让事务二同时在13-正无穷之间插入数据,这个时候会发现,只要事务二一执行插入。MySQL立即报了死锁,我们就会见到如下提示:

[40001][1213] Deadlock found when trying to get lock; try restarting transaction 。

#事务二在13-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (13,'十六',16);

5、因为咱们已经用1-5以及6-9这种明显的间隙还原了间隙锁死锁,所以13-正无穷发生间隙锁死锁的原理与其无异,这里有个非常大的区别就是事务一已经在13-正无穷加了间隙锁,事务二依然可以对此间隙加间隙锁,所以我们用实际证明了间隙锁加锁是非互斥的。此时咱们回忆一下Mybatis-plus的saveOrUpdate方法,发现线上只要出现两个并发事务去修改同一条不存在的数据,就会立马出现间隙锁死锁。

5.3 验证当修改数据存在时,间隙锁只会锁住当前行

还有一个比较重要的点就是,当修改的数据存在时,MySQL只会锁住当前行,咱们一起来分析下整个过程。

5.3.1 依然以t_gap_lock为例

mysql> select * from t_gap_lock;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   21 |
|  5 | 李五   |   25 |
|  6 | 赵六   |   26 |
|  9 | 王九   |   29 |
| 12 | 十二   |   12 |
+----+--------+------+

5.3.2 操作

1、此时我们开启事务一,然后执行更新id=12的数据,按照咱们的理论,id=12这个数据存在,说明MySQL只会锁定id=12这一行数据。

#开启事务一
begin;
#事务一只在12上加间隙锁
update t_gap_lock t set t.age = 12 where t.id = 12;

2、然后我们开启事务二,然后执行更新id=13的数据,按照咱们的理论,id=13这个数据不存在,说明它会在13-正无穷(因为当前索引树上没有比13更大的值)之间加间隙锁

#开启事务二
begin;
#事务二在13-正无穷添加间隙锁
update t_gap_lock t set t.age = 13 where t.id = 13;

3、那么重点来了,此时我们需要做的操作就是让事务一在13-正无穷之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。

#事务一在13-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (15,'十五',15);

4、在事务一等待锁的同时,咱们让事务二在12-正无穷之间插入数据,这个时候会发现,事务二能够正常插入,说明事务二没有被间隙锁阻塞,待事务二提交或回滚后,事务一也正常提交。

#事务二在13-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (13,'十六',16);

5、通过以上验证,MySQL在更新id=12,即数据存在时,并没有对12-正无穷添加间隙锁,而是只锁定了id=12这一行数据,从而降低锁的颗粒度以提高性能。

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

END

MySQL间隙锁死锁问题相关推荐

  1. MySQL间隙锁详细分析

    什么是间隙锁 间隙锁(Gap Lock):间隙锁是(RR级别下)一个在索引记录之间的间隙上的锁,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间 首先要明确,间隙锁是为了防止 ...

  2. 八、Mysql 间隙锁(gap 锁)与慢查询

    Mysql 间隙锁(gap 锁)与慢查询 gap锁与慢查询 gap锁 事务语法 开启事务 事务回滚 事务提交 还原点(演示) 业务设计 逻辑设计 范式设计 查询测试 反范式设计 总结 范式化设计优缺点 ...

  3. mysql间隙锁 打开_MySQL间隙锁问题

    间隙锁(Gap Lock):锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间. 最近用户反馈说系统老是出现insert时,等待超时了,最后发现是ins ...

  4. mysql间隙锁可重入_关于mysql 间隙锁

    前段时间系统老是出现update死锁,很是纠结.经过排查发现是间隙锁!间隙锁是innodb中行锁的一种, 但是这种锁锁住的却不止一行数据,他锁住的是多行,是一个数据范围.间隙锁的主要作用是为了防止出现 ...

  5. MySQL间隙锁(幻读解决原理)

    文章目录 一.间隙锁概念 二.测试间隙锁范围加锁 场景1:用不可重复的主键id测试间隙锁 场景2:用可重复的age(有索引)测试间隙锁 场景3:实际情况需要具体分析用的到底是行锁还是表锁 三.测试等值 ...

  6. Mysql间隙锁实战

    前言 本次实战是基于数据库mysql5.7.14 什么是间隙锁? 间隙锁是对索引记录之间间隙的锁,或者对第一个索引记录之前或最后一个索引记录之后的间隙的锁.例如:SELECT c1 FROM t WH ...

  7. mysql间隙锁触发条件,详解系列文章

    java基础 1.1java的8种基本数据类型装箱拆箱 1.2重写重载封装继承多态 1.3 Stack Queue 1.7 Concurrent包 1.8面向对象 1.9 String StringB ...

  8. 2021字节跳动春招技术面试题:mysql间隙锁触发条件

    java基础 1.1java的8种基本数据类型装箱拆箱 1.2重写重载封装继承多态 1.3 Stack Queue 1.7 Concurrent包 1.8面向对象 1.9 String StringB ...

  9. MYSQL(04)-间隙锁详解

    间隙锁(Gap Lock)是Innodb在提交下为了解决幻读问题时引入的锁机制,(下面的所有案例没有特意强调都使用可重复读隔离级别)幻读的问题存在是因为新增或者更新操作,这时如果进行范围查询的时候(加 ...

  10. mysql可重复读和间隙锁_解决MySQL可重复读——详解间隙锁

    间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制,(下面的所有案例没有特意强调都使用可重复读隔离级别)幻读的问题存在是因为新增或者更新操作,这时如果进行范围查询的 ...

最新文章

  1. python多线程并发_Python进阶记录之基础篇(二十四)
  2. 基于Vue的小日历(支持按周切换)
  3. java使用uploadify上传文件
  4. 今天才知道什么是柏拉图式的爱情
  5. boost::hana::members用法的测试程序
  6. CSS基础(part19)--CSS3属性选择器
  7. alook浏览器js扩展网站_备用浏览器Alook所能实现的功能果真强大(文末有alook兑换码抽奖)...
  8. springbank 开发日志 springbank是如何执行一个handler的requestMapping对应的方法的
  9. [ 原创 ]2017年3月25日
  10. Objective-C语法之集合对象的那些事儿(九)
  11. jquery中has方法
  12. SpringBoot高级篇-属性配置
  13. 软考:McCabe环路复杂度计算方法
  14. python语法错误怎么办_Python中出现语法错误时解决方法
  15. 一张电影票引发的思考,谈谈边际成本和机会成本
  16. matlab 柱状图怎么叠加,如何让柱形图重叠起来
  17. Linux软件的安装
  18. java计算机毕业设计幼儿影视节目智能推荐系统源码+数据库+系统+lw文档+部署
  19. Python函数设计与使用
  20. google输入法PK搜狗输入法

热门文章

  1. 人脸检测FDDB测试ROC曲线生成
  2. maven命令打jar包
  3. 技术中心部门薪酬和考核体系
  4. 医用计算机app,‎App Store 上的“金融-计算器”
  5. c语言编程输出一到十,c语言编程输出1?
  6. httprunner 2.x学习4-测试用例分层
  7. 部署在IIS上的网站程序以管理员权限运行 设置方法
  8. 理解Andriod 硬件加速
  9. 资源监视器中看不到磁盘队列等等问题的解决方案
  10. AXURE母版事件(Raised-events)