本节摘要:声明式事务是工程里最有分量的切面:一个注解划出方法边界,边界内多条数据库操作要么全部生效、要么全部作废。本节讲注解背后的拦截器做了什么、回滚的精确规则、传播行为的语义,以及四类失效场景的机理——它们全部能归结为第 4.1 节的代理机制。
下单要写订单表、扣库存、记流水,三步必须同生共死:
@Service public class CheckoutService { @Transactional public void checkout(Long userId, Cart cart) { orderRepo.save(buildOrder(userId, cart)); // 步骤一 stockRepo.deduct(cart); // 步骤二 ledgerRepo.append(userId, cart); // 步骤三 } }
请求走到 checkout 时,事务拦截器先于方法体执行:取连接、关自动提交、开启事务;方法正常返回则提交,抛出匹配的异常则回滚,最后归还连接。方法内的三次写库共用同一个连接、同一个事务——边界划在方法上,原子性就有了物理载体。
默认规则只有一条:运行期异常且非受检的(含运行时异常与错误)触发回滚,受检异常不触发。这意味着:
@Transactional public void transfer(Long from, Long to, double amt) throws Exception { accountRepo.debit(from, amt); accountRepo.credit(to, amt); if (amt > 5000) throw new Exception("超额需人工审核"); // 不回滚! }
金额已划走、审核还没开始——数据处于中间态。修法有二,把需要回滚的异常类型显式声明:
@Transactional(rollbackFor = Exception.class)
或让自定义异常继承运行时异常。我倾向于统一在团队规范里规定 rollbackFor = Exception.class,把这条隐性规则显式化,减少每个人的记忆负担。
一个事务方法调用另一个事务方法,两个边界如何合并?由传播行为决定。常用的三种:
| 传播行为 | 被调用方行为 | 典型场景 |
|---|---|---|
| REQUIRED(默认) | 有则加入,无则新建 | 绝大多数业务方法 |
| REQUIRES_NEW | 挂起当前,独立开新 | 操作日志必须独立落库 |
| NESTED | 在当前事务内设保存点 | 部分失败可回退到保存点 |
操作日志是传播行为的经典用例:主业务回滚时,日志不该跟着消失,于是日志方法单独声明 REQUIRES_NEW——它的事务与主事务物理隔离,主事务回滚不影响它提交。

实践中"事务不回滚"的投诉,绝大多数落在这四类,根因都回到代理:
⚠️ 排查模板:事务失效先问三句——调用穿过代理了吗?方法可见吗?异常穿出边界了吗?三问过完,九成案例当场定位。
背景:促销活动期间运营反馈"改价失败但库存却被锁了一半"。涉及代码是调价方法:先锁库存、再调价、失败抛自定义异常。操作与排查:按三问模板走。第一问,调用是否穿过代理——查调用方,是控制器注入后调用的,排除自调用。第二问,方法可见性——public,排除。第三问,异常是否穿出边界——看到方法体里有一段捕获逻辑把自定义异常记日志后返回了空值。根因落定:异常被吞,拦截器认为方法正常结束,照常提交,锁库存的写入就此生效。
修复分两步。第一步把捕获改为转抛运行时异常并注明原因;第二步在团队规范里明确:事务方法体内禁止消化异常,确需捕获时必须重新抛出。结果:复测调价失败场景,库存锁定的写入随事务回滚消失。解读:这一事故没有任何冷门知识点,三条规则都在本节前文,组合起来才让排查变难——这也解释了为什么建议用"三问"模板把隐性推理固化成显性步骤。变式:若该方法将来被批处理任务自调用复用,第 4.1 节的自调用问题会再次潜伏,重构时把事务方法独立成 Bean 是一劳永逸的解法。
其一,长事务怎么治?边界划得太大(比如把远程调用、文件处理圈进事务方法)会让数据库连接被长时间占用,池子很快见底。治法是收窄边界:只把真正需要原子性的写库操作留在事务方法内,其余工作挪到边界之外,必要时用传播行为分段。判断标准很朴素——事务方法里不该出现任何"等待外部系统"的调用。其二,只读查询要不要事务?多数场景加只读标记有益:框架可据此跳过脏检查、优化快照,读密集的列表接口收益明显;但只读标记不是强约束,方法内照样能写库,别把它当安全机制。事务的两大纪律——边界最小、异常穿出——值得写进团队规范第一条。
最后一组检查题:默认规则下受检异常抛出后事务何去何从;REQUIRES_NEW 挂起当前事务的"挂起"具体指什么;三问排查模板的第一问问什么。能脱口而出,本章的事务主线就闭环了。
把本节与全书主线对齐一下:事务拦截器是请求旅程第七站上最重的一块包装,它拿连接、开事务、提交回滚的所有动作都发生在代理层,业务代码对此无感。正因无感,失效场景才显得诡异——理解了代理与边界的机理,诡异就退化为可推理。建议把三问排查模板打印贴在工位,直到它成为条件反射。