Spring 源码系列(27): @Transactional 七大失效场景与源码归因

📌 阅读前提示 :事务阶段收官篇。前面两篇讲了事务怎么开、传播行为怎么决策。本篇是最「实战排错」的一篇------@Transactional 明明加了却不回滚/不生效,到底为什么?我按「源码归因」逐一拆 7 类高频失效场景,每一条都对应一个源码机制。


一、引子:事务不生效,先问三个问题

遇到「事务没回滚」,按这个顺序排查:

  1. 这个 Bean 被 AOP 代理了吗?(没代理,拦截器根本不介入)
  2. 方法被拦截器匹配到了吗?(传播行为、异常类型)
  3. 异常真的触发回滚了吗?(rollbackFor / 异常类型)

下面 7 个场景,全是这三条的具象化。


二、七大失效场景与源码归因

场景 1:同类自调用(最常见)

java 复制代码
@Service
public class OrderService {
    public void create() { this.pay(); }     // this 指向原始对象
    @Transactional
    public void pay() { /* ... */ }
}

归因 :第 14 篇原理------this 是原始对象,绕过了代理,TransactionInterceptor 没介入。

解决@EnableTransactionManagement(exposeProxy=true) + AopContext.currentProxy(),或拆到另一个 Bean。

场景 2:方法非 public

java 复制代码
@Transactional
private void update() { ... }   // ★ private 不生效

归因 :Spring AOP(JDK/CGLIB)默认只能拦截 public 方法;CGLIB 重写也只覆盖 public/protectedAbstractFallbackTransactionAttributeSource 对非 public 方法返回 null 事务属性------等于没事务。

场景 3:异常被 catch 吞掉

java 复制代码
@Transactional
public void update() {
    try { dao.update(); } catch (Exception e) { /* 吞掉 */ }
}

归因TransactionInterceptor 只有在异常抛出到拦截器 时(completeTransactionAfterThrowing)才回滚。catch 住没抛,拦截器认为「正常返回」→ 提交。

场景 4:抛了 checked Exception

java 复制代码
@Transactional
public void update() throws IOException { throw new IOException("x"); }

归因 :第 25 篇------默认 rollbackOn 只对 RuntimeException/Error 回滚(见 RuleBasedTransactionAttribute.rollbackOn)。IOException 是 checked,默认不回滚

解决@Transactional(rollbackFor = Exception.class)

场景 5:数据库引擎不支持事务

MyISAM 表没有事务;或数据源未配 PlatformTransactionManager

归因 :底层 Connection 非事务连接,commit/rollback 无效果。

场景 6:传播行为设为 NOT_SUPPORTED / NEVER

java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void update() { ... }   // 挂起事务,非事务执行

归因 :第 26 篇------NOT_SUPPORTED 挂起现有事务、以非事务运行;NEVER 直接抛异常。

场景 7:多线程调用

java 复制代码
@Transactional
public void outer() {
    new Thread(() -> inner()).start();   // 新线程没有事务上下文
}

归因 :事务信息存在 ThreadLocal(如 TransactionSynchronizationManager 的资源绑定)。新线程拿不到外层连接,等于无事务。


三、失效场景归因速查表

# 场景 源码归因
1 同类自调用 this 是原始对象,绕开代理(第14篇)
2 非 public 方法 事务属性解析对非 public 返回 null
3 异常被 catch 异常未传到拦截器,判定为正常返回
4 checked 异常 默认 rollbackOn 不含 checked
5 引擎/管理器缺失 底层非事务连接
6 传播行为不当 NOT_SUPPORTED/NEVER 非事务
7 多线程 事务绑 ThreadLocal,新线程无上下文

四、事务失效场景与源码机制图


五、常见误区

误区 正解
加了 @Transactional 就一定有事务 自调用/非public/异常被吞都可能使事务不生效
任何异常都回滚 仅 RuntimeException/Error 默认回滚
catch 后事务自动回滚 异常没抛到拦截器,反而提交
多线程共享外层事务 事务绑 ThreadLocal,跨线程失效
失败一定是配置问题 多数失效是「调用方式」问题(自调用最常见)

🧪 面试题自测

  1. 同类自调用为什么让 @Transactional 失效?怎么解决?
  2. 为什么 private 方法上的 @Transactional 不生效?
  3. catch 住异常后事务还会回滚吗?为什么?
  4. 抛 IOException 默认回滚吗?如何让它回滚?
  5. 多线程调用为什么会丢失事务上下文?
  6. 如何用源码视角排查「事务不回滚」?

🔧 Debug 小技巧

TransactionInterceptor.completeTransactionAfterThrowing 断点 + 在 RuleBasedTransactionAttribute.rollbackOn 断点,分别用「抛 RuntimeException」「抛 IOException」复现,对比回滚决策;再用自调用场景断 createTransactionIfNecessary,会发现根本没进入(因为代理没介入)。


下一篇预告

事务阶段完结。下一篇进入 进阶与实战(第 28 篇) :Spring 核心扩展点全景------BeanFactoryPostProcessor / BeanPostProcessor / Aware / ImportBeanDefinitionRegistrar / FactoryBean 各自在什么时机、能做什么,一张图收齐前面 30 篇的扩展点。


如果这篇对你有帮助,欢迎 点赞 · 收藏 · 关注 三连支持。

Spring 源码系列共 30 篇,由浅入深持续更新中。有疑问或想深挖的源码点,评论区告诉我,下篇见。

相关推荐
随遇而安zx1 小时前
【多线程】---线程池 ThreadPoolExecutor 原理与调优
java·线程池·多线程
抠脚小弟1 小时前
WebSocketServer 使用详解:从入门到实战
java·spring·websocketserver
妙码生花1 小时前
PHP 各框架下和 Go 的性能比较
前端·后端·go
随便做点啥1 小时前
32卡64G-910B4-16后端集群部署报告
java·大数据·运维
名字还没想好☜1 小时前
Python 字符编码实战:encode/decode、UnicodeDecodeError 与 open 的 encoding 坑
开发语言·后端·python·编程语言
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之字段验证与自定义验证注解实战
java·算法·spring
CodeStats1 小时前
【Java 类加载器】Java 类加载器完整体系深度拆解(上):从 JVM 启动到双亲委派模型
java·开发语言·jvm·classloader·双亲委派
带多刺的玫瑰1 小时前
Leecode#29刷题之两数相除
java·python·算法
大勇前进2 小时前
Java 线程创建的 4 种方式,优缺点对比,开发推荐写法
后端