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

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方法),并发场景下,数据库报了如下错误


2 为什么是间隙锁死锁?

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

2.1 什么是死锁?

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

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;
*************************** 1. row ***************************lock_id: 749:0:360:3
lock_trx_id: 749lock_mode: X,GAPlock_type: RECORDlock_table: `test`.`t_gap_lock`lock_index: `PRIMARY`lock_space: 0lock_page: 360lock_rec: 3lock_data: 5
*************************** 2. row ***************************lock_id: 74A:0:360:3
lock_trx_id: 74Alock_mode: X,GAPlock_type: RECORDlock_table: `test`.`t_gap_lock`lock_index: `PRIMARY`lock_space: 0lock_page: 360lock_rec: 3lock_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加锁。

3 如何解决?

3.1 关闭间隙锁(不推荐)

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

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

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

3.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);}}

4 拓展

4.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: 75Clock_mode: Xlock_type: RECORDlock_table: `test`.`t_gap_lock`lock_index: `PRIMARY`lock_space: 0lock_page: 360lock_rec: 2lock_data: 1
*************************** 2. row ***************************lock_id: 75B:0:360:2
lock_trx_id: 75Block_mode: Xlock_type: RECORDlock_table: `test`.`t_gap_lock`lock_index: `PRIMARY`lock_space: 0lock_page: 360lock_rec: 2lock_data: 1
2 rows in set (0.00 sec)

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

5 结语

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

以上就是我们在生产环境间隙锁死锁分析的全过程,如果大家觉得本文让你对间隙锁,以及间隙锁死锁有一点的了解,别忘记一键三连,多多支持转转技术,转转技术在未来将会给大家带来更多的生产实践与探索。


转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/219464.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux中使用podman管理容器

本章主要介绍使用podman管理容器 了解什么是容器&#xff0c;容器和镜像的关系安装和配置podman拉取和删除镜像给镜像打标签导出和导入镜像创建和删除镜像数据卷的使用管理容器的命令使用普通用户管理容器 对于初学者来说&#xff0c;不太容易理解什么是容器&#xff0c;这里…

挑战与创新:光学字符识别技术在处理复杂表格结构中的应用

OCR&#xff08;Optical Character Recognition&#xff09;光学字符识别技术是指通过计算机软硬件将印刷或手写的字符转化为可编辑和搜索的文本。这项技术已经被广泛应用于各个领域&#xff0c;例如扫描文档、自动化数据输入、图书数字化等。但是&#xff0c;当涉及到处理复杂…

“ABCD“[(int)qrand() % 4]作用

ABCD[(int)qrand() % 4] 作用 具体来说&#xff1a; qrand() 是一个函数&#xff0c;通常在C中用于生成一个随机整数。% 4 会取 qrand() 生成的随机数除以4的余数。因为4只有四个不同的余数&#xff08;0, 1, 2, 3&#xff09;&#xff0c;所以这实际上会生成一个0到3之间的随…

java方法引用语法规则以及简单案例

目录 一、方法引用1.1 什么是方法引用1.2 方法引用的语法规则1.3 构造器引用1.4 方法引用的简单案例 参考资料 一、方法引用 1.1 什么是方法引用 方法引用是 Lambda 表达式的一种简写形式&#xff0c;用于表示已有方法的直接引用。 类似于lambda表达式&#xff0c;方法引用也…

window系统使用ESP8266开发板(CP2102)

连接开发板到电脑 虚拟机中选择连接的开发板硬件 查看设备管理器 更新驱动: CP210x USB to UART Bridge VCP Drivers - Silicon Labs 驱动安装成功

day03、关系模型之基本概念

关系模型之基本概念 1.关系模型概述1.1 关系模型三要素基本结构&#xff1a;relation/Table基本操作:relation operator 2.什么是关系3.关系模型中的完整性约束 本视频来源于B站&#xff0c;战德臣老师 1.关系模型概述 1.1 关系模型三要素 基本结构&#xff1a;relation/Table…

FlieZilla服务器配置与数据访问、传输

概述 手机apk当初服务器&#xff0c;PC端访问手机端的数据&#xff0c;再没有数据线的情况下&#xff0c;非常方便。希望各位同仁搞起来&#xff0c;在此做个笔录。 安装包下载链接&#xff1a;https://download.csdn.net/download/qq_36075612/88577274 一、下载安装包&…

2023.12.12 关于 Java 反射详解

目录 基本概念 定义 用途 反射相关的类 反射基本原理 Class 类中的相关方法 常用获得类相关的方法 常用获得类中属性相关的方法 常用获得类中构造器相关的方法 常用获得类中方法相关的方法 实例理解 反射优缺点 基本概念 定义 Java 的反射&#xff08;reflection&a…

算法笔记—链表、队列和栈

链表、队列和栈 1. 链表1.1 单链表反转1.2 双链表反转1.3 合并两个有序链表1.4 链表相加1.5 划分链表 2. 队列和栈2.1 循环队列2.2 栈实现队列2.3 队列实现栈2.4 最小栈2.2 双端队列 1. 链表 1.1 单链表反转 力扣 反转链表 // 反转单链表public ListNode reverseList(ListNod…

【RTOS学习】模拟实现任务切换 | 寄存器和栈的变化

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《RTOS学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 目录 &#x1f3c0;认识任务切换&#x1f3d0;切换的实质&#x1f3d0;栈中的内容&#x1f3d0;切…

基于ssm的前后端分离鲜花销售系统论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本鲜花销售系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&am…

java中的包

1.包的本质分析(原理) 包的本质 实际上就是创建不同的文件夹来保存类文件 2.一个文件中有两个类的i情况 package com.use;import com.xiaoqiang.Dog;public class Test {public static void main(String[] args) {Dog dog new Dog();System.out.println(dog); //com.xiaoqian…

最新版ES8的client API操作 Elasticsearch Java API client 8.0

作者&#xff1a;ChenZhen 本人不常看网站消息&#xff0c;有问题通过下面的方式联系&#xff1a; 邮箱&#xff1a;1583296383qq.comvx: ChenZhen_7 我的个人博客地址&#xff1a;https://www.chenzhen.space/&#x1f310; 版权&#xff1a;本文为博主的原创文章&#xff…

“京东API接口技术大揭秘:让你轻松驾驭电商开发!“

京东平台API接口技术贴 一、概述 京东平台提供了丰富的API接口&#xff0c;方便开发者进行应用开发。这些API接口涵盖了商品信息、订单管理、用户认证等多个方面&#xff0c;为开发者提供了强大的支持。本文将详细介绍京东平台API接口的技术细节和使用方法。 二、API接口概述…

回归预测 | MATLAB实现CHOA-BiLSTM黑猩猩优化算法优化双向长短期记忆网络回归预测 (多指标,多图)

回归预测 | MATLAB实现CHOA-BiLSTM黑猩猩优化算法优化双向长短期记忆网络回归预测 &#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现CHOA-BiLSTM黑猩猩优化算法优化双向长短期记忆网络回归预测 &#xff08;多指标&#xff0c;多图&#xff09;效果…

CMake是什么

文章目录 一.什么是CMake二.CMake安装三.CMake一个HelloWord-的语法介绍3.1 PROJECT关键字3.2 SET关键字3.3 MESSAGE关键字3.4 ADD_EXECUTABLE关键字3.5 include_directories关键字3.6 aux_source_directory 四.语法的基本原则4.1 语法注意事项 五.内部构建和外部构建5.1 外部构…

dialog 在xml文件进行了自适应宽,但是失效了

如下图 讲述了为什么已经设置好了dialog的宽高 到了显示的时候就会失效的原因 解决方式 &#xff1a; 在自定的dialog中的onstart()方法中进行重新设置宽高 Window window getWindow();WindowManager.LayoutParams lp window.getAttributes();lp.height LinearLayout.La…

【操作系统的IO模型有哪些?】

操作系统的IO模型有哪些&#xff1f; 操作系统中的IO模型逐一拓展同步阻塞IO模型同步非阻塞IO模型IO复用模型信号驱动IO模型异步IO模型 操作系统中的IO模型 为了保护操作系统的安全&#xff0c;通过缓存加快系统读写&#xff0c;会将内存分为用户空间和内存空间两个部分。如果…

想学精MySQL,得先捋一捋高可用架构

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

ARM day8

1.题目&#xff1a;主机获取从机里面的温湿度数据&#xff0c;并打印出来 结果&#xff1a; 代码&#xff1a; main.c #include "iic.h"#include "si7006.h"void delay(int ms){int i,j;for(i0;i<ms;i){for(j0;j<2000;j);}}int main(){short tem;…