您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

Mybatis-Plus可能会导致数据库死锁

时间:2023-08-14 16:25:27  来源:  作者:dbaplus社群

一、场景还原

1.版本信息

MySQL版本:5.6.36-82.1-log

MyBatis-Plus的starter版本:3.3.2

存储引擎:InnoDB

2.死锁现象

A同学在生产环境使用了Mybatis-Plus提供的

com.baomidou.mybatisplus.extension.service.IService#saveOrUpdate(T, com.baomidou.mybatisplus.core.conditions.WrApper) 方法(以下简称B方法),并发场景下,数据库报了如下错误图片。

二、为什么是间隙锁死锁?

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

1.什么是死锁?

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

2.什么是间隙锁?

间隙锁是MySQL行锁的一种,与Record lock不同的是,间隙锁锁定的是一个间隙。

锁定规则如下:

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

3.MySQL为什么要引入间隙锁?

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

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方法,并发场景下,是不是就很大几率会满足事务一和事务二相互等待对方持有的间隙锁,从而导致死锁。

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

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;

*************************** 1. row ***************************

lock_id: 749:0:360:3

lock_trx_id: 749

lock_mode: X,GAP

lock_type: RECORD

lock_table: `test`.`t_gap_lock`

lock_index: `PRIMARY`

lock_space: 0

lock_page: 360

lock_rec: 3

lock_data: 5

*************************** 2. row ***************************

lock_id: 74A:0:360:3

lock_trx_id: 74A

lock_mode: X,GAP

lock_type: RECORD

lock_table: `test`.`t_gap_lock`

lock_index: `PRIMARY`

lock_space: 0

lock_page: 360

lock_rec: 3

lock_data: 5

2 rows in set (0.00 sec)

根据lock_type和lock_mode我们可以很清晰的看到锁类型是行锁,锁模式是间隙锁。

  • 与此同时我们在事务二中执行如下语句
  •  

insert into t_gap_lock(id, name, age) value (4,'间隙锁4',24);

一执行以上语句,数据库就立马报了死锁,并且回滚了事务二(可以在死锁日志中看到*** WE ROLL BACK TRANSACTION (2))

  •  

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

到这里,细心的同学就会发现,诶,你这上面故意造了一个间隙,并且让两个事务分别在对方的间隙中插入数据,太刻意了,生产环境基本上不会有这种场景,是的,生产环境怎么会有这种场景呢,上面的数据只是为了让大家直观的看到间隙锁的死锁过程,接下来那我们再来一组数据,我们简称验证二。

  • 我们还是以验证一的表结构与数据,我们来执行这样一个操作。首先我们开始开启事务一并且执行如下操作,依然不提交事务
  •  

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 = 4;

Query OK, 0 rows affected (0.00 sec)

Rows matched: 0 Changed: 0 Warnings: 0

  • 于是乎我们在事务一执行如下操作,我们又惊奇地发现事务一被阻塞了
  •  

insert into t_gap_lock(id, name, age) value (4,'间隙锁4',24);

  • 在事务一被阻塞的同时,我们在事务二执行同样的语句,我们发现数据库立马就报了死锁
  •  

insert into t_gap_lock(id, name, age) value (4,'间隙锁4',24);

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

验证二完整复现了线上死锁的过程,也就是事务一先执行了更新语句,事务二在同一时刻也执行了更新语句,然后事务一发现没有更新到就去执行主键查询语句,发现确实没有,所以执行了插入语句,但是插入要先获取插入意向锁,在获取插入意向锁的时候发现这个间隙已经被事务二加锁了,所以事务一开始等待事务二释放间隙锁。同理,事务二也执行上述操作,最终导致事务一与事务二互相等待对方释放间隙锁,最终导致死锁。

验证二还说明了一个问题,就是间隙锁加锁是非互斥的,也就是事务一对间隙A加锁后,事务二依然可以给间隙A加锁。

三、如何解决?

1.关闭间隙锁(不推荐)

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

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

PS:以上方法仅适用于当前业务场景确实不关心幻读的问题。

2.自定义saveOrUpdate方法(推荐)

建议自己编写一个saveOrUpdate方法,当然也可以直接采用Mybatis-Plus提供的saveOrUpdate方法,但是根据源码发现,会有很多额外的反射操作,并且还添加了事务,大家都知道,MySQL单表操作完全不需要开事务,会增加额外的开销。

  •  

@Transactional(

rollbackFor = {Exception.class}

public boolean saveOrUpdate(T entity) {

if (null == entity) {

return false;

} else {

Class<?> cls = entity.getClass();

TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);

Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!", new Object[0]);

String keyProperty = tableInfo.getKeyProperty();

Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!", new Object[0]);

Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());

return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity);

}

}

四、拓展

1.如果两个事务同时修改存在的行会发生什么?

在验证二中两个事务修改的都是不存在的行,都能加间隙锁成功,那如果两个事务修改的是存在的行,MySQL还会加间隙锁吗?或者说把间隙锁从锁间隙降为锁一行?带着疑问,我们执行以下数据验证,我们还是使用验证一的表和数据。

  • 首先我们开启事务一执行以下语句
  •  

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 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 = 1;

  • 这个时候我们执行以下语句看下当前正在锁的事务。
  •  

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS G;

*************************** 1. row ***************************

lock_id: 75C:0:360:2

lock_trx_id: 75C

lock_mode: X

lock_type: RECORD

lock_table: `test`.`t_gap_lock`

lock_index: `PRIMARY`

lock_space: 0

lock_page: 360

lock_rec: 2

lock_data: 1

*************************** 2. row ***************************

lock_id: 75B:0:360:2

lock_trx_id: 75B

lock_mode: X

lock_type: RECORD

lock_table: `test`.`t_gap_lock`

lock_index: `PRIMARY`

lock_space: 0

lock_page: 360

lock_rec: 2

lock_data: 1

2 rows in set (0.00 sec)

根据lock_type和lock_mode我们看到事务一和二加的锁变成了Record Lock,并没有再添加间隙锁,根据以上数据验证MySQL在修改存在的数据时会给行加上Record Lock,与间隙锁不同的是该锁是互斥的,即不同的事务不能同时对同一行记录添加Record Lock。

五、结语

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

作者丨谢星

来源丨公众号:转转技术(ID:zhuanzhuantech)



Tags:Mybatis-Plus   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
如何在Spring项目中配置MP(MyBatis-Plus)集成?
在Spring项目中集成MP,需要进行以下配置:1. 引入依赖:在项目的pom.xml文件中添加MP相关依赖,例如:```xml<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plu...【详细内容】
2024-01-09  Search: Mybatis-Plus  点击:(86)  评论:(0)  加入收藏
对比 MyBatis 和 MyBatis-Plus 批量插入、批量更新的性能和区别
1 环境准备1.1 搭建 MyBatis-Plus 环境 创建 maven springboot 工程 导入依赖:web 启动器、jdbc、、java 连接 mysql、Lombok、druid 连接池启动器、mybatis-plus 启动器 编...【详细内容】
2023-09-08  Search: Mybatis-Plus  点击:(191)  评论:(0)  加入收藏
Mybatis-Plus可能会导致数据库死锁
一、场景还原1.版本信息MySQL版本:5.6.36-82.1-logMybatis-Plus的starter版本:3.3.2存储引擎:InnoDB2.死锁现象A同学在生产环境使用了Mybatis-Plus提供的com.baomidou.mybatisp...【详细内容】
2023-08-14  Search: Mybatis-Plus  点击:(171)  评论:(0)  加入收藏
超越 Mybatis-Plus?超强 ORM 框架横空出世
最近逛开源社区的时候,偶尔发现了一个 MyBatis 增强框架-MyBatis-Flex ,感觉挺不错的,集合了 MyBatis-Plus、Fluent-MyBatis 的优点。 MyBatis-Flex 的作者是一位技术大佬,还开...【详细内容】
2023-07-27  Search: Mybatis-Plus  点击:(265)  评论:(0)  加入收藏
SpringBoot整合Mybatis-Plus多数据源
MyBatis-Plus则是一个优秀的ORM框架,它为我们封装了大量的数据库操作细节,简化了我们的开发工作,同时也提供了多数据源方案。一、前言随着业务的不断扩展和复杂度的增加,我们在...【详细内容】
2023-06-07  Search: Mybatis-Plus  点击:(225)  评论:(0)  加入收藏
框架之选:为什么 MyBatis-Plus 被越来越多的开发者使用
MyBatis-Plus 是一个基于 MyBatis 的增强工具,它提供了许多实用的功能和工具,可以大大简化 MyBatis 的开发过程。本文将介绍 MyBatis-Plus 的使用过程、底层原理以及相关的代...【详细内容】
2023-03-21  Search: Mybatis-Plus  点击:(103)  评论:(0)  加入收藏
SpringBoot+Mybatis-Plus整合Sharding-JDBC5.1.1实现单库分表
一、前言小编最近一直在研究关于分库分表的东西,前几天docker安装了mycat实现了分库分表,但是都在说mycat的bug很多。很多人还是倾向于shardingsphere,其实他是一个全家桶,有JDB...【详细内容】
2023-03-05  Search: Mybatis-Plus  点击:(392)  评论:(0)  加入收藏
MySql中json类型数据的查询以及在MyBatis-Plus中的使用
表结构和初始数据 新建表结构CREATE TABLE `json_test` ( `id` int NOT NULL AUTO_INCREMENT, `roles` json DEFAULT NULL COMMENT &#39;角色&#39;, `project` json DEFA...【详细内容】
2022-11-18  Search: Mybatis-Plus  点击:(734)  评论:(0)  加入收藏
还在手写SQL实现?试试MyBatis-Plus同款IDEA插件吧!一键生成
最近发现之前使用的MyBatis插件很久都没更新了,就想换个其他插件来用用。偶然发现MyBatis-Plus团队也开发了一款插件MyBatisX,体验了一把确实非常好用,提示很全,而且还能通过GUI...【详细内容】
2022-11-01  Search: Mybatis-Plus  点击:(467)  评论:(0)  加入收藏
MyBatis-Plus条件构造器,从此优雅写SQL
前言本章主要介绍MyBatis-Plus的条件构造器,条件构造器是MyBatis-Plus的核心,可以使用面向对象的方式来实现查询,本文内容涵盖开发的90%以上查询场景,我们慢慢往下看一、条件构...【详细内容】
2022-10-25  Search: Mybatis-Plus  点击:(514)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条