SpringBoot中事务失效的场景

 在 Spring 的物理世界里,@Transactional 的底层完全依赖 Spring AOP 动态代理ThreadLocal 本地线程变量。一旦破坏了这两个底座的因果链,事务就会瞬间物理塌方。

[TOC]

生产环境中最硬核、最高频的 8 大事务失效惨案,顺着底层的物理因果,我们将其归纳为四大派系:

一、 代理机制血案(流量压根没走代理)

这是最经典、也是你现在最熟悉的盲区。如果流量没有撞击到外层的 Proxy 对象,AOP 拦截器就无法拔刀。

1. 同类内部调用(Self-Invocation)

  • 硬核因果: 我们在上几轮复盘中刚狙击过它。本类中的 methodA() 直接通过 this.methodB() 调用了加有 @TransactionalmethodB()

  • 底层真相: this 指针死死指向肉身对象而非代理对象,直接抄近道绕过了 TransactionInterceptor 拦截器。

  • 一阵见血的例子:

     public void registerUser() {
         // 🚨 丢掉主权:内部调用,事务直接装聋作哑
         this.insertData();
     }
     @Transactional
     public void insertData() { ... }

2. 方法修饰符不是 public

  • 硬核因果: 如果你把 @Transactional 顺手写在了 privateprotected 或者 default 权限的方法上,事务会直接失效。

  • 底层真相: Spring 事务拦截器中的 AbstractFallbackTransactionAttributeSource 在解析方法注解时,源码里第一行就刚性限制了:如果方法不是 public,直接返回 null(不予解析事务属性)。CGLIB 动态代理也无法重写 private 方法。

二、 异常斩断血案(逆天改命破坏了 Catch 闭环)

我们在 @Around 章节里拆解过,事务回滚死死依赖于 try-catch 对异常的捕获。

3. 开发者在内部手动吞掉异常(把 Catch 写死了)

  • 硬核因果: 核心业务抛出了异常,但你在方法内部写了个大括号,把异常给 try-catch 拦截了,并且只打印了日志,没有往外抛出

  • 底层真相: 外层的 TransactionInterceptor 包装网在等待业务抛出异常来触发 conn.rollback()。你在里面悄悄把异常吞了,代理对象以为天下太平,老老实实执行了 conn.commit(),导致脏数据物理落盘。

  • 一阵见血的例子:

     @Transactional
     public void updateStock() {
         try {
             productMapper.reduce(); // 假设这里抛出了数据库异常
        } catch (Exception e) {
             log.error("报错啦!", e); // 🚨 物理毁灭:吞掉异常,外层代理以为成功了,照常提交!
        }
     }

4. 抛出了非运行时异常(Checked Exception)

  • 硬核因果: 方法内部往外抛出了 IOExceptionSQLException 或者自定义的受检异常(Checked Exception),事务竟然没有回滚!

  • 底层真相: Spring 事务默认的铁血回滚策略是:只有发生 RuntimeException(运行时异常)和 Error 时才会触发回滚。这承袭了 EJB 时代的古老历史包袱。

  • 完美救场: 必须充满存在感地手动修改回滚策略:@Transactional(rollbackFor = Exception.class)

三、 多维时空撕裂(ThreadLocal 绑定断裂)

5. 异步多线程调用(Async Thread)

  • 硬核因果: 在主线程的 @Transactional 方法内部,开启了一个子线程去执行数据库写操作,或者调用了外挂有 @Async 的异步方法。子线程里的 SQL 一旦炸了,主线程的事务绝对无法跟着回滚。

  • 底层真相: Spring 事务管理器的底层武器是 TransactionSynchronizationManager,它利用 ThreadLocal 将数据库连接 Connection 物理绑定在当前线程上。一旦跨入多线程,子线程根本拿不到主线程的 Connection 变量,它会去连接池里捞取一条全新的连接,两者的时空和因果完全断绝!

四、 物理底座塌方(基础设施不具备因果条件)

6. 所在类没有被 Spring IoC 容器管理

  • 硬核因果: 你在方法上把 @Transactional 写得天衣无缝,但忘了在类名上加 @Service@Component

  • 底层真相: 容器在启动时,图纸库(BeanDefinitionMap)里根本没有这个 Bean,动态代理和 AOP 织入连发动的物理载体都没有。

7. 数据库引擎本身不支持事务

  • 硬核因果: 代码写得极其严谨,甚至加了 rollbackFor,但发生异常数据依然没回滚。

  • 底层真相: 物理底座骗了你。如果你手贱把 MySQL 的表引擎设成了 MyISAM(或者接手了远古时期的老项目),它在底层磁盘存储上就不支持事务(没有 redolog/undolog 的物理支持)。Spring 哪怕在内存里喊破喉咙执行 Connection.rollback(),MySQL 底层也无法收卷时空。

8. 错误的传播行为(Propagation)

  • 硬核因果: 配置了错误的事务传播属性,导致期望的事务因果链断开。

  • 底层真相: 如果你把属性配成了 @Transactional(propagation = Propagation.NOT_SUPPORTED),意思是“老子不需要事务,如果当前有事务,挂起它,我用无事务的物理裸奔去跑”。这时候抛异常自然不会回滚。

⚖️ 工业级上线前的“御敌检查清单”

以后写完事务代码,在脑海中死死过一遍这四个物理因果问答,保你生产无忧:

1. 流量进来了吗? ➔ 外部调用的吗?方法是 public 吗?类上有 @Service 吗?

2. 异常交出去了吗? ➔ 我有没有手贱写 try-catch 吞异常?我是不是忘了写 rollbackFor = Exception.class

3. 线程跨越了吗? ➔ 我里面有没有拉起 new Thread@Async 导致 ThreadLocal 变量死锁断裂?

4. 磁盘答应了吗? ➔ MySQL 对应表的引擎是 InnoDB 吗?

只要牢牢控住 “代理通行证”“ThreadLocal 线程栈帧” 这两个 AOP 宇宙的终极坐标,声明式事务在你的架构设计里就绝对不敢出轨!

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个