一段被 try-catch 包裹后的段被的代点让丢工代码在产线稳定运行了 200 天后忽然发生了异常,而这个异常竟然导致了产线事务回滚。包裹 图片来自 Pexels 这期间究竟发生了什么?码差日常在项目过程中该如何避免事务异常?就在这个时候,老板拿着《XX 公司关于三十岁员工优化通知》走了过来...... 01 产线部分数据丢失了,段被的代点让丢工因为一个蹊跷的包裹事务回滚。而造成事务回滚的码差,竟然是段被的代点让丢工一段被 try-cath 包裹后的代码,一段已经在产线稳定运行了 200 天的包裹代码,稳定到我们已经把它遗忘了。码差 谁也没想到的段被的代点让丢工是,它竟然以这样一种方式重新回到了我们的包裹视野,宣告着它的码差存在! 小九九是一个永远 19 岁的程序员,和所有程序员一样地阳光、段被的代点让丢工帅气(这句话不管你信不信,包裹反正我自己也不信。码差为了能够开始今天的文章,高防服务器就这么瞎编吧,总比以“一个没有头发的程序员”开头的好)。 当他告诉我一段 try-catch 的代码造成产线事务回滚后,我温柔、耐心地对他说:“滚一边去,没看我正忙着吗?”,然后他给我甩出了一段代码,用猥琐又真诚的眼睛告诉我,他说的是真的。 02 我们来看一下这段导致了产线事务回滚的代码,类似于下面这样的: methodA 方法需要事务控制,methodB 方法不管遇到什么异常都不能影响 A 事务,所以加了 try-catch。 可能有的人和我的第一反应一样,是不是最后的 userOtherProcess 方法执行异常造成了 methodA 的服务器托管事务回滚? 小九九告诉我真的是因为 methodB,这段代码当初经过严格的测试,而且已经 200 天没人碰过了。 也可能已经有人猜出了问题的原因了,这里先卖个关子,因为这件事情里,最重要的是这个坑是如何一步步产生的。 为了更形象地描述这个事情我画一个图,红色背景表示该方法是有事务控制的,白色背景表示该方法没有事务: 一开始的时候,正如大家所看到的代码,methodA 方法有事务,methodB 无事务且被 try-catch 包裹了,运行得很完美。 过了一段时间后来到了阶段二,因为一些需求变更新增了 methodC,该业务也依赖了 methodB,依然很完美地上线了。 过了一段时间来到了阶段 3,依赖 methodC 相关业务再次发生了变更,需要在 methodB 里增加一些逻辑且需要事务控制。亿华云计算 经过评估确实对 methodA 没有影响,于是经过充分测试后再次完美地上线了,然而隐藏的炸弹就在这个时候埋下了。 小伙伴们这个时候应该已经猜到原因了,是的,你猜的没错。某一天 methodA 调用 methodB 时 methodB 发生了异常,由于是继承性事务,虽然 methodB 发生了异常被 try-catch 了,依然造成了 methodA 事务回滚。 还没有理解的小伙伴,可以看下面这张图: 我们可以把事务控制机制理解为上图这样一个红色的长长的房间,这个房间是有人看守的,他负责事务的开始、提交,还有一项重要的任务就是监控异常。 一旦发现 RuntimeException 异常直接回滚整个事务,我们给他一个 title,称之为“监事”吧。 再来看阶段三和一开始的代码,方法的开头有一个 @Transactional 注解,于是他打开了这个红色房间的门,把 methodA 放了进去。 接着 methodB 过来了,也开启了事务--继承性事务,于是监事把 methodB 也安排到了这个房间。 methodB 虽然发生了异常且被 try-catch 包裹,但逃不过监事的火眼金睛,于是他按下了事务回滚的按钮。 这样理解了之后,我们再来简单看一下源码: 根据异常提示,可以看到错误发生在 AbstractPlatformTransactionManager 的 873 行 processRollback 方法。 通过 Find Usages 找到调用方 commit 方法,显然这是一段事务提交的逻辑。 shouldCommitOnGlobalRollbackOnly:默认实现是 false,意思是如果发现事务被标记全局回滚并且该标记不需要提交事务的话,那么则进行回滚。 defStatus.isGlobalRollbackOnly():判断是否是读取 DefaultTransactionStatus 中 transaction 对象的 ConnectionHolder 的 rollbackOnly 标志位。 继续往上追溯,来到 TransactionAspectSupport.invokeWithinTransaction 方法: 整个执行过程参见注释说明,其它源码就不罗列了。Spring 捕获异常后,正如我们所猜测的,事务将会被设置全局 rollback。 而最外层的事务方法执行 commit 操作,这时由于事务状态为 rollback,Spring 认为不应该 commit 提交事务,而应该回滚事务,所以抛出 rollback-only 异常。 03 还有一个比较典型的事务问题就是:在同一个类中,mehtodA 没有事务,mehtodB 开启了(声明式)事务。 此时 mehtodA 调用 mehtodB 时事务是不生效的: 如上面这张图所示,我们还是把 AOP 想像成一个长方形的房间,由于 mehtodA 没有事务,这个房间已经被标志为没有事务无人值守了,mehtodB 虽然标记了事务,但很显然是不生效的。 接下来我们重新回顾一下事务的几种配置: 这方面的文章很多,这里就不做描述了。 04 事务问题本身是比较难通过测试发现的,我们再来聊一聊项目过程中如何防止事务问题的发生。 比如笔者之前曾负责过支付及资金处理相关系统,产品的单笔交易额比较大,每笔至少 1 万+,正常 10 万+,很多时候一笔支付就是 300 万,所以容不得出现一笔资金差错。好在我们资金交易从 0 做到了 3000 亿,依然资金 0 差错。 针对可能的事务问题,我们采取的措施有: 笔者在之前一家公司还有一种做法就是通过开发规范约束:所有事务的方法全部以 tx 开头。 比如 methodB 方法需要开启事务,则新增一个 txMethodB 方法,在该方法中调用 methodB。通过这种方式完全可以避免上面问题的发生,但很显然这种方式相当地“丑陋”。 05 正和小九九聊着事务问题,老板手里拿着几张 A4 纸走了过来。 作为公司唯一的 30 岁程序员,我提高了声音对小九九说:你有没有发现 @Transactional 中还有一个配置项 readOnly,如果需要使用这个参数,必须启动一个事务。 但如果是读取数据,根本就不需要事务啊?为什么会有这么一个自相矛盾的配置项呢?小九九一脸茫然地摇了摇头。 老板冲我点了点头,转身回到了办公室,坐下思考了一会,然后把手里的 A4 纸《XX 公司关于三十岁员工优化通知》放到了抽屉一叠资料的最下面,接着又抽出来放到了资料的中间。 看来我的程序生涯,又可以持续一段时间了! 作者:剑圣 编辑:陶家龙 出处:转载自微信公众号码大叔(ID:ma_dashu)