Spring事务失效的8个场景:每个场景都有可复现的代码
2026/4/30大约 7 分钟
Spring事务失效的8个场景:每个场景都有可复现的代码
适读人群:在项目中遇到过事务不回滚问题的Java开发者 | 阅读时长:约20分钟
开篇故事
生产环境出现过一次数据不一致的事故:订单创建成功了,但库存没有扣减,两张表的数据对不上。
排查了一个多小时,最终发现是 @Transactional 加在了一个私有方法上。私有方法!Spring AOP 代理对私有方法无效,事务根本没有开启,两个数据库操作完全独立执行。
这是我职业生涯里遇到的最"无语"的 bug 之一,也是最有学习价值的。
事务失效这个话题,很多人只知道"同一个类调用自身的方法"那个坑,其实还有很多其他场景。今天把 8 个最常见的全部整理出来,每个都有可以直接跑的代码。
一、8个场景速览
二、8个场景完整代码
场景1:方法不是 public
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// ❌ 错误:@Transactional加在private方法上,事务完全不生效
@Transactional
private void createOrderPrivate(Order order) {
orderMapper.insert(order);
if (order.getAmount() > 10000) {
throw new RuntimeException("金额超限");
}
// 即使抛异常,insert也不会回滚!
}
// ✅ 正确:public方法
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
if (order.getAmount() > 10000) {
throw new RuntimeException("金额超限");
}
}
}原因:Spring 的 CGLIB 代理通过继承实现,子类无法覆盖父类的 private 方法,AOP 切面无法拦截。protected 方法在某些配置下也有问题,最保险的是始终用 public。
场景2:同类内部方法调用(最常见!)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
// 外部调用这个方法:事务正常(有代理)
public void processOrder(Order order) {
// ❌ 错误:this.xxx 调用的是原始对象,不是代理对象,事务不生效
this.createOrder(order);
this.deductStock(order.getProductId(), order.getQuantity());
}
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
}
@Transactional
public void deductStock(Long productId, Integer quantity) {
stockMapper.deduct(productId, quantity);
}
}可复现的验证:
@Service
public class TransactionSelfCallDemo {
@Autowired
private TransactionSelfCallDemo self; // 注入自己的代理
@Autowired
private UserMapper userMapper;
public void outerMethod() {
// ❌ 通过 this 调用,事务不生效
this.innerTransactional();
// ✅ 通过 self(代理)调用,事务生效
self.innerTransactional();
}
@Transactional
public void innerTransactional() {
userMapper.insert(new User("test"));
throw new RuntimeException("测试回滚");
// 用this调用时:数据已写入,不会回滚
// 用self调用时:数据写入后被回滚
}
}三种解决方案:
// 方案1:注入自己(常用)
@Autowired
private OrderService self;
self.createOrder(order);
// 方案2:通过AopContext获取代理(需要exposeProxy=true)
@EnableAspectJAutoProxy(exposeProxy = true) // 在配置类上加
((OrderService) AopContext.currentProxy()).createOrder(order);
// 方案3:重构代码,把需要事务的方法提取到独立的Bean
@Service
public class OrderCreationService {
@Transactional
public void createOrder(Order order) { ... }
}场景3:异常被 catch 吞掉
@Service
public class OrderService {
@Transactional
public void createOrderWithCatch(Order order) {
try {
orderMapper.insert(order);
stockMapper.deduct(order.getProductId(), order.getQuantity());
if (true) throw new RuntimeException("模拟异常");
} catch (Exception e) {
// ❌ 错误:把异常吞掉了,Spring感知不到异常,不会回滚!
log.error("创建订单失败", e);
// 数据库操作已经发生,但不会回滚,数据不一致!
}
}
@Transactional
public void createOrderCorrect(Order order) {
try {
orderMapper.insert(order);
stockMapper.deduct(order.getProductId(), order.getQuantity());
} catch (Exception e) {
log.error("创建订单失败", e);
// ✅ 方案1:重新抛出异常,让Spring感知
throw e;
// ✅ 方案2:手动标记事务回滚
// TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}场景4:抛出的是 Checked 异常
@Service
public class OrderService {
@Transactional // ❌ 默认只回滚 RuntimeException 和 Error
public void createOrderChecked(Order order) throws IOException {
orderMapper.insert(order);
// IOException 是 checked exception,默认不触发回滚!
throw new IOException("文件操作失败");
// 数据已写入,不会回滚!
}
// ✅ 解决方案:指定 rollbackFor
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void createOrderFixed(Order order) throws IOException {
orderMapper.insert(order);
throw new IOException("文件操作失败");
// 现在会回滚了
}
}建议:在公司内部规范里统一要求 @Transactional(rollbackFor = Exception.class),避免遗漏。
场景5:多线程中使用事务
@Service
public class BatchOrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void batchCreateOrders(List<Order> orders) {
// ❌ 错误:开了一个新线程,新线程里的操作不在当前事务里
orders.forEach(order -> {
new Thread(() -> {
// 这里的 insert 在独立的连接里执行,和外层事务不相关
orderMapper.insert(order);
if (order.getAmount() < 0) {
throw new RuntimeException("金额不合法");
}
}).start();
});
// 主线程事务提交,但子线程里的操作可能还没完成,也不在这个事务里
}
// ✅ 正确:在同一个线程里执行
@Transactional(rollbackFor = Exception.class)
public void batchCreateOrdersCorrect(List<Order> orders) {
for (Order order : orders) {
orderMapper.insert(order);
if (order.getAmount() < 0) {
throw new RuntimeException("金额不合法");
}
}
// 所有操作在同一个事务里,任何一个失败都全部回滚
}
}原因:Spring 事务用 ThreadLocal 绑定连接,跨线程事务失效。
场景6:数据库引擎不支持事务
-- ❌ MyISAM 不支持事务
CREATE TABLE `orders` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`amount` decimal(10,2) NOT NULL
) ENGINE=MyISAM; -- 换成 InnoDB!
-- ✅ 使用 InnoDB
CREATE TABLE `orders` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`amount` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个坑在老项目里会遇到,建表时用了 MyISAM,或者 ALTER TABLE 时不小心改了引擎。验证方式:SHOW CREATE TABLE orders; 查看引擎类型。
场景7:错误的传播机制
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
try {
// ❌ 问题:inventoryService.deduct() 内部抛异常
// 但被catch住了,Spring以为一切正常
// 但事务已经被标记为rollback-only了!
inventoryService.deduct(order.getProductId(), order.getQuantity());
} catch (Exception e) {
log.warn("库存扣减失败,忽略: {}", e.getMessage());
}
// 在这里尝试提交事务 → 报 UnexpectedRollbackException
// 因为子事务已经把事务标记为rollback-only
}
}@Service
public class InventoryService {
@Transactional // 默认 REQUIRED,加入外层事务
public void deduct(Long productId, Integer quantity) {
// 抛异常时,会把外层共享的事务标记为 rollback-only
throw new RuntimeException("库存不足");
}
}解决方案:
// 方案1:子事务用 REQUIRES_NEW,开独立事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deduct(Long productId, Integer quantity) {
// 失败只影响这个独立事务,不污染外层事务
throw new RuntimeException("库存不足");
}
// 方案2:子事务用 NESTED(嵌套事务,回滚到保存点)
@Transactional(propagation = Propagation.NESTED)
public void deduct(Long productId, Integer quantity) {
// 失败只回滚到嵌套事务的保存点,外层事务可以继续
throw new RuntimeException("库存不足");
}场景8:@Transactional 加在接口上
// 接口层加注解
public interface OrderService {
@Transactional // ❌ 加在接口上,在某些情况下不生效
void createOrder(Order order);
}
// 实现类
@Service
public class OrderServiceImpl implements OrderService {
@Override
public void createOrder(Order order) {
// ...
}
}何时生效,何时不生效:
- 用 JDK 动态代理时:代理会考虑接口上的注解,可能生效(取决于版本)
- 用 CGLIB 代理时(Spring Boot 默认):CGLIB 代理继承实现类,不继承接口,接口上的注解不生效
最佳实践:始终把 @Transactional 加在实现类的方法上,而不是接口上。
三、踩坑实录(额外场景)
坑:@Transactional 和 @Async 同时使用
@Service
public class OrderService {
@Transactional
@Async // ❌ 异步方法 + 事务 = 事务失效
public void asyncCreateOrder(Order order) {
// @Async 把这个方法放到新线程执行
// @Transactional 的事务绑定在主线程的ThreadLocal上
// 新线程里事务不生效
orderMapper.insert(order);
}
}解决:@Async 和 @Transactional 分开,在异步方法里调用一个带事务的同步方法:
@Async
public void asyncCreateOrder(Order order) {
// 在异步线程里调用事务方法(通过代理)
self.doCreateOrder(order);
}
@Transactional
public void doCreateOrder(Order order) {
orderMapper.insert(order);
}四、总结:事务失效检查清单
| 场景 | 检查点 |
|---|---|
| 场景1 | 方法是否 public |
| 场景2 | 是否通过代理对象调用(避免 this.xxx) |
| 场景3 | 异常是否被 catch 吞掉 |
| 场景4 | rollbackFor 是否包含 Checked 异常 |
| 场景5 | 是否在多线程中调用 |
| 场景6 | 数据库引擎是否支持事务(InnoDB) |
| 场景7 | 传播机制是否符合预期 |
| 场景8 | 注解加在实现类而非接口上 |
遇到事务问题,按这个清单逐一排查,99% 的情况都能找到原因。
