
很多团队都有一个习惯:所有
@Transactional后面都顺手加上rollbackFor = Exception.class。 也有人反对:不要无脑加,Spring 默认只回滚运行时异常是有原因的。 那到底该不该加?
线上有一个订单确认接口,逻辑很简单:
CONFIRMED这三个动作必须在同一个事务里。
代码大概长这样:

按理说,库存扣减失败,整个事务应该回滚。
但数据库里订单状态已经提交了。
第一反应通常是:
@Transactional又失效了?
其实不是注解没生效,而是异常类型不对。
看一下 stockService.deduct(orderId) 的实现:

注意这里抛的是:

这是一个受检异常,也就是 checked exception。
而 Spring 事务默认只会回滚这两类异常:

默认不会因为 checked exception 回滚。
所以当 deduct() 抛出 Exception 时,外层方法确实中断了,调用方也确实看到了异常。
但是 Spring 事务拦截器判断后发现:
这是 checked exception,不在默认回滚范围内。
于是事务提交了。
订单状态就这样被提交到了数据库。
下面这段代码就能复现这个问题。

调用:

你会发现:

这就是最容易误判的地方。
很多人以为:
只要方法抛异常,事务就一定回滚。
实际上不是。
更准确地说:
只有抛出的异常命中了事务回滚规则,Spring 才会回滚。
这不是 Spring 的 bug。
Spring 默认遵循的是 EJB 时代留下来的事务语义:

比如这些异常从语义上看,可能只是业务流程的一部分:

Spring 没法判断你的 checked exception 到底意味着什么。
它不知道“库存不足”是应该回滚,还是应该允许前面的操作提交。
所以它选择了一个保守默认值:
checked exception 不回滚,除非你明确告诉 Spring 要回滚。
最直接的修复方式:

加上后,只要方法抛出 Exception 或它的子类,事务都会回滚。
也就是说:
throw new Exception("库存不足");现在会触发回滚。
很多团队会把它当成默认写法:
@Transactional(rollbackFor = Exception.class)这个习惯不是没有道理。
因为在真实业务代码里,很多人会随手写:
throws Exception或者在底层工具、RPC、文件、消息、第三方接口里抛出 checked exception。
如果事务方法里没加 rollbackFor,就很容易出现:
异常抛出去了,但数据已经提交了。
另一种方式是把业务异常设计成运行时异常。

然后业务里这样写:

因为 BizException 继承了 RuntimeException,所以即使不写 rollbackFor,事务也会默认回滚。
这也是很多项目里的统一异常设计:

这样事务回滚规则会简单很多。
我的建议是:
业务系统里,绝大多数写操作事务,都建议加
rollbackFor = Exception.class。
尤其是这些场景:
场景 | 建议 |
|---|---|
订单、支付、库存、账户余额 | 必须加 |
多表写入,要求强一致 | 必须加 |
调用底层组件可能抛 checked exception | 建议加 |
历史代码里大量 throws Exception | 建议加 |
只读查询事务 | 意义不大 |
明确希望 checked exception 不回滚 | 不要加,单独声明规则 |
为什么我倾向于加?
因为线上事故里,“该回滚却没回滚”的代价,通常比“多回滚了一次”的代价更高。
订单状态、账户金额、库存数量这种数据,一旦错了,后面补偿很麻烦。
所以我更愿意让事务默认更谨慎一点。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。