SpringBoot中事务失效的场景
@Transactional 的底层完全依赖 Spring AOP 动态代理和 ThreadLocal 本地线程变量。一旦破坏了这两个底座的因果链,事务就会瞬间物理塌方。
[TOC]
生产环境中最硬核、最高频的 8 大事务失效惨案,顺着底层的物理因果,我们将其归纳为四大派系:
一、 代理机制血案(流量压根没走代理)
这是最经典、也是你现在最熟悉的盲区。如果流量没有撞击到外层的 Proxy 对象,AOP 拦截器就无法拔刀。
1. 同类内部调用(Self-Invocation)
硬核因果: 我们在上几轮复盘中刚狙击过它。本类中的
methodA()直接通过this.methodB()调用了加有@Transactional的methodB()。底层真相:
this指针死死指向肉身对象而非代理对象,直接抄近道绕过了TransactionInterceptor拦截器。一阵见血的例子:
public void registerUser() {
// 🚨 丢掉主权:内部调用,事务直接装聋作哑
this.insertData();
}
public void insertData() { ... }
2. 方法修饰符不是 public
硬核因果: 如果你把
@Transactional顺手写在了private、protected或者default权限的方法上,事务会直接失效。底层真相: Spring 事务拦截器中的
AbstractFallbackTransactionAttributeSource在解析方法注解时,源码里第一行就刚性限制了:如果方法不是public,直接返回 null(不予解析事务属性)。CGLIB 动态代理也无法重写private方法。
二、 异常斩断血案(逆天改命破坏了 Catch 闭环)
我们在 @Around 章节里拆解过,事务回滚死死依赖于 try-catch 对异常的捕获。
3. 开发者在内部手动吞掉异常(把 Catch 写死了)
硬核因果: 核心业务抛出了异常,但你在方法内部写了个大括号,把异常给
try-catch拦截了,并且只打印了日志,没有往外抛出。底层真相: 外层的
TransactionInterceptor包装网在等待业务抛出异常来触发conn.rollback()。你在里面悄悄把异常吞了,代理对象以为天下太平,老老实实执行了conn.commit(),导致脏数据物理落盘。一阵见血的例子:
public void updateStock() {
try {
productMapper.reduce(); // 假设这里抛出了数据库异常
} catch (Exception e) {
log.error("报错啦!", e); // 🚨 物理毁灭:吞掉异常,外层代理以为成功了,照常提交!
}
}
4. 抛出了非运行时异常(Checked Exception)
硬核因果: 方法内部往外抛出了
IOException、SQLException或者自定义的受检异常(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 线程栈帧”
评论
发表评论