Spring 事务在什么情况下会失效?
从事务代理、同类调用、方法可代理性、异常回滚规则、异步线程、传播行为、多数据源和数据库能力等角度,系统讲解 Spring 声明式事务不生效的常见原因。
Spring 事务在什么情况下会失效?
面试回答
Spring 声明式事务主要通过 AOP 代理实现:外部调用先进入代理对象,TransactionInterceptor 根据 @Transactional 配置开启事务,再调用目标方法,最后根据方法是否正常返回以及异常是否命中回滚规则决定提交或回滚。
因此,常见的事务失效场景可以分成两类。第一类是调用没有经过事务代理,例如对象不是 Spring Bean、同一个类中使用 this 调用、在 @PostConstruct 阶段调用事务方法,或者方法是代理无法增强的 private、final、static 方法。第二类是事务已经创建,但行为与预期不同,例如默认不对受检异常回滚、异常被业务代码捕获后没有继续抛出、方法切换到新线程执行、事务传播行为选择不当、多数据源使用了错误的事务管理器,或者底层数据库操作本身不支持事务。
排查时先确认当前对象是不是代理对象、调用是否从代理外部进入,再确认事务是否真正开启,最后检查异常类型、传播行为、线程边界、事务管理器和数据库能力。
一句话总结:
Spring 事务要生效,必须让调用经过正确的事务代理,并且异常、线程、传播行为、事务管理器和底层存储都处在同一个有效事务边界内。
一张图看懂
详细讲解
一、先理解 Spring 事务为什么能够生效
在默认的代理模式下,Spring 会为符合条件的 Bean 创建事务代理。外部代码调用事务方法时,实际链路是:
调用方
↓
Spring 事务代理
↓
TransactionInterceptor
├─ 读取 @Transactional 属性
├─ 选择 TransactionManager
├─ 开启或加入事务
└─ 调用目标方法
↓
正常返回:提交
命中回滚规则的异常:回滚
所以,@Transactional 本身只是事务元数据。只有 Spring 的事务基础设施识别到它,并且方法调用经过代理,事务拦截器才有机会工作。
本文所说的“事务失效”包含两种不同现象:
- 事务根本没有创建,例如同类自调用绕过代理;
- 事务已经创建,但没有按照预期回滚或隔离,例如异常被吞掉、切换线程或使用了独立事务。
排查时必须先区分是哪一种。
二、对象没有交给 Spring 管理
下面的对象是手动创建的:
OrderService orderService = new OrderService();
orderService.createOrder();
即使 createOrder() 上有 @Transactional,这个对象也不是 Spring 容器创建的 Bean,没有对应的事务代理,因此注解不会生效。
public class OrderService {
@Transactional
public void createOrder() {
// 数据库操作
}
}
应当让类成为 Spring Bean,并通过容器注入后调用:
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 数据库操作
}
}
同时还要确认应用已经启用事务基础设施。Spring Boot 通常会根据依赖和事务管理器自动配置;非 Boot 项目一般需要显式配置事务管理器并启用注解事务管理。
三、同一个类中自调用
这是最常见的失效场景之一:
@Service
public class OrderService {
public void create() {
saveOrder();
}
@Transactional
public void saveOrder() {
// 数据库操作
}
}
外部调用 create() 时,代理把调用转交给真实的 OrderService。进入真实对象后,saveOrder() 实际相当于:
this.saveOrder();
这次调用直接发生在目标对象内部,没有再次经过事务代理,所以 saveOrder() 上的事务拦截器不会执行。
外部调用 → 代理 → 真实对象.create()
↓
this.saveOrder()
↓
绕过事务代理
优先采用的解决方式是拆分职责,让调用跨 Bean 进行:
@Service
public class OrderService {
private final OrderWriter orderWriter;
public OrderService(OrderWriter orderWriter) {
this.orderWriter = orderWriter;
}
public void create() {
orderWriter.saveOrder();
}
}
@Service
public class OrderWriter {
@Transactional
public void saveOrder() {
// 数据库操作
}
}
也可以使用 TransactionTemplate 显式划分事务边界。通过注入自身代理或 AopContext.currentProxy() 调用虽然能够绕过问题,但会增加类与 Spring AOP 的耦合,通常不是优先方案。
四、方法无法被代理增强
在常见的 Spring AOP 代理模式下,以下方法不能作为可靠的事务入口:
1. private 方法
@Transactional
private void saveOrder() {
}
私有方法不能被子类覆盖,也不会作为接口代理的外部调用入口,因此不能依赖它建立声明式事务。
2. final 方法
@Transactional
public final void saveOrder() {
}
CGLIB 类代理依靠生成子类并覆盖方法完成增强,final 方法不能被覆盖,所以无法被这种方式增强。
3. static 方法
@Transactional
public static void saveOrder() {
}
静态方法属于类,不属于 Spring 管理的 Bean 实例,实例代理无法拦截这种调用。
4. 非 public 方法
Spring Framework 6 以后,基于类的代理可以支持 protected 和包可见方法,但接口代理仍然主要面向接口中的 public 方法,不同代理方式和版本存在差异。对入门级业务代码而言,最稳妥的规则仍然是:
把事务边界放在 Spring Bean 的可覆盖 public 方法上,并从其他 Bean 通过代理调用。
五、在 Bean 初始化阶段调用事务方法
不要依赖 @PostConstruct 中的事务调用:
@PostConstruct
public void init() {
loadData();
}
@Transactional
public void loadData() {
// 数据库操作
}
一方面它属于同类调用,另一方面 Bean 初始化期间代理可能尚未处于可供业务调用的完整状态。需要在应用启动后执行事务逻辑时,可以监听容器启动完成事件,再调用另一个 Bean 的事务方法。
六、异常没有命中回滚规则
Spring 默认对以下异常回滚:
RuntimeException及其子类;Error及其子类。
默认不会因为受检异常自动回滚:
@Transactional
public void importFile() throws IOException {
repository.save(data);
throw new IOException("文件读取失败");
}
如果业务要求 IOException 也触发回滚,需要明确配置:
@Transactional(rollbackFor = Exception.class)
public void importFile() throws IOException {
repository.save(data);
throw new IOException("文件读取失败");
}
更精确的写法是只声明真正需要回滚的异常类型:
@Transactional(rollbackFor = IOException.class)
noRollbackFor 则表示某类异常不触发回滚。排查时要同时检查 rollbackFor、noRollbackFor 以及异常的实际类型,不能只看方法是否抛出了异常。
七、异常被捕获后没有继续抛出
事务拦截器需要观察目标方法的返回或异常,才能决定提交还是回滚:
@Transactional
public void createOrder() {
try {
orderRepository.save(order);
inventoryRepository.deduct(stock);
} catch (RuntimeException exception) {
log.error("创建订单失败", exception);
}
}
异常在方法内部被捕获后,方法对事务拦截器表现为正常返回,默认结果就是提交。
通常应让异常继续向外抛出:
catch (RuntimeException exception) {
log.error("创建订单失败", exception);
throw exception;
}
如果业务必须吞掉异常,也可以显式标记当前事务只能回滚:
TransactionAspectSupport
.currentTransactionStatus()
.setRollbackOnly();
不过这种方式让业务代码依赖 Spring 事务 API,能够通过异常和声明式规则表达时,优先使用声明式方式。
八、切换到异步线程或手动创建线程
传统的 PlatformTransactionManager 通常把事务资源绑定到当前线程。事务方法中启动的新线程,不会自动继承原线程的事务:
@Transactional
public void createOrder() {
orderRepository.save(order);
new Thread(() ->
inventoryRepository.deduct(stock)
).start();
}
这里的订单写入和库存扣减不在同一个本地事务中。即使外层事务回滚,也不能自动回滚新线程已经完成的数据库操作。
@Async 同样会切换执行线程。可以让异步方法在自己的线程中重新开启一个独立事务,但这仍然不是对调用方事务的继承:
调用线程事务 A
↓ 提交异步任务
异步线程事务 B
跨线程、跨进程的一致性通常需要消息、Outbox、补偿或状态机等方案,而不能依赖一个线程绑定的本地事务自然传播。
九、事务传播行为与预期不一致
传播行为决定事务方法被调用时,是加入已有事务、创建新事务,还是暂停或拒绝事务。
常见配置包括:
| 传播行为 | 主要语义 | 容易产生的误解 |
|---|---|---|
REQUIRED | 有事务就加入,没有就创建 | 内外层通常属于同一个物理事务 |
REQUIRES_NEW | 暂停外层事务,创建独立事务 | 内层提交后,外层回滚不会撤销内层提交 |
NESTED | 在已有事务中创建保存点 | 依赖事务管理器和数据库对保存点的支持 |
NOT_SUPPORTED | 暂停已有事务,以非事务方式执行 | 方法中的操作不会跟随外层事务回滚 |
NEVER | 存在事务时直接抛出异常 | 不是“不开新事务后继续执行” |
例如:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog() {
auditRepository.save(log);
}
如果调用确实经过另一个 Bean 的代理,审计日志使用独立事务。它提交后,即使外层业务事务回滚,日志仍然保留。这是传播行为的正常语义,不是事务失效。
还要注意,同类自调用时,传播配置同样会因为绕过代理而无法生效。
十、多数据源使用了错误的事务管理器
应用存在多个数据源时,通常也存在多个事务管理器:
@Transactional(transactionManager = "orderTransactionManager")
public void createOrder() {
// orderDataSource 操作
}
如果方法实际操作库存数据源,却使用订单数据源对应的事务管理器,库存连接可能没有加入当前事务。常见现象是订单表回滚了,库存表却已经提交。
排查时需要确认:
@Transactional选择的是哪个事务管理器;- Repository、Mapper 或 JdbcTemplate 使用的是哪个数据源;
- 参与操作的连接是否由当前事务管理器管理;
- 是否误以为一个本地事务能够覆盖多个独立数据库。
一个普通的本地事务管理器只能管理对应资源。多个独立数据库需要根据一致性要求选择分布式事务、消息最终一致性或业务补偿方案。
十一、底层存储或操作不支持事务
Spring 能管理事务,不代表所有底层操作都具备事务能力。例如:
- 使用不支持事务的数据库存储引擎;
- 在事务中调用远程 HTTP 接口;
- 发送不受当前事务管理器协调的普通消息;
- 操作另一个没有加入当前事务的连接或数据源;
- 数据库 DDL 的提交行为与普通 DML 不同。
这些操作即使写在 @Transactional 方法中,也不一定能随着数据库事务一起回滚。
例如下面的远程调用不会因为本地数据库回滚而自动撤销:
@Transactional
public void createOrder() {
orderRepository.save(order);
paymentClient.createPayment(order);
throw new IllegalStateException("创建失败");
}
本地订单可以回滚,但远程支付系统已经完成的操作需要对方提供撤销接口或通过补偿机制处理。
十二、代理类型或注解位置不合适
Spring AOP 常见代理包括 JDK 动态代理和基于类的代理。把 @Transactional 放在具体实现类的 public 方法上,通常最直观、最稳定:
@Service
public class OrderServiceImpl implements OrderService {
@Override
@Transactional
public void createOrder() {
// 数据库操作
}
}
只在接口上放注解可能受到代理方式、注解查找方式和 AspectJ 模式影响。Spring 官方更推荐把事务注解放在具体类的方法上,避免更换代理或织入方式后出现差异。
十三、排查顺序
1. 确认是否真的没有事务
不要只根据“数据库数据没回滚”下结论。先确认:
- 方法是否经过事务代理;
- 是否创建或加入了事务;
- 最终执行了提交还是回滚;
- 没回滚的操作是否属于同一个事务资源。
可以临时提高 Spring 事务相关日志级别,并结合数据库连接、SQL 日志和调用链判断。
2. 检查代理入口
依次确认:
对象是不是 Spring Bean
↓
调用是不是从其他 Bean 进入
↓
方法是否可被当前代理方式增强
↓
是否发生在 Bean 初始化阶段
3. 检查回滚条件
重点检查:
实际抛出的异常类型
rollbackFor / noRollbackFor
异常是否在方法内部被捕获
异常是否在事务方法返回后才发生
事务方法已经正常返回并提交后,调用方后续再抛异常,不能回头撤销已经提交的事务。
4. 检查事务边界
继续确认:
- 是否切换线程;
- 是否使用
REQUIRES_NEW、NOT_SUPPORTED等传播行为; - 是否跨数据源或跨数据库;
- 是否包含远程调用、消息或其他非事务资源。
十四、常见问题速查
| 现象 | 首先检查 |
|---|---|
| 方法完全没有事务日志 | 对象是否为 Spring Bean,调用是否经过代理 |
| 同类方法上的注解不生效 | 是否通过 this 自调用 |
| 抛异常但数据仍提交 | 异常是否为受检异常,是否被捕获 |
| 外层回滚但内层数据保留 | 是否使用 REQUIRES_NEW |
| 主线程回滚但异步操作成功 | 是否切换到 @Async 或新线程 |
| 一个库回滚、另一个库提交 | 事务管理器和数据源是否匹配 |
| 数据库回滚但远程操作没撤销 | 远程调用不属于本地事务 |
评论与讨论
回复 :
留下你的想法