@transactional默认不回滚受检异常,仅对runtimeexception和error回滚;内部调用、非public方法、手动new对象、异步方法等导致代理失效;传播行为影响回滚范围;try-catch吞异常会阻止回滚。

Spring中@Transactional不回滚的常见原因
默认情况下,@Transactional只对RuntimeException及其子类、Error触发回滚,对普通Exception(比如IOException、SQLException)不会回滚——这是最常踩的坑。
根本原因是Spring事务的默认回滚策略是rollbackFor = RuntimeException.class,它不关心受检异常(checked exception)。
- 如果业务逻辑抛出
Exception但没显式声明rollbackFor,数据库操作照样提交 - 自定义异常继承
RuntimeException可绕过配置,但会模糊异常语义,不推荐 - 使用
throws Exception声明方法签名,不代表Spring会捕获并回滚——事务代理只看实际抛出的异常类型
正确做法是在注解里明确指定:
@Transactional(rollbackFor = Exception.class)或更精确地写
rollbackFor = {IOException.class, SQLException.class}。
@Transactional失效的典型场景
事务失效不是代码写错了,而是Spring AOP代理机制没生效。最常见的是:方法调用发生在同一个类内部。
立即学习“Java免费学习笔记(深入)”;
- 在ServiceA中,
methodA()加了@Transactional,但它直接调用本类的methodB(),而methodB()也操作数据库——此时methodB()不在代理链中,事务不延伸过去 - 方法不是public:Spring CGLIB代理要求目标方法必须是public,private/protected/package-private方法加注解无效
- 没有走Spring容器管理的对象:比如用
new ServiceA()手动实例化,事务注解完全被忽略 - 异步方法(
@Async)默认运行在新线程,脱离原事务上下文,即使加@Transactional也无意义
验证是否生效,可以在方法开头打日志+查TransactionSynchronizationManager.isActualTransactionActive()返回值。
事务传播行为影响回滚边界
@Transactional的propagation参数决定多个事务方法嵌套时怎么协同,它直接影响“谁负责回滚”和“回滚范围”。
-
PROPAGATION_REQUIRED(默认):有事务就加入,没事务就新建;外层方法回滚,内层也会一起回滚 -
PROPAGATION_REQUIRES_NEW:总是挂起当前事务,新建一个独立事务;外层回滚不影响内层已提交的结果 -
PROPAGATION_NESTED:外层事务设保存点,内层异常可仅回滚到该点;但HikariCP + MySQL需开启allowMultiQueries=true且驱动支持
例如:支付扣款(需强一致性)调用发券(允许最终一致),应把发券方法设为REQUIRES_NEW,避免扣款失败时把已发的券也撤回。
try-catch吞掉异常导致事务不回滚
这是逻辑层面最隐蔽的问题:异常被catch住,没再抛出,Spring根本不知道出错了。
- 写了
try { ... } catch (Exception e) { log.error(...); },事务照常提交 - 即使catch后
throw new RuntimeException(e),也要注意异常类型是否匹配rollbackFor设置 - 在catch里手动调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()能强制回滚,但属于补救手段,掩盖了设计问题
真正健壮的做法是:不捕获业务异常,让异常穿透到事务切面;或捕获后重新抛出符合回滚策略的异常类型。
事务不是万能兜底,它解决的是单次请求内的数据一致性。跨服务、跨库、异步操作的回滚得靠Saga、TCC或消息补偿——这点容易被忽略,但恰恰是复杂系统里最常翻车的地方。










