多线程场景下谨慎使用@Transactional注解,你不信我也没办法

最近遇到一个很诡异的bug,觉得很有趣也很值得分享,于是想写篇文章记录下来,希望有缘人看到以后少踩坑~


先简单说下场景:有个任务平台,功能很多但我们只关注 提交任务和取消任务 两个功能,并且取消任务后会有消息通知

业务代码不方便透露,写个简化的伪代码帮助理解吧

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)public void cancel() {Job job = select();//查出job才进入ifif (job != null) {update();//这个方法跟bug无关,可以忽略delete(job);//删除成功后,下一次执行就不会进入ifsendMsg();//发送通知}}

业务流程:任务提交执行后,可以通过cancel方法取消执行,cancel方法内部逻辑很好理解,先查询一个job对象,如果对象不为null则进行下面的一系列操作,因为涉及到多个写操作,所以整个方法加了注解@Transactional用于事务控制

下面是我的排查过程,极其精彩,极其费头发!!
在这里插入图片描述

bug描述:提交任务后,有三种情况

  1. 👉提交任务后立马取消,此时不发送通知,正常
  2. 👉提交后在很短时间内取消(任务执行时间在六秒内),此时发送两条通知,异常
  3. 👉提交后正常取消(任务执行超过十秒),此时发送一条通知,正常

问题就在第二种情况

上面的规律看起来简单,其实是花了很长很长很长很长的时间才发现的,从发现这个规律开始才找到了稳定复现bug的方法

又花了很长很长很长很长时间,我确定cancel方法在上面第二种情况时会执行两次,并且两次执行间隔很短


两次间隔时间大概就这么点

所以,我很自信的判定这是由多线程导致的,对于一个多写的操作,不应该允许多个线程异步执行

具体执行情况:

T时刻,两个线程同时执行cancel方法
A线程和B线程读取到的job都不为null,于是都进入if语块
导致sendMsg()执行两次,所以发送两次消息通知

给大家看下消息通知长啥样,注意看时间,其实相差了几十纳秒,只是没显示出来

在这里插入图片描述

原来问题这么简单!

于是,我给整个方法加上 synchronized

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)public synchronized void cancel() {//加上synchronized Job job = select();if (job != null) {update();//这个方法跟bug无关,可以忽略delete(job);//删除成功后,下一次执行就不会进入ifsendMsg();//发送通知}}

完事儿测试发现不行

我就知道你synchronized是渣锁不靠谱,我换lock

换lock测试也不行
在这里插入图片描述

加锁后虽然线程能顺序执行,但依然会发送两次通知

麻了

好好好,这么玩是吧


继续分析,方向肯定没错,两个线程select都不为null,所以继续往下执行导致bug

但是我已经加了锁了,从日志看线程也已经同步执行了为啥还是不行(加了日志后能看出来是一个线程执行完了才执行另一个)

此时,我把目光集中在了@Transactional注解

会不会是事务导致的??

为了验证猜想,我把事务直接注释掉,测试发现居然行了

这么神奇?尊嘟假嘟~
在这里插入图片描述

它虽然行了,但业务不行,谁家好人这种多写操作不加事务啊

虽然但是,到这里我几乎确定了bug跟事务有关

原来问题这么简单!+1

继续猜测,有没有可能是事务虽然提交了,但执行删除操作需要时间,还没来得及删除成功第二个线程就进来,此时查到的job是不为null的,所以才出现bug

为了验证这个猜想,我在方法结束前加了个sleep,既然你删除需要时间,我就给你时间

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)public synchronized void cancel() {//加上synchronized Job job = select();if (job != null) {update();//这个方法跟bug无关,可以忽略delete(job);//删除成功后,下一次执行就不会进入ifsendMsg();//发送通知}Thread.sleep(100);//给删除操作预留时间}

测试发现又行了

它虽然行了,但业务还是不行,谁家好人用这么取巧的手段啊,万一网络波动或者数据库卡了导致删除操作延时,bug还是会复现

虽然但是,到这里我几乎确定了bug跟事务提交后的删除操作耗时有关

原来问题这么简单!+2

继续排查掉头发

很快想到了新疑点:声明式事务其实有个弊端,它提交事务的时机是在方法执行完成后的,记住这句话,后面要考

所以,有没有可能是因为锁的释放时机和事务提交时机导致的,锁是方法执行完释放,事务也是方法执行完才提交,那问题就出在锁刚刚释放,第二个线程立马拿到锁入栈搞偷袭

在这里插入图片描述

好好好,原来是你不讲武德搞偷袭

原来问题这么简单!+3

继续验证猜想

其实很好验证,将声明式事务改成手动提交事务即可

    public synchronized void cancel() {//加上synchronized try {// 创建数据库连接connection = DriverManager.getConnection(url, username, password);// 开始事务connection.setAutoCommit(false);// 执行一些数据库操作Job job = select();if (job != null) {update();//这个方法跟bug无关,可以忽略delete(job);//删除成功后,下一次执行就不会进入ifsendMsg();//发送通知}//提交事务connection.commit();} catch (SQLException e) {e.printStackTrace();//回滚事务try {if (connection != null) {connection.rollback();}} catch (SQLException ex) {ex.printStackTrace();}} finally {// 关闭数据库连接try {if (connection != null) {connection.close();}} catch (SQLException e) {e.printStackTrace();}}}

测试发现确实可以,多次测试也未发现异常

ok破案

至此,bug就算是修复了

但是,第二天我又想起这个bug,忍不住多思考了一下

有没有可能通过修改事务隔离级别来实现??

其实通过加锁和手动提交事务达到的效果,理论上确实可以通过隔离级别来实现

原来问题这么简单!+4

继续验证猜想

当前的数据库隔离级别是READ_COMMITTED,先设置到REPEATABLE_READ试试

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)//指定隔离级别public synchronized void cancel() {//加上synchronized Job job = select();if (job != null) {update();//这个方法跟bug无关,可以忽略delete(job);//删除成功后,下一次执行就不会进入ifsendMsg();//发送通知}}

测试发现8太行

估计还是事务注解提交事务的时机导致,READ_COMMITTED虽然能保证事务内多次读取同一条数据是一样的,但保证不了删除数据

直接设置成SERIALIZABLE试试

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)//指定隔离级别public synchronized void cancel() {//加上synchronizedJob job = select();if (job != null) {update();//这个方法跟bug无关,可以忽略delete(job);//删除成功后,下一次执行就不会进入ifsendMsg();//发送通知}}

欸嗨,可以了

多次测试也未发现问题

看来还是得让两个线程串行,SERIALIZABLE手动提交事务并且加锁的原理和效果其实是一样的,都是从源头上保证一个事务内只有一个线程执行

原来问题这么简单!+10086

至此,bug正式修复

bug是修复了,头发没了

在这里插入图片描述

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

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

相关文章

blender基本操作

文章目录 引言一、选择二、移动1. xyz轴移动2. xyz平面移动3. 精确移动4. 快捷键移动G 三、旋转四、缩放五、变换1. 变换坐标系2. 变换轴心: 六、吸附七、模式切换八、物体的合并和分离1. 合并2.分离 九、设置父子级关系十、叠加层和快速收藏夹1. 叠加层2. 快速收藏…

【算法与数据结构】106、LeetCode从中序与后序遍历序列构造二叉树

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析:首先我们要知道后序遍历数组的最后一个元素必然是根节点,然后根据根节点在中序遍历数组中的…

NSSCTF web 刷题记录1

文章目录 前言题目[GXYCTF 2019]禁止套娃方法一方法二 [NCTF 2019]Fake XML cookbook[NSSRound#7 Team]ec_RCE[NCTF 2018]Flask PLUS 前言 今天是2023.9.3,大二开学前的最后一天。老实说ctf的功力还是不太够做的题目太少,新学期新气象。不可急于求成&am…

【Docker】 08-Dockerfile

什么是Dockerfile Dockerfile可以认为是Docker镜像的描述文件,是由一系列命令和参数构成的教程,主要作用是用来构建docker镜像的构建文件。 Dockerfile解析过程 Dockerfile的保留命令 保留字作用FROM当前镜像是基于哪个镜像的 第一个指令必须是FROMMA…

SpringAOP详解

目录 代理模式 静态代理 动态代理 AOP spring api实现 自定义类实现 使用注解实现 代理模式 AOP的底层机制就是动态代理,所以学习aop之前 , 我们要先了解一下代理模式。 静态代理 拿租房的例子进行举例 抽象角色 : 一般使用接口或者抽象类来实现&#xff…

【python爬虫】—豆瓣电影Top250

豆瓣电影Top250 豆瓣榜单简介需求描述Python实现 豆瓣榜单简介 豆瓣电影 Top 250 榜单是豆瓣网站上列出的评分最高、受观众喜爱的电影作品。这个榜单包含了一系列优秀的影片,涵盖了各种类型、不同国家和时期的电影。 需求描述 使用python爬取top250电影&#xff…

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析}

AVL树 一、AVL树的概念 二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明…

分类算法系列②:KNN算法

目录 KNN算法 1、简介 2、原理分析 数学原理 相关公式及其过程分析 距离度量 k值选择 分类决策规则 3、API 4、⭐案例实践 4.1、分析 4.2、代码 5、K-近邻算法总结 🍃作者介绍:准大三网络工程专业在读,努力学习Java,涉…

centos7下docker设置新的下载镜像源并调整存放docker下载镜像的仓库位置

目录 1.设置镜像源 2.调整存放下载镜像的仓库位置 1.设置镜像源 在 /etc/docker下创建一个daemon.json文件。在json中下入 "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"] 完成配置 加载配置 systemctl daemon-reload 重启docker sy…

docker安装redis实操记录

1.Docker拉取镜像 docker pull redis2.Docker挂载配置文件 创建挂载文件夹 mkdir -p /home/redis/data下载默认配置文件 redis.conf 3.启动redis 容器 docker run --restartalways --log-opt max-size100m --log-opt max-file2 -p 6379:6379 --name redis -v /home/redi…

java IDEA文件路径分层级

如下图这样 在设置里找到Compact Middle Packages,去掉勾选就行了

【Linux系列】vmware虚拟机网络配置详解

非原创 原文地址[1] 首发博客地址[2] 系列文章地址[3] vmware 为我们提供了三种网络工作模式,它们分别是:Bridged(桥接模式)、NAT(网络地址转换模式)、Host-Only(仅主机模式)。 打开…