@Transactional 在哪些情况下会失效?默认回滚规则是什么?
简化版
@Transactional 依赖 Spring 事务代理,手动 new、同类自调用、不可代理的方法或未启用事务基础设施都会使拦截失效。默认对 RuntimeException 和 Error 回滚,对受检异常通常不回滚;异常若被方法内部吞掉,代理看不到失败,也不会按异常规则回滚。
详细版
高频失效原因包括:调用没有经过容器代理;this 调用同类方法;方法为 private/final 等无法被所用代理拦截;选错事务管理器;新线程脱离原事务上下文;异常被捕获且未重新抛出。具体方法可见性还受 JDK 接口代理和 CGLIB 类代理差异影响,业务事务方法使用 public 最稳妥。
默认回滚规则是未检查异常和 Error 回滚、受检异常提交。可用 rollbackFor、noRollbackFor 定制;规则决定的是“看到某种异常后怎么结束事务”,前提仍是代理已建立事务且异常传播到事务拦截器。
完整版教学
一、声明式事务的代理模型
外部调用代理方法时,事务拦截器先根据 TransactionAttribute 获取或加入事务,再调用目标方法;方法正常返回则尝试提交,抛出异常则根据回滚规则提交或回滚。
调用方 → 事务代理 → 开启/加入事务 → 目标方法 → 提交或回滚
因此排查事务问题的第一问不是“注解写了吗”,而是“这次调用有没有进入事务代理”。
二、同类自调用与手动 new
@Service
class OrderService {
public void create() {
this.save(); // 不经过代理
}
@Transactional
public void save() {}
}
如果外部调用的 create 本身没有事务,内部 save 不会凭注解新建事务。把 save 抽到另一个 Bean 并通过注入调用,是最清晰的修复方式。
手动 new OrderService() 同样绕开容器,既没有依赖注入,也没有事务代理。测试中若只 new 目标类,就不能据此验证声明式事务行为。
三、方法可代理性与代理类型
private 方法不能被子类覆盖,也不属于接口对外方法;final 方法无法被 CGLIB 子类覆盖;final 类也不能创建 CGLIB 子类代理。JDK 动态代理则围绕接口方法工作,调用方通过代理接口进入拦截链。
现代 Spring 的类代理对部分非 public 方法提供支持,但这不适用于 private/final 方法,也不能据此假设所有代理模式行为一致。将事务边界设计为可从 Bean 外部调用的 public 方法,最容易保持可读和可移植。
四、异常为什么没有触发回滚
@Transactional
public void create() {
try {
repository.save();
} catch (RuntimeException ex) {
log.warn("保存失败", ex); // 异常被吞掉,方法正常返回
}
}
事务拦截器只看到正常返回,通常会尝试提交。需要整体失败时应让异常继续抛出,或在确有必要时显式将当前事务标记为 rollback-only;后者会让业务代码与 Spring 事务 API 耦合。
受检异常默认不触发回滚,可以声明:
@Transactional(rollbackFor = IOException.class)
public void importData() throws IOException {}
也可用 noRollbackFor 表达某类业务异常允许提交。规则应围绕业务原子性设计,而不是机械地给所有方法写 rollbackFor = Exception.class。
五、线程与事务管理器边界
常见本地事务上下文绑定在当前线程。方法中新建线程或切换到 @Async 执行后,不会自动携带调用线程的事务;异步方法若自己标注事务,建立的是异步线程上的另一个事务边界。
应用同时配置多个数据源或事务管理器时,需要确保 @Transactional 选择了正确的 manager。一个本地事务管理器不会自动让另一个数据源或远程服务参与同一原子事务。
六、配置与提交阶段问题
只有注册了事务管理器并启用相应事务管理基础设施,注解才会被处理。Spring Boot 常通过自动配置完成这些步骤,但纯 Spring 配置不能默认假设已经开启。
数据库不支持事务、表引擎不具备预期事务能力,或者外部调用根本不在同一资源事务中,也会表现为“没有回滚”。此外,方法正常结束不等于提交必然成功,约束冲突或连接故障也可能在 flush/commit 阶段才暴露。
七、用判定矩阵排查“失效”
假设 createOrder() 更新订单和库存共 2 张表,库存更新抛出 RuntimeException。只有调用经过代理、选择了覆盖这两次数据库操作的事务管理器,并且异常没有被吞掉时,默认规则才会把两项更新一起回滚;缺少任一条件,现象都可能被误称为“注解失效”。
| 检查层 | 要确认的问题 | 失败时的典型现象 |
|---|---|---|
| 对象 | 是否为容器管理的代理 | 手动 new,根本未开启声明式事务 |
| 调用 | 是否从代理入口进入 | 同类自调用,内层注解不建立新边界 |
| 方法 | 当前代理能否拦截 | private/final 等方法不受相应代理通知 |
| 资源 | manager 是否覆盖目标资源 | 另一个数据源或远程调用不跟随回滚 |
| 异常 | 拦截器是否看到了异常 | catch 后正常返回,事务尝试提交 |
| 规则 | 异常是否匹配 rollback 规则 | 受检异常默认通常提交 |
shouldRollback =
enteredProxy
&& transactionStarted
&& resourceParticipated
&& exceptionEscaped
&& rollbackRuleMatched
这个表达式不是 Spring 源码,而是排障清单。比如 IOException 已传播到代理,但没有配置 rollbackFor,前四项为真、最后一项为假,默认仍不会因该受检异常回滚。
易错点:回滚规则只决定“已建立的事务遇到异常怎么办”,不能让绕过代理的调用凭空获得事务。
八、常见误区与追问
- 误区:方法上写了 @Transactional 就一定开启事务。 注解需要事务基础设施识别,且这次调用必须经过相应 Spring 代理。
- 误区:catch 住异常后记录日志,事务仍会自动回滚。 若方法正常返回且没有显式标记 rollback-only,拦截器通常会尝试提交。
- 误区:所有 Exception 默认都会触发回滚。 默认规则针对 RuntimeException 和 Error,受检异常通常不回滚,除非配置匹配规则。
- 追问:同类自调用怎样修复最清晰? 把事务边界移到可由外部 Bean 调用的协作者中,让调用自然穿过代理;自注入或 AopContext 仅适合谨慎使用的替代方案。
- 追问:@Async 中为什么不继承调用方事务? 常见本地事务上下文绑定线程,切换到线程池后是另一个执行上下文;异步方法可建立自己的独立事务。
- 追问:异常在 commit 阶段才出现怎么办? 目标方法正常返回不等于提交成功,flush、约束检查或连接故障可能在拦截器提交时暴露,调用方仍需处理提交异常。
- 追问:rollbackFor = Exception.class 是否总是最佳选择? 不是,应按业务原子性决定哪些异常必须回滚;一刀切会把可恢复或允许提交的业务结果也扩大成回滚。
九、加强记忆
事务是否生效看三关:调用先经过代理,代理再建立正确的事务,异常最后要按规则回到拦截器。默认运行时异常和 Error 回滚、受检异常不回滚;自调用、手动 new、吞异常、跨线程和错误的事务管理器是排查重点。