记录一次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就不支持事务

相关推荐
GalaxyPokemon16 分钟前
MySQL基础 [六] - 内置函数+复合查询+表的内连和外连
linux·运维·数据库·mysql·ubuntu
Linux运维老纪1 小时前
Linux 命令清单(Linux Command List)
linux·运维·服务器·数据库·mysql·云计算·运维开发
可观测性用观测云1 小时前
阿里巴巴 Druid 可观测性最佳实践
数据库
代码吐槽菌2 小时前
基于微信小程序的智慧乡村旅游服务平台【附源码】
java·开发语言·数据库·后端·微信小程序·小程序·毕业设计
喝醉酒的小白2 小时前
数据库如何确定或计算 LSN(日志序列号)
数据库
dengjiayue2 小时前
使用 redis 实现消息队列
数据库·redis·缓存
小羊学伽瓦2 小时前
【Redis】——最佳实践
数据库·redis·缓存
biubiubiu07063 小时前
Mysql
数据库·mysql
凌辰揽月3 小时前
眨眼睛查看密码工具类
java·开发语言·数据库
Arbori_262153 小时前
oracle 索引失效
数据库·oracle