当前位置:首页 >知识 >慎用,Mybatis 所以凡事都有两面性

慎用,Mybatis 所以凡事都有两面性

2024-06-25 19:32:34 [百科] 来源:避面尹邢网

慎用,慎用Mybatis-Plus这个方法可能导致死锁

作者:谢星 开发 前端 虽然Mybatis-Plus提供的慎用这个方法可能会造成死锁,但是慎用依然不可否认它是一款非常优秀的增强框架,其提供的慎用lambda写法在日常工作中极大的提高了我们的开发效率,所以凡事都有两面性,慎用我们应该秉承辩证的慎用态度,熟悉的慎用方法尝试用,陌生的慎用方法谨慎用。

1 场景还原

1.1 版本信息

MySQL版本:5.6.36-82.1-log Mybatis-Plus的慎用starter版本:3.3.2存储引擎:InnoDB

1.2 死锁现象

A同学在生产环境使用了Mybatis-Plus提供的 com.baomidou.mybatisplus.extension.service.IService#saveOrUpdate(T, com.baomidou.mybatisplus.core.conditions.Wrapper) 方法(以下简称B方法),并发场景下,慎用数据库报了如下错误

图片

慎用,Mybatis 所以凡事都有两面性


2 为什么是间隙锁死锁?

如上图示,数据库报了死锁,慎用那死锁场景千万种,慎用为什么确定B方法是慎用由于间隙锁导致的死锁?

慎用,Mybatis 所以凡事都有两面性

2.1 什么是死锁?

两个事务互相等待对方持有的锁,导致互相阻塞,慎用从而导致死锁。

慎用,Mybatis 所以凡事都有两面性

2.2 什么是间隙锁?

  • 间隙锁是MySQL行锁的一种,与Record lock不同的是间隙锁锁定的是一个间隙。
  • 锁定规则如下:

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

2.3 MySQL为什么要引入间隙锁?

与Record lock组合成Next-key lock,在可重复读这种隔离级别下一起工作避免幻读。

2.4 间隙锁死锁分析

理论上一款开源的框架,经过了多年打磨,提供的方法不应该造成如此严重的错误,但理论仅仅是理论上,事实就是发生了死锁,于是我们开始了一轮深度排查。首先我们从这个方法的源码入手,源码如下:

default boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper) {         return this.update(entity, updateWrapper) || this.saveOrUpdate(entity);    }

从源码上看此方法就没有按套路出牌,正常逻辑应该是首先执行查询,存在则修改,不存在则新增,但此方法上来就执行了修改。我们就猜想是不是MySQL在修改时增加了什么锁导致了死锁,于是我们找到了DBA获取了最新的死锁日志,即执行show engine innodb status,我们发现了两项关键信息如下:

*** (1) TRANSACTION:...省略日志*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71C lock_mode X locks gap before rec insert intention waiting  *** (2) TRANSACTION:...省略日志*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71D lock_mode X locks gap before rec insert intention waiting

简单翻译一下,就是事务一在获取插入意向锁时,需要等待间隙锁(事务二添加)释放,同时事务二在获取插入意向锁时,也在等待间隙锁释放(事务一添加),(本文不讨论MySQL在修改与插入时添加的锁,我们把修改时添加间隙锁,插入时获取插入意向锁为已知条件)那我们回到B方法,并发场景下,是不是就很大几率会满足事务一和事务二相互等待对方持有的间隙锁,从而导致死锁。

现在我们理论有了,我们现在用真实数据来验证此场景。

2.5 验证间隙锁死锁

  • 准备如下表结构(以下简称验证一)
create table t_gap_lock(id int auto_increment primary key comment '主键ID',name varchar(64) not null comment '名称',age int not null comment '年龄') comment '间隙锁测试表';
  • 准备如下表数据
mysql> select * from t_gap_lock;+----+------+-----+| id | name | age |+----+------+-----+|  1 | 张三 |  18 ||  5 | 李四 |  19 ||  6 | 王五 |  20 ||  9 | 赵六 |  21 || 12 | 孙七 |  22 |+----+------+-----+
  • 我们开启事务一,并执行如下语句,注意这个时候我们还没有提交事务
mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> update t_gap_lock t set t.age = 25 where t.id = 4;Query OK, 0 rows affected (0.00 sec)Rows matched: 0  Changed: 0  Warnings: 0
  • 同时我们开启事务二,并执行如下语句,事务二我们同样不提交事务
mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> update t_gap_lock t set t.age = 25 where t.id = 7;Query OK, 0 rows affected (0.00 sec)Rows matched: 0  Changed: 0  Warnings: 0
  • 接下来我们在事务一中执行如下语句
mysql> insert into t_gap_lock(id, name, age) value (7,'间隙锁7',27);
  • 我们会发现事务一被阻塞了,然后我们执行以下语句看下当前正在锁的事务。
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;

(责任编辑:热点)

    推荐文章
    热点阅读