多线程场景下谨慎使用@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.rhkb.cn/news/121175.html

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

相关文章

K210-CanMV IDE开发软件

K210-CanMV IDE开发软件 界面功能简介连接设备临时运行开机运行程序 界面功能简介 区域①菜单栏:操作文件,使用工具等。 区域②快捷按钮:区域①中的文件和编辑中部分功能的快捷方式。 区域③连接设备:连接设备和程序控制按钮。 …

【网络安全带你练爬虫-100练】第20练:数据处理-并写入到指定文档位置

目录 一、目标1:解码去标签 二、目标2:提取标签内内容 三、目标3:处理后的数据插入原位置 四、目标4:将指定的内容插入指定的位置 五、目标5:设置上下文字体格式 六、目标6:向多个不同位置插入不同的…

PMP是什么?项目管理专业人士资格认证介绍

PMP认证旨在评估和确认具备一定经验和知识的项目管理专业人士的能力。通过获得PMP认证,项目经理可以证明他们具备在各个行业中成功领导和管理项目所需的技能。这些技能包括十二原则、8大绩效等方面的知识。 以下是PMP认证的详细介绍: 1. 资格要求&…

详解TCP/IP的三次握手和四次挥手

文章目录 前言一、TCP/IP协议的三次握手1.1 三次握手流程 二、TCP/IP的四次挥手2.1 四次挥手流程 三、主要字段3.1、标志位(Flags)3.2、序号(sequence number)3.3、确认号(acknowledgement number) 四、状态…

PAT 1164 Good in C 测试点3,4

个人学习记录,代码难免不尽人意。 When your interviewer asks you to write “Hello World” using C, can you do as the following figure shows? Input Specification: Each input file contains one test case. For each case, the first part gives the 26 …

手写Mybatis:第9章-细化XML语句构建器,完善静态SQL解析

文章目录 一、目标:XML语句构建器二、设计:XML语句构建器三、实现:XML语句构建器3.0 引入依赖3.1 工程结构3.2 XML语句构建器关系图3.3 I/O资源扫描3.4 SQL源码3.4.1 SQL对象3.4.2 SQL源码接口3.4.3 原始SQL源码实现类3.4.4 静态SQL源码实现类…

《TCP/IP网络编程》阅读笔记--地址族和数据序列

目录 1--IP地址和端口号 2--地址信息的表示 3--网络字节序与地址变换 4--网络地址的初始化与分配 5--Windows部分代码案例 1--IP地址和端口号 IP 地址分为两类: ① IPv4 表示 4 字节地址族; ② IPv6 表示 16 字节地址族; IPv4 标准的 4 …

B081-Lucene+ElasticSearch

目录 认识全文检索概念lucene原理全文检索的特点常见的全文检索方案 Lucene创建索引导包分析图代码 搜索索引分析图代码 ElasticSearch认识ElasticSearchES与Kibana的安装及使用说明ES相关概念理解和简单增删改查ES查询DSL查询DSL过滤 分词器IK分词器安装测试分词器 文档映射(字…

使用ELK收集解析nginx日志和kibana可视化仪表盘

文章目录 ELK生产环境配置filebeat 配置logstash 配置 kibana仪表盘配置配置nginx转发ES和kibanaELK设置账号和密码 ELK生产环境配置 ELK收集nginx日志有多种方案,一般比较常见的做法是在生产环境服务器搭建filebeat 收集nginx的文件日志并写入到队列(k…

uniapp - 倒计时组件-优化循环时间倒计时

使用定时器的规避方法 为了避免定时器误差导致倒计时计算错误,可以采用一些规避方法,比如将倒计时被中断时的剩余时间记录下来,重新开启定时器时再将这个剩余时间加到新的计算中。同时,为了避免定时器延迟,可以在每次执…

Python数据分析实战-Series转DataFrame并将index设为新的一列(附源码和实现效果)

实现功能 Series转DataFrame并将index设为新的一列 实现代码 import pandas as pd# 创创建series series pd.Series([1, 2, 3, 4, 5])# 创建一个DataFrame对象 data {column_name: series} df pd.DataFrame(data)# 重新设置索引,将原有的索引作为新的一列 df.r…

GIT实战篇,教你如何使用GIT可视化工具

系列文章目录 手把手教你安装Git,萌新迈向专业的必备一步 GIT命令只会抄却不理解?看完原理才能事半功倍! 快速上手GIT命令,现学也能登堂入室 GIT实战篇,教你如何使用GIT可视化工具 系列文章目录一、GIT有哪些常用工具…

2023高教社杯数学建模A题B题C题D题E题思路模型 国赛建模思路分享

文章目录 0 赛题思路1 竞赛信息2 竞赛时间3 建模常见问题类型3.1 分类问题3.2 优化问题3.3 预测问题3.4 评价问题 4 建模资料 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 竞赛信息 全国大学生数学建模…

如何截取视频中的一段视频?分享几种视频分割方法

当处理长视频时,视频分割可以使您更加高效。如果您只需要处理其中的一部分,而不是整个视频,那么分割视频可以使您更容易找到需要处理的部分。而且,分割视频还可以使您更容易在不同的项目之间重复使用视频片段。教大家几种简单的视…

基于Python的IOS自动化测试环境搭建

文章目录 一、测试架构介绍1.1 WebDriverAgent原理分析1.2 tidevice原理分析二、环境安装2.1 iOS 设备安装 WebDriverAgent2.2 安装iTunes2.3 安装tidevice2.4 安装facebook-wda自动化三、操作流程四、Weditor的安装和使用一、测试架构介绍 以下为测试架构原理图 手机端的WDA…

【vue2第十二章】ref和$refs获取dom元素 和 vue异步更新与$nextTick使用

ref和$refs获取dom元素 为什么会有 ref 和 $refs? 因为在vue页面中使用dom查找元素,不管你是不是在子组件里面查找,查找的都是整个页面的元素,如果你想查找单独组件里面的元素是不容易实现的,除非把每个组件的class写…

【Java转Go】Go中使用WebSocket实现聊天室(私聊+群聊)

目录 前言功能效果(一人分饰多角.jpg😎)用户上线、群聊私聊和留言下线 实现思路代码服务端 chat.go 完整代码客户端 html 完整代码 最后 前言 之前在Java中,用 springbootwebsocket 实现了一个聊天室:springbootwebso…

【广州华锐互动】利用AR远程指导系统进行机械故障排查,实现远程虚拟信息互动

随着工业自动化和智能化的不断发展,机械故障诊断已经成为了工业生产中的重要环节。为了提高故障诊断的准确性和效率,近年来,AR(增强现实)远程协助技术逐渐应用于机械故障诊断领域。本文将探讨AR远程协助技术在机械故障…

华为数通方向HCIP-DataCom H12-821题库(单选题:201-220)

第201题 BGP 协议用​​ beer default-route-advertise​​ 命令来给邻居发布缺省路由,那么以下关于本地 BGP 路由表变化的描述,正确的是哪一项? A、在本地 BGP 路由表中生成一条活跃的缺省路由并下发给路由表 B、在本地 BGP 路由表中生成一条不活跃的缺省路由&…

应用于伺服电机控制、 编码器仿真、 电动助力转向、发电机、 汽车运动检测与控制的旋变数字转换器MS5905P

MS5905P 是一款 12bit 分辨率的旋变数字转换器。 片上集成正弦波激励电路,正弦和余弦允许输入峰峰值 幅度为 2.3V 到 4.0V ,可编程激励频率为 10kHz 、 12kHz 、 15kHz 、 20kHz 。 转换器可并行或串行输出角度 和速度对应的数字量。 MS5905…