Spring 事务那点事,我被坑过的几次
起因
项目里有个下单逻辑,扣库存和写订单放一个方法里,加了 @Transactional,测的时候没问题。上线之后有一回库存扣了,订单没生成,查了半天发现事务根本没回滚。那会儿才知道,@Transactional 不是加上就万事大吉的。
这篇就把我自己踩过的、以及排查时搞清楚的东西记一下。
默认只回滚 RuntimeException
这个是最容易忽略的。@Transactional 默认只在抛出 RuntimeException 和 Error 时回滚,受检异常(比如 IOException)抛出去是不回滚的。
@Transactional
public void createOrder() throws IOException {
orderDao.insert(order);
fileService.write(file); // 抛 IOException
}
上面这段,IOException 抛了,订单照样写进去了。想让它回滚得显式指定:
@Transactional(rollbackFor = Exception.class)
我现在的习惯是,只要写 @Transactional,就把 rollbackFor = Exception.class 带上,省得以后忘。
异常被 catch 了,等于没抛
@Transactional
public void createOrder() {
try {
orderDao.insert(order);
stockService.deduct(order);
} catch (Exception e) {
log.error("下单失败", e);
}
}
catch 住没往外抛,Spring 认为方法正常结束,直接提交。要回滚就得在 catch 里重新抛出去,或者手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
一般我不在事务方法里 catch,真要处理异常就往外抛,让上层去处理。
同类调用,事务不生效
这个上一篇提过,但事务场景下最容易撞上,再写一遍。
@Service
public class OrderService {
public void batchCreate(List<Order> orders) {
for (Order o : orders) {
this.createOne(o); // 走 this,代理没参与
}
}
@Transactional
public void createOne(Order o) { }
}
createOne 上的事务完全不生效,一条失败不会回滚前面的。这种我一般把 createOne 抽到单独的 Service 里,让 Spring 注入进来调,比注入自己干净。
事务方法不是 public
Spring 事务默认基于代理,代理拦不到非 public 方法。protected、private 上标 @Transactional,加了也白加,还容易让人误以为有事务。
这个没什么好办法,改成 public,或者用 AspectJ 模式(一般不这么干)。
传播行为,默认 REQUIRED 有时不够
默认是 REQUIRED:有事务就加入,没有就新建。大部分场景够用,但有两种情况得注意。
一是内层方法想独立提交。 比如主流程失败要回滚,但操作日志得留下来。这时候用 REQUIRES_NEW:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Log log) { }
它会挂起外层事务,自己开一个新事务,外层回滚不影响它。但要注意,REQUIRES_NEW 会占用额外连接,并发高的时候连接池容易吃紧。
二是查询方法。 只读查询用 SUPPORTS 或者 NOT_SUPPORTED,别白白占着事务。我们项目里查询方法基本不标事务,需要的时候才加 @Transactional(readOnly = true)。
事务和异步一起用,顺序很要命
@Transactional
public void createOrder() {
orderDao.insert(order);
asyncService.sendNotify(order); // @Async
}
sendNotify 是异步的,可能在事务提交前就执行了,这时候查订单查不到。要么把异步调用挪到事务提交之后(用 TransactionSynchronization 或者事件监听),要么接受这个时序。
我是用 @TransactionalEventListener(phase = AFTER_COMMIT) 处理的,事务提交后再发通知,稳一点。
自测的时候怎么确认事务真的生效了
光看代码看不出来,我一般这么验证:
- 在事务方法里手动抛个
RuntimeException,看数据有没有进去。 - 开
logging.level.org.springframework.transaction=DEBUG,看日志里有没有Creating new transaction、Rolling back这些。 - 断点打在
DataSourceTransactionManager的doBegin和doRollback上,看走没走到。
第 2 种最省事,日志一看就知道事务开没开、回没回滚。
小结
事务这块,出错基本都是"以为生效了其实没有"。几个高频原因:异常类型不对、异常被吞了、同类调用、方法不是 public。排查的时候先看日志确认事务开没开,再看异常有没有抛出去,基本能定位到。
先记这些,后面再遇到别的坑接着补。
- 点赞
- 收藏
- 关注作者
评论(0)