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

news2025/4/20 1:41:01

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


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

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

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

业务流程:任务提交执行后,可以通过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);//删除成功后,下一次执行就不会进入if
            sendMsg();//发送通知
        }
    }

完事儿测试发现不行

我就知道你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);//删除成功后,下一次执行就不会进入if
            sendMsg();//发送通知
        }
        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);//删除成功后,下一次执行就不会进入if
           	 sendMsg();//发送通知
        	}
            //提交事务
            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);//删除成功后,下一次执行就不会进入if
            sendMsg();//发送通知
        }
    }

测试发现8太行

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

直接设置成SERIALIZABLE试试

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

欸嗨,可以了

多次测试也未发现问题

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

原来问题这么简单!+10086

至此,bug正式修复

bug是修复了,头发没了

在这里插入图片描述

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

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

相关文章

全民拼购模式:美妆行业的新机遇和挑战

美妆是一个充满创意和变化的行业,每个人都想拥有自己独特的美丽风格。但是,美妆产品的价格和品质却不尽相同,很多消费者在购买时会遇到困惑和不满。有没有一种方法,可以让消费者以更低的价格买到更好的美妆产品,同时还…

八个针对高级职位的高级 JavaScript 面试题

JavaScript 是一种功能强大的语言,是网络的主要构建块之一。这种强大的语言也有一些怪癖。例如,您是否知道 0 -0 的计算结果为 true,或者 Number("") 的结果为 0? 问题是,有时这些怪癖会让你摸不着头脑&…

了解 glTF 2.0 格式

推荐:使用 NSDT场景编辑器快速搭建3D应用场景 介绍 glTF 代表 GL 传输格式。 glTF 是一种用于存储和加载 3D 场景的标准化文件格式,其基本目的是由 3D 创建工具轻松生成并被任何图形应用程序使用,无论使用何种 API,处理最少。 …

软考(1)-面向对象的概念

目录 一. 软考基本信息 1. 软考时间: 2. 软考科目: 3.专业知识介绍 -- 综合知识考点分布 4. 专业介绍 -- 软件设计考点分布 二. 面向对象概念 1. 封装 考点一:对象 考点二:封装private 2. 继承 考点三:类 考…

python初阶

基本类型 int float str bool list--列表 [1,2,3] tuple--元组 (1,2,3) set---集合 {1,2,3} dict---字典 {1:”python","acwing":2,3:4.0}int支持高精度,没有大小限制 赋值 a3 b3.0 c‘s’ d“asdf” 单引号和双引号是没有区别…

macos 不支持svn安装

macos 10.13可能不支持svn命令,所以要安装 xcode-select --install 弹窗在线安装失败的话只能手动下载安装 打开:Sign In - Apple 搜索Command Line Tools (macOS 10.13) 下载9.4.1版本直接安装后即可

每天一个工业通信协议(4)2023.8.30 (I2C接口)

文章目录 参考文献1.I2C介绍2.I2C接口2.1 位传输,识别2.2 传输数据3.I2C的电气连接参考文献 广州周立功单片机发展有限公司 《I2C总线规范》 1.I2C介绍 为了使这些相似之处对系统设计者和器件厂商都得益 而且使硬件效益最大电路最简单 Philips 开发了一个简单的双向两线总线…

L1-012 计算指数 C++

#include<iostream> #include<math.h> using namespace std; int main() {int n;int ret;cin >> n;if (n < 10) {ret pow(2, n);cout << "2^" << n << " " << ret<<endl;}return 0; } 所用知识点 …

入选VLDB 2023! OceanBase 4.0单机分布式一体化研究成果获国际顶会认可

近日&#xff0c;国际顶级数据库学术会议 VLDB 2023 在加拿大温哥华开幕。OceanBase 研究成果论文 “OceanBase Paetica: A Hybrid Shared-nothing/Shared-everything Database for Supporting Single Machine and Distributed Cluster” 被 VLDB 2023 收录&#xff01;相继 20…

pytorch搭建squeezenet网络的整套工程,及其转tensorrt进行cuda加速

本来&#xff0c;前辈们用caffe搭建了一个squeezenet的工程&#xff0c;用起来也还行&#xff0c;但考虑到caffe的停更后续转trt应用在工程上时可能会有版本的问题所以搭建了一个pytorch版本的。 以下的环境搭建不再细说&#xff0c;主要就是pyorch&#xff0c;其余的需要什么p…

Stable Diffusion stable-diffusion-webui开发笔记

https://lexica.art/ lexica.art 该网站拥有数百万Stable Diffusion案例的文字描述和图片&#xff0c;可以为大家提供足够的创作灵感。可以提供promt灵感 https://civitai.com/ Civitai是一个聚集AI绘图爱好者的社区&#xff0c;在此网站上有许多定制化的模型&#xff0c;特…

【PyGame/PyQy5】get-start 快速入门

1.windows、linux&#xff0c;mac 安装python3 2.PC系统安装pygame, 指令&#xff1a;pip install pygame 3. 保存如下文件&#xff1a; test_game.py 4.PC上运行 python test_game.py import pygame# 初始化Pygame pygame.init()# 创建游戏窗口 window pygame.display.s…

SAP FI 之定义利润中心(Profit Center)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 SAP中的利润中心是SAP Controlling内部控制的一个组织单元。它帮助组织管理成本和收…

基于snat+dnat发布内网K8S及Jenkins+gitlab+Harbor模拟CI/CD的综合项目

目录 项目名称 项目架构图 项目环境 项目概述 项目准备 项目步骤 一、修改每台主机的ip地址&#xff0c;同时设置永久关闭防火墙和selinux&#xff0c;修改好主机名&#xff0c;在firewalld服务器上开启路由功能并配置snat策略。 1. 在firewalld服务器上配置ip地址、设…

blender基本操作

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

【python基础知识】2.if条件判断与条件嵌套

文章目录 前言条件判断单向判断&#xff1a;if双向判断&#xff1a;if…else…多向判断&#xff1a;if…elif…else… if嵌套if嵌套的执行顺序 如何写嵌套代码 前言 在上一关&#xff0c;我带领你攻克了如何与Python沟通的语言大关——通过三种数据类型&#xff08;字符串、整…

【ALM工具软件】上海道宁与Perforce为您带来用于整个生命周期的应用程序生命周期管理软件

Helix ALM是 用于整个生命周期的 应用程序生命周期管理的ALM软件 具有专用于 需求管理&#xff08;Helix RM&#xff09;、测试用例管理&#xff08;Helix TCM&#xff09; 问题管理&#xff08;Helix IM&#xff09;的功能模块 Helix ALM提供了 无与伦比的可追溯性 您将…

【MySQL学习笔记】(十)使用索引的操作

简单使用索引 创建索引创建主键索引创建唯一键索引普通索引的创建 查询索引删除索引索引创建原则 数据库中的索引与书籍中的索引类似&#xff0c;在一本书中&#xff0c;利用索引可以快速地查找所需信息&#xff0c; 而无须阅读整本书。在数据库中&#xff0c;索引使数据库程序…

跨专业可读的免统考双证的中国社科院-美国杜兰大学金融硕士

跨专业可读的免统考双证的中国社科院-美国杜兰大学金融硕士 项目优势 一、跨专业申请优势 中国社科院-美国杜兰大学金融硕士的招生对象不仅仅是金融行业的高端人才&#xff0c;因为金融业门槛相对较高&#xff0c;进入者往往已经具备了硕士晋升的学习能力&#xff0c;所以我…

【Github】git本地仓库建立与远程连接

文章目录 前言一、git简介二、git下载2.1下载地址 三、git安装3.1安装3.2 配置3.3 config设置&#xff08;增删改查&#xff09; 四.github与git连接——本地Git仓库4.1 建本地的版本库4.2 源代码放入本地仓库4.3提交仓库 五、github与git的连接——远程连接5.1 创建SSH Key5.2…