Spring 事务失效的 8 个场景,每一个都踩过
@Transactional 在同类方法调用、非 public 方法、异常被吞掉、多数据源等 8 种场景下会悄悄失效。每种给出复现代码、失效原因和修法。
"我加了 @Transactional,为什么还是没回滚?"
这是 Spring 里被问得最多的问题之一。答案往往是这句话:@Transactional 不是"打开事务"的开关,它只是给方法套了一层代理。 代理绕过去的地方,事务就不存在。
先把这个前提讲透,后面 8 个场景就好理解了。
一、事务到底是怎么加上去的
Spring 的事务基于 AOP 动态代理。你写:
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) { ... }
}
Spring 启动时给 OrderService 生成了一个代理对象,容器里装的其实是这个代理。外部调用 orderService.createOrder() 时,实际执行的是代理的 createOrder:
调用方
-> 代理对象
|-- 1. 从 DataSource 拿连接,关闭自动提交
|-- 2. 执行业务方法(真实对象)
|-- 3. 正常返回 -> commit
|-- 4. 抛异常 -> rollback
-> 真实对象的方法体
因此有一条铁律:只有经过代理的调用,事务才生效。 剩下的 8 个场景,本质上都是"代理被绕过"或者"回滚条件不满足"。
场景 1:同类内部调用(最高频)
@Service
public class OrderService {
public void outer() {
// 这是 this.inner(),绕过了代理
this.inner();
}
@Transactional
public void inner() {
// 这里的事务不生效
orderMapper.insert(...); // 抛异常也不会回滚
}
}
this.inner() 调用的是真实对象自己,代理完全没有参与。所有方法级别的切面在这个瞬间全部失效。
修复三选一:
最干净的是拆分职责,让调用跨类:
@Service
public class OrderService {
@Resource private OrderInnerService innerService;
public void outer() {
innerService.inner(); // 走代理
}
}
其次可以自注入代理:
@Service
public class OrderService {
@Resource @Lazy private OrderService self; // @Lazy 避免循环依赖
public void outer() {
self.inner();
}
}
或者用 AopContext.currentProxy()(需要在启动类加 @EnableAspectJAutoProxy(exposeProxy = true),性能略差,不推荐)。
注意 @Lazy 那个写法实际拿到的是代理对象,不是真实对象——这也是为什么它能生效。
场景 2:方法不是 public
Spring 5.x 的 TransactionInterceptor 在处理时,对非 public 方法默认不拦:
@Transactional
private void doSomething() { ... } // 不生效
@Transactional
protected void doOther() { ... } // 不生效(CGLIB 下也是)
@Transactional
void packageMethod() { ... } // 不生效
原因在 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute:
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
修复:改成 public。 如果确实需要 private 的逻辑带事务,说明方法边界划错了,应该提取到独立的类里。
补充一句:Spring 6 / Boot 3 之后对 protected 和包级方法有放宽(ProxyTransactionManagementConfiguration 的行为变了),但依赖这个特性会让代码在 5.x 和 6.x 之间行为不一致,不如统一用 public。
场景 3:异常被自己吞掉了
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int a = 1 / 0;
} catch (Exception e) {
log.error("下单失败", e);
// 异常被吃掉了,代理看不到,认为方法正常返回 -> commit
}
}
事务管理器靠异常传播到代理层来判断要不要回滚。catch 住不抛,代理收到的是正常返回,直接 commit,数据就脏了。
修复:要么重新抛,要么手动标记回滚。
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int a = 1 / 0;
} catch (Exception e) {
log.error("下单失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new BizException("下单失败", e); // 推荐:让调用方知道失败了
}
}
只调用 setRollbackOnly() 不抛异常,事务会回滚但调用方拿到的是成功——这个坑比不回滚更隐蔽,因为它静默失败。
场景 4:抛的是检查异常
@Transactional 的默认 rollbackFor 是 RuntimeException 和 Error,不包括 Exception 及其检查子类:
@Transactional
public void importData() throws IOException {
// 抛 IOException 时不会回滚
Files.readAllBytes(path); // IOException
mapper.insert(...); // 已经写进去了
}
修复:显式声明。
@Transactional(rollbackFor = Exception.class)
这条建议值得无脑加在所有业务方法上。约定优于配置在这里反而危险——没人会记得"我抛的是检查异常"。
场景 5:类没被 Spring 管理
// 没有 @Service / @Component
public class OrderService {
@Transactional
public void create() { ... }
}
// 调用方
OrderService s = new OrderService(); // 自己 new 的,没有代理
s.create(); // 事务不生效
你 new 出来的对象,Spring 一无所知,自然也不会给它套代理。
同类问题还有:
- 用了
@Component但所在包不在@ComponentScan范围内 - 注解加在接口上而代理方式是 CGLIB 且
proxyTargetClass=true(部分版本会丢注解) - 把
@Transactional加在@Bean方法返回的new对象上
修复:确认对象出自 Spring 容器。 快速验证方法是在类里注入 ApplicationContext,看 ctx.getBean(OrderService.class) 拿到的是不是 OrderService$$EnhancerBySpringCGLIB$$xxx。
场景 6:多线程与异步
@Transactional
public void batchCreate(List<Order> orders) {
orders.forEach(o -> executor.submit(() -> {
// 子线程里,事务上下文(ThreadLocal)已经丢了
orderMapper.insert(o); // 独立自动提交,外部事务回滚不了它
}));
}
Spring 的事务上下文存在 ThreadLocal 里。跨线程就断了,子线程的操作走自己的连接、自己提交。
修复:让每个子任务自己开事务,并且由外部统一收集异常。
@Service
public class OrderBatchService {
@Resource private OrderService orderService;
public void batchCreate(List<Order> orders) throws Exception {
List<Future<?>> futures = new ArrayList<>();
for (Order o : orders) {
futures.add(executor.submit(() -> orderService.createOne(o)));
}
for (Future<?> f : futures) {
f.get(); // 有异常就会抛出来,触发补偿逻辑
}
}
}
@Service
class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOne(Order o) { orderMapper.insert(o); }
}
注意这样不是原子操作——部分成功部分失败。真正的分布式一致性要靠补偿(Saga/TCC)或者先落中间表再异步处理。
@Async 同理:@Async + @Transactional 加在同一个方法上,事务范围只包住"提交任务"这一步,异步线程里的操作不受控。
场景 7:传播行为设错
@Transactional(propagation = ...) 的 7 种取值里,最容易被误用的是这三个:
REQUIRES_NEW:挂起当前事务,新开一个独立事务,独立提交独立回滚。
@Transactional
public void placeOrder() {
orderMapper.insert(o); // 事务 A
logService.writeLog(); // REQUIRES_NEW -> 事务 B,自己提交
throw new RuntimeException(); // A 回滚,但 B 已提交,日志留下了
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeLog() { ... }
这不是 bug,是设计意图。但如果你的预期是"要么全成功要么全失败",那就是用错了。
NOT_SUPPORTED:挂起事务,以非事务方式执行。常见误区是拿它当"只读加速"用,实际它完全不参与事务,里面写的操作无法回滚。
SUPPORTS:有事务就加入,没有就以非事务执行。在外层没开事务的情况下被调用,里面的操作是没有事务保护的——这常常出现在"某个方法被两个入口调用,一个开事务一个没开"的场景里。
场景 8:数据库引擎不支持
SHOW TABLE STATUS LIKE 'orders';
-- Engine: MyISAM <- 不支持事务
MySQL 5.5 之后默认是 InnoDB,但老库迁移过来的表、或者建表时手写了 ENGINE=MyISAM,就会静默失败:@Transactional 正常执行、正常 commit,但回滚时数据纹丝不动。
修复:
ALTER TABLE orders ENGINE = InnoDB;
附:两个不在"失效"范畴但同样致命的问题
事务里做远程调用。 事务开着的时候去调第三方 HTTP 接口,连接池被占住,接口一慢就把数据库连接池打满:
@Transactional
public void createOrder() {
orderMapper.insert(o);
httpClient.post("https://third-party/notify"); // 网络抖动 5 秒 = 连接被占 5 秒
}
正确姿势是把远程调用移出事务边界:先落库提交,再用事件或消息队列异步通知。
事务里做大批量操作。 一条 @Transactional 包住 10 万行 update,会产生巨大的 undo log,长事务还会阻塞其他会话。分批提交更稳。
自检清单
排查"事务不生效"时按顺序过一遍,能覆盖 95% 的情况:
- 对象是不是从容器里拿的(不是
new的) - 调用是不是经过了代理(不是
this.自调用) - 方法是不是
public - 异常有没有被
catch掉 - 抛的是不是
RuntimeException(检查异常需要rollbackFor) - 是不是跨线程/异步了
- 传播行为是不是
REQUIRES_NEW/NOT_SUPPORTED(独立提交,回滚不了) - 表引擎是不是 InnoDB
快速验证是否真的开了事务,最直接的办法是打开 SQL 日志看有没有 SET autocommit=0:
logging:
level:
org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG
org.mybatis.spring.transaction: DEBUG
如果这行日志压根没出现,说明事务根本没开始,直接跳到第 1、2、3 条去查。
正在加载评论…