最后更新:2026-10-28
适用场景:Spring Boot 3.x、Spring Framework 6.x、JDBC / MyBatis 单库事务
订单写入成功了,后面扣库存抛了异常,数据库里却只留下半条业务记录。看到 @Transactional,很容易先怀疑数据库。实际排查时,我会先问两个更近的问题:这次调用有没有进入事务代理?异常有没有从事务方法里抛出去?
先用一个可观察的失败验证事务
假设项目已引入 JDBC Starter、配置好单库事务管理器,并在测试库里准备了一张 demo_order 表。然后让一次插入后必然抛出运行时异常:
@Service
public class OrderService {
private final JdbcTemplate jdbcTemplate;
public OrderService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void create(long id) {
jdbcTemplate.update("insert into demo_order(id) values (?)", id);
throw new IllegalStateException("模拟后续步骤失败");
}
}
从 另一个 Spring Bean 调用 orderService.create(1001L),捕获异常后查询 id=1001。正常的单库事务应看不到这条记录。不要只看接口返回了 500;返回状态并不能证明数据库回滚了。测试前清掉同 ID 的旧数据,避免把上次的记录当成这次的结果。
想确认当前线程是否真的有事务,可在方法内临时打印:
TransactionSynchronizationManager.isActualTransactionActive()
它返回 false,先查代理与事务管理器;返回 true,再查异常、传播行为和实际使用的数据源。这个值只用于定位,别把它当成业务代码的永久分支。Spring 的声明式事务文档说明了默认代理模式的边界。
最常见的坑:同一个类里自己调用自己
下面的 submit() 调用 save(),没有经过 Spring 代理。save() 上有注解,不代表这次调用被事务拦截:
@Service
public class OrderService {
public void submit(long id) {
save(id); // 同类内部调用
}
@Transactional
public void save(long id) {
// 写订单、扣库存
}
}
把事务边界放在实际被外部调用的 submit() 上,或者把 save() 拆进另一个 Spring Bean,由 OrderService 注入并调用。通常前者更符合“下单要么全成、要么全败”的业务边界。private 方法、new OrderService() 创建的对象也不适合作为这种代理事务的入口;先确认调用对象确实来自容器。
异常被吃掉,事务会认为方法正常结束
假如 create() 里把库存异常抓住,只打日志,然后正常 return,默认事务拦截器看不到失败,就会提交前面的写入。
@Transactional
public void create(long id) {
try {
saveOrder(id);
reduceStock(id);
} catch (RuntimeException ex) {
log.error("下单失败,订单号={}", id, ex);
throw ex; // 让异常继续穿过事务边界
}
}
若业务必须在方法内转换异常,就转换成有明确语义的运行时异常并保留 cause;不要吞掉后返回“失败”字符串。Spring 默认对 RuntimeException 和 Error 回滚,受检异常默认不触发回滚。确实要对受检异常回滚,可以写 @Transactional(rollbackFor = IOException.class),但要先确认调用方能处理这种异常,而不是把所有异常一股脑改成 Exception.class。回滚规则有完整说明。
还有事务,但不是同一个事务
排查到这里还不对,再看三类边界:
@Transactional(propagation = REQUIRES_NEW)会开启独立事务。内层已经提交,外层后来失败,不能指望外层把内层一并撤销。@Async、新线程或线程池里的数据库操作不自动加入调用方线程的事务。跨线程工作应设计成独立任务,而不是假设注解能传过去。- 项目有多个数据源或事务管理器时,确认注解使用的管理器与实际写入的数据库一致;单库示例不能直接套到多库场景。
MyBatis 项目还要确认 Mapper 参与的是 Spring 管理的会话,不要自行打开 SqlSession 后绕开现有事务配置。事务只负责它管得到的资源;调用第三方接口、发消息、写本地文件并不会因为数据库回滚而自动撤销。
一次排查只改一个变量
先用最小插入加抛异常确认回滚,再逐步加回业务步骤。每次记录:调用入口、事务是否 active、异常类型、异常是否被重新抛出、最终数据库行数。这样能分清“根本没进事务”和“进了事务但提前提交”。
如果你刚在多环境里切换数据库,也顺手核对实际加载的配置;Spring Boot 配置文件怎么写里有 profile 和配置覆盖的基础排查。业务层结构可对照Spring Boot 整合 MyBatis。
参考资料
Spring Framework 6.2:使用 @Transactional
Spring Framework 6.2:事务回滚规则
更新记录
2026-10-28:创建文章,补充代理、自调用、异常类型与事务边界的验证方法。
Spring Boot @Transactional 不回滚怎么排查
https://java.li/archives/spring-boot-transactional-not-rollback
评论