记录一次Spring事务失效导致的生产问题

一、背景介绍

公司做的是"聚合支付"业务,对接了微信、和包、数字人民币等等多家支付机构,我们提供统一的支付、退款、自动扣款签约、解约等能力给全国的省公司、机构、商户等。

同时,需要做对账功能,即支付机构将对账文件给到我们聚合支付,由我们进行如下三个定时任务:

1、入库:解析支付机构给的对账文件,解析每一笔订单,登记到数据库中;

2、对账:将支付机构的对账数据,与我们本地的对账数据,进行比对;

3、生成文件并下发:对完后,将对完账的我们的数据,按商户生成文件,并下发给商户的sftp机器。

我们的问题,就出在这个对账上面。

我们每天数据量都有500万-2000万 笔订单数据,导致我们入库时,解析文件,每2000条数据提交数据库一次,压力还算能够承受;但是由于集团业务调整,导致我们每天的订单量,激增20%-50%。

而月初月末,交易量会更大,所以一到月末月初那阵子,每天的数据量都在两千万以上

这时由于支付机构给的对账数据更多,导致入库时间长,数据库DBA也提醒我们数据库压力大,所以我们研讨决定,牺牲部分时间,提升稳定性;将每2000条提交一次,修改为1000条修改一次;

导致入库的时间翻倍。这是业务背景

二、出现的异常现象

月中14号,修改了这个参数(2000笔提交一次->1000笔提交一次),一切正常。

到了月末(我们的业务月初、月末是高峰期,数据量更多),问题就来了:

我们的三个定时任务中的对账任务,对账突然卡住了,一直都没有动弹,出现告警;

由于一时看不出问题,临时决定使用:

重启大法。

重启后,并手动删除分布式锁后,对账任务恢复正常并下发对账文件给商户。

三、排查问题

前期排查,数据库资源cpu达到100%,以为是数据库的性能限制,导致对账线程池出现异常,无法获取连接导致的;但是后续我们调整多线程对账的线程数(10线程->8线程),数据库CPU占用降到90%多,但是第二天依然出现卡死现象。

无奈,继续排查:

通过使用jmap和jstack命令

bash 复制代码
# 打印 20250305.heapdump.hprof
# 这个命令用于生成Java进程的堆内存转储文件(Heap Dump),便于后续分析内存问题(如内存泄漏)
jmap -dump:live,format=b,file=20250305.heapdump.hprof  2365589

# 打印 20250305.threaddump.txt
# 这个命令用于生成Java进程的线程转储(Thread Dump),帮助诊断线程阻塞、死锁、高CPU占用等问题。
jstack -l 2365589> 20250305.threaddump.txt

线程转储中看到,对数据库中有一个表的操作没有得到响应,就一直卡住

问了DBA的人员,确定,是这张表的行锁引起了问题。

于是,我们去看代码分析,经过我的仔细观察,与推翻各种不成立的假设后,终于被我定位到一个方法调用的错误:

如下是我们的大致调用图

可以看到解析并入库的主方法A,是带事务Transactional的

在下载文件完成后,调用方法B,再调用方法C,修改了一张状态表tb_status,修改状态为"下载成功"。

这里本意是方法C新开一个事务(REQUIRES_NEW),这样修改完后,单独提交;

后面的方法E,也是同样的打算。

可是写这段代码的时候没想到,spring事务注解是有可能会失效的;

在 Spring 中,当一个类中的方法 自调用(即类内部方法A调用方法B)时,如果方法B上标注了 @Transactional 事务注解,事务会失效。这是由 Spring 的 AOP 代理机制导致的。

在我们这个案例中,正是出现了这样的情况:方法A,调用B,再调用C时,以为C会新开一个事务去直接提交,但是没想到事务失效了,事务C的修改,并没有直接提交;

一直等到,文件解析入库(耗时很长)后,方法A的整个流程结束,才一起提交;

导致这一段时间内,定时任务"对账"想修改这一行数据,无法修改,导致卡住。

为什么之前没事呢?这代码跑了很久都没事啊,怎么回事呢?这就要结合我前面罗里吧嗦说的一大堆业务来看了,本来因为集团切业务,导致我们系统业务量暴增,又是月初月末,数据量更加大;光是一个微信商户给的对账文件就有2.5GB多(900多万,接近一千万订单);

导致解析文件并入库时间由10分钟左右,增加到45分钟以上。我们的"对账"定时任务,每15分钟执行一次,而入库的这45分钟,由于对tb_status的修改,一直没有提交,故而"对账"任务,意图修改tb_status的状态时,一直拿不到行锁修改数据,就卡住了。

四、解决方案

既然知道问题的原因,那么解决方案,我相信读者有很多种。

第一:方法A不带事务,让其他自己的事务,各自以非事务的方式,自动提交(此方式一定要根据自己业务逻辑的上下文来看,不能盲目各自提交)

第二:修改方法C上注解的位置,移动至方法B使其生效

经过评估,我们是选择了第一种。

另外,附上spring事务失效的几种场景:

1、如果被@Transactional修饰的方法,不是public的,那么事务会失效;

2、一个类中的方法 自调用(即类内部方法A调用方法B)时,如果方法B上标注了 @Transactional 事务注解,事务会失效。

3、如果一个方法是final的,那么加@Transactional事务也是无法生效的

4、被try-catch捕获了异常,没有往外抛出,那么spring事务会认为方法,没有发生异常,就不会回滚,事务失效

5、数据库表本身不支持事务,导致事务失效,例如InnoDB就不支持事务

相关推荐
A仔不会笑1 小时前
Redis面试常见问题——集群方案
数据库·redis·面试
邴越1 小时前
LeetCode 42.接雨水
数据库·redis·缓存
shenmu842 小时前
pt-archiver删除数据库的数据表/各种报错类型
数据库
Dann Hiroaki3 小时前
文献分享: ConstBERT固定数目向量编码文档
数据库·机器学习·自然语言处理·nlp
黑风风4 小时前
探索 Ubuntu 中的 Hostname 配置与管理
数据库·ubuntu·php
rkmhr_sef5 小时前
MyBatis-Plus 自定义 SQL 和复杂查询
数据库·sql·mybatis
Craaaayon5 小时前
Docker基础-自定义镜像与容器网络
java·运维·网络·数据库·后端·docker·容器
天地风雷水火山泽6 小时前
二百八十五、华为云PostgreSQL——建分区表并设置主键
数据库·postgresql·华为云
IT程序媛-桃子6 小时前
Oracle连接满了,无法登录数据库,如何分析连接来源?
数据库·oracle