我见过不少项目把这个问题归因成「注解没扫到」。但如果注解明明在、Bean 也正常,先别急着查配置。
多数时候,是调用没经过 Spring 的事务代理。
同一个类里这么调,saveOrder不会被拦截
@Service public class OrderService { public void createOrder() { saveOrder(); } @Transactional public void saveOrder() { orderRepository.save(new Order()); } }
请求进来时,外层确实会先经过OrderService的代理。
可createOrder()运行起来以后,调用saveOrder()用的是当前对象的this。它没有再绕回代理,所以事务拦截器根本看不见这次调用。
这也是为什么调试时常会觉得很别扭:@Transactional就贴在方法上,断点也进来了,实际却没有事务。
Spring Framework 的事务文档把这个行为说得很直白:默认代理模式只拦截从代理进入的外部调用,同一对象内部的自调用不会触发事务。
业务入口和事务方法拆开,调用路径就清楚了
我更常用的写法是把编排和落库拆成两个 Bean:
@Service @RequiredArgsConstructor public class OrderFacade { private final OrderService orderService; public void createOrder() { orderService.saveOrder(); } } @Service public class OrderService { @Transactional public void saveOrder() { orderRepository.save(new Order()); inventoryRepository.decrease(); } }
这里OrderFacade注入的是OrderService的代理,调用saveOrder()时才会进入事务边界。
如果事务本来就该覆盖整个createOrder(),那就直接把注解放在入口方法上,内部再调私有的辅助方法。别为了「让注解生效」专门搞自注入,调用链会越来越难看。
事务开了,也不等于所有异常都会回滚
还有一个面试里很容易接着问的点:
@Transactional public void saveOrder() throws IOException { orderRepository.save(new Order()); throw new IOException("network error"); }
默认规则下,RuntimeException和Error会触发回滚,受检异常不会。上面这段如果没有额外配置,写入仍可能提交。
需要把受检异常也纳入回滚时,明确写出来:
@Transactional(rollbackFor = IOException.class) public void saveOrder() throws IOException { orderRepository.save(new Order()); throw new IOException("network error"); }
排查事务问题时,我一般先看日志里是不是出现了事务边界,再看调用是不是绕过了代理,最后才检查传播行为和回滚规则。