Spring 事务那点事,我被坑过的几次

举报
yd_241780069 发表于 2026/10/22 22:17:21 2026/10/22
【摘要】 起因项目里有个下单逻辑,扣库存和写订单放一个方法里,加了 @Transactional,测的时候没问题。上线之后有一回库存扣了,订单没生成,查了半天发现事务根本没回滚。那会儿才知道,@Transactional 不是加上就万事大吉的。这篇就把我自己踩过的、以及排查时搞清楚的东西记一下。 默认只回滚 RuntimeException这个是最容易忽略的。@Transactional 默认只在抛...

起因

项目里有个下单逻辑,扣库存和写订单放一个方法里,加了 @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) 处理的,事务提交后再发通知,稳一点。

自测的时候怎么确认事务真的生效了

光看代码看不出来,我一般这么验证:

  1. 在事务方法里手动抛个 RuntimeException,看数据有没有进去。
  2. 开 logging.level.org.springframework.transaction=DEBUG,看日志里有没有 Creating new transaction、Rolling back 这些。
  3. 断点打在 DataSourceTransactionManager 的 doBegin 和 doRollback 上,看走没走到。

第 2 种最省事,日志一看就知道事务开没开、回没回滚。

小结

事务这块,出错基本都是"以为生效了其实没有"。几个高频原因:异常类型不对、异常被吞了、同类调用、方法不是 public。排查的时候先看日志确认事务开没开,再看异常有没有抛出去,基本能定位到。

先记这些,后面再遇到别的坑接着补。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。