慎用,Mybatis-Plus可能导致死锁

场景还原

  • 当时同事A在线上代码中使用了Mybatis-Plus的如下方法:
com.baomidou.mybatisplus.extension.service.IService
#saveOrUpdate(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行锁的一种,与行锁不同的是间隙锁可能锁定的是一行数据,也可能锁住一个间隙。
  • 锁定规则如下:
  1. 当修改的数据存在时,间隙锁只会锁定当前行。
  2. 当修改的数据不存在时,间隙锁会向左找第一个比当前索引值小的值,向右找第一个比当前索引值大 的值(没有则为正无穷),将此区间锁住,从而阻止其他事务在此区间插入数据。

间隙锁的作用

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

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

  • 降低隔离级别,例如降为提交读。
Q:那么为什么降低隔离级别可以关闭间隙锁呢?
A:因为间隙锁默认工作在可重复读隔离级别下,需要与MVCC组合才能真正避免幻读,因为MVCC
      避免了修改和删除不可重复读的问题,但是插入MVCC阻止不了,所以MySQL引入了间隙锁来阻
      止插入,从而真正的避免幻读的产生,反之亦然,所以降低隔离级别,间隙锁将失效。
  • 直接修改my.cnf,将开关,innodb_locks_unsafe_for_binlog改为1,默认为0即开启

接下来咱们来还原线上间隙锁死锁的场景

复现间隙锁死锁

  • 我们先准备一个表:
mysql> select * from t_gap_lock;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   21 |
|  5 | 李五   |   25 |
|  6 | 赵六   |   26 |
|  9 | 王九   |   29 |
| 12 | 十二   |   12 |
+----+--------+------+
  • 表中的id数据咱们准备了三个间隙
  1. 间隙一:1-5
  2. 间隙二:6-9
  3. 间隙三:12-正无穷
  • 此时我们开启事务一,然后执行更新id=3的数据,按照咱们的理论,id=3这个数据不存在,说明它会在1-5之间加间隙锁。
#开启事务一
begin;

#事务一在1-5之间加间隙锁
update t_gap_lock t set t.age = 23 where t.id = 3;
  • 然后我们开启事务二,然后执行更新id=7的数据,按照咱们的理论,id=7这个数据不存在,说明它会在6-9之间加间隙锁。
#开启事务二
begin;

#事务二在6-9之间加间隙锁
update t_gap_lock t set t.age = 27 where t.id = 7;
  • 那么重点来了,此时我们需要做的操作就是让事务一在6-9之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。
#事务一在6-9之间插入数据
insert into t_gap_lock(id, name, age) values(8,'李八',28);
  • 在事务一等待锁的同时,咱们让事务二同时在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);

间隙锁死锁效果图

  • 咱们对整个死锁过程进行原理分析:
1、首先事务一开启事务后,更新id=3的数据,此数据不存在,所以事务一会锁住1-5这个间隙,
      即为1-5这个间隙添加间隙锁,同理,事务二会为6-9这个间隙添加间隙锁;
2、然后我们让事务一在6-9这个间隙插入数据,因为事务二已经加了间隙锁,所以事务一
     需要等待事务二释放间 隙锁才能进行插入操作,此时事务一等待事务二释放间隙锁;
3、同理,事务二在1-5间隙插入时需要等待事务一释放间隙锁,两个事务相互等待,死锁产生。
  • 那么咱们此时就能大概明白最初那个Mybatis-plus的saveOrUpdate方法为什么会造成间隙锁死锁的问题,也就是线上存在两个并发事务,然后更新的时候都没有更新到,此时都在自己的间隙加了间隙锁,然后再到彼此的区间进行数据插入,此时就会造成两个事务互相等待对方的释放间隙锁,从而导致死锁。
  • 也许有同学会想,线上的数据几乎不可能刚好会存在1-5,6-9这种间隙,来给并发事务各自加锁,又刚好到彼此区间插入数据的场景,所以我们就会有接下来验证间隙锁加锁是非互斥的,再一次深度还原间隙锁死锁的场景。

验证间隙锁加锁非互斥

  • 首先咱们依然以t_gap_lock为例
mysql> select * from t_gap_lock;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   21 |
|  5 | 李五   |   25 |
|  6 | 赵六   |   26 |
|  9 | 王九   |   29 |
| 12 | 十二   |   12 |
+----+--------+------+
  • 此时咱们开启事务一,然后执行更新id=13的数据,按照咱们的理论,id=13这个数据不存在,说明它会在12-正无穷(因为当前索引树上没有比12更大的值)之间加间隙锁。
#开启事务一
begin;
#事务一在12-正无穷添加间隙锁
update t_gap_lock t set t.age = 13 where t.id = 13;
  • 然后我们开启事务二,然后也执行更新id=13的数据,按照咱们的理论,事务二也会对12-正无穷之间加间隙锁
#开启事务二
begin;
#在12-正无穷添加间隙锁
update t_gap_lock t set t.age = 13 where t.id = 13;
  • 那么重点来了,此时我们需要做的操作就是让事务一在12-正无穷之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。
#事务一在12-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (13,'十六',16);
  • 在事务一等待锁的同时,咱们让事务二同时在12-正无穷之间插入数据,这个时候会发现,只要事务二一执行插入。MySQL立即报了死锁,我们就会见到如下提示:[40001][1213] Deadlock found when trying to get lock; try restarting transaction
#事务二在12-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (13,'十六',16);
  • 因为咱们已经用1-5以及6-9这种明显的间隙还原了间隙锁死锁,所以12-正无穷发生间隙锁死锁的原理与其无异,这里有个非常大的区别就是事务一已经在12-正无穷加了间隙锁,事务二依然可以对此间隙加间隙锁,所以我们用实际证明了间隙锁加锁是非互斥的。
  • 此时咱们回忆一下Mybatis-plus的saveOrUpdate方法,发现线上只要出现两个并发事务去修改同一条不存在的数据,就会立马出现间隙锁死锁。

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

  • 还有一个比较重要的点就是,当修改的数据存在时,MySQL只会锁住当前行,咱们一起来分析下整个过程。
  • 首先咱们依然以t_gap_lock为例:
mysql> select * from t_gap_lock;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   21 |
|  5 | 李五   |   25 |
|  6 | 赵六   |   26 |
|  9 | 王九   |   29 |
| 12 | 十二   |   12 |
+----+--------+------+
  • 此时我们开启事务一,然后执行更新id=12的数据,按照咱们的理论,id=12这个数据存在,说明MySQL只会锁定id=12这一行数据。
#开启事务一
begin;
#事务一只在12上加间隙锁
update t_gap_lock t set t.age = 12 where t.id = 12;
  • 然后我们开启事务二,然后执行更新id=13的数据,按照咱们的理论,id=13这个数据不存在,说明它会在12-正无穷(因为当前索引树上没有比12更大的值)之间加间隙锁
#开启事务二
begin;
#事务二在12-正无穷添加间隙锁(当前索引树最大值为12)
update t_gap_lock t set t.age = 13 where t.id = 13;
  • 那么重点来了,此时我们需要做的操作就是让事务一在12-正无穷之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。
#事务一在12-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (15,'十五',15);
  • 在事务一等待锁的同时,咱们让事务二在12-正无穷之间插入数据,这个时候会发现,事务二能够正常插入,说明事务二没有被间隙锁阻塞,待事务二提交或回滚后,事务一也正常提交。
#事务二在12-正无穷中新增数据
insert into t_gap_lock(id, name, age) values (13,'十六',16);
  • 通过以上验证,MySQL在更新id=12,即数据存在时,并没有对12-正无穷添加间隙锁,而是只锁定了id=12这一行数据,从而降低锁的颗粒度以提高性能。

最后,虽然咱们一起复现了间隙锁死锁、间隙锁加锁非互斥、以及间隙锁在数据存在时只锁定当前行这一系列场景,但是还是希望各位能够抽出宝贵的时间,能够自己验证一遍,真的非常有趣,希望大家都能真正的掌握这个知识,这也是我分享的最终目的。如果各位同学觉得对你有所帮助,请关注、点赞、评论、收藏来支持我,手头宽裕的话也可以赞赏来表达各位的认可,各位同学的支持是对我最大的鼓励。未来为大家带来更好的创作。 

分享一句非常喜欢的话:把根牢牢扎深,再等春风一来,便会春暖花开。

版权声明:以上引用信息以及图片均来自网络公开信息,如有侵权,请留言或联系

504401503@qq.com,立马删除。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖啡攻城狮Alex

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值