一、背景介绍
公司做的是“聚合支付”业务,对接了微信、和包、数字人民币等等多家支付机构,我们提供统一的支付、退款、自动扣款签约、解约等能力给全国的省公司、机构、商户等。
同时,需要做对账功能,即支付机构将对账文件给到我们聚合支付,由我们进行如下三个定时任务:
1、入库:解析支付机构给的对账文件,解析每一笔订单,登记到数据库中;
2、对账:将支付机构的对账数据,与我们本地的对账数据,进行比对;
3、生成文件并下发:对完后,将对完账的我们的数据,按商户生成文件,并下发给商户的sftp机器。
我们的问题,就出在这个对账上面。
我们每天数据量都有500万-2000万笔订单数据,导致我们入库时,解析文件,每2000条数据提交数据库一次,压力还算能够承受;但是由于集团业务调整,导致我们每天的订单量,激增20%-50%。
而月初月末,交易量会更大,所以一到月末月初那阵子,每天的数据量都在两千万以上
这时由于支付机构给的对账数据更多,导致入库时间长,数据库DBA也提醒我们数据库压力大,所以我们研讨决定,牺牲部分时间,提升稳定性;将每2000条提交一次,修改为1000条修改一次;
导致入库的时间翻倍。这是业务背景
二、出现的异常现象
月中14号,修改了这个参数(2000笔提交一次->1000笔提交一次),一切正常。
到了月末(我们的业务月初、月末是高峰期,数据量更多),问题就来了:
我们的三个定时任务中的对账任务,对账突然卡住了,一直都没有动弹,出现告警;
由于一时看不出问题,临时决定使用:
重启大法。
重启后,并手动删除分布式锁后,对账任务恢复正常并下发对账文件给商户。
三、排查问题
前期排查,数据库资源cpu达到100%,以为是数据库的性能限制,导致对账线程池出现异常,无法获取连接导致的;但是后续我们调整多线程对账的线程数(10线程->8线程),数据库CPU占用降到90%多,但是第二天依然出现卡死现象。
无奈,继续排查:
通过使用jmap和jstack命令
# 打印 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就不支持事务