场景实战:Spring 事务失效分析
❓ 面试实战:你们项目中 @Transactional 事务有失效过吗?一般是什么原因导致的?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): 我们在开发中确实踩过不少事务失效的坑。排查下来,90% 的原因都是破坏了 Spring AOP 的代理机制(比如发生了“同类内部方法自调用”),另外 10% 是因为异常被手动吃掉(catch)或者抛出了不受检的异常类型。
📝 详细经典失效场景与解决方案(面试高分答题模板):
场景一:同类内部方法自调用(最经典、最容易犯的错)
- 错误代码重现:java
@Service public class OrderService { public void createOrder() { // 在没有加事务的方法里,直接调用了加了事务的方法 updateStock(); } @Transactional public void updateStock() { // 扣减库存,发生异常 int a = 1 / 0; } } - 为什么失效? Spring 事务的底层是 AOP 动态代理。当你从 Controller 调
createOrder时,拿到的是代理对象。但由于createOrder没加事务,代理对象就直接调用了真实对象的原生createOrder方法。 在真实对象的内部,执行updateStock()相当于this.updateStock()。这个this指向的是原生对象自己,而不是被 Spring 增强过的代理对象!所以切面逻辑(开启事务、提交、回滚)根本没有被触发,直接裸奔执行了。 - 怎么解决?
- 最推荐:重构代码。把
updateStock抽离到另一个StockService类中去调用。 - 懒人解法(暴露代理):在类上加
@EnableAspectJAutoProxy(exposeProxy = true),然后在代码里强行拿到当前代理对象再去调用:((OrderService) AopContext.currentProxy()).updateStock();。 - 利用自己注入自己:在
OrderService里@Autowired自己,然后用注入的对象调用。
- 最推荐:重构代码。把
场景二:异常被手动 catch 给吃了(极其坑人)
- 错误代码重现:java
@Transactional public void doBiz() { try { userMapper.insert(); int a = 1 / 0; // 发生异常 } catch (Exception e) { log.error("插入失败", e); // 致命错误:抓住了异常却没有往外抛出! } } - 为什么失效? Spring 的 AOP 切面是包裹在
doBiz()外面的。它是否回滚,完全取决于它能不能捕捉到你方法里抛出的异常。 现在异常被你自己的try-catch给悄悄“消化”了,方法正常执行结束。外面的切面一看,哦,没报错,然后开心地执行了commit提交事务!于是脏数据就这么存进数据库了。 - 怎么解决? 如果在事务方法里必须要写 catch,在 catch 块的最后,必须手动抛出个 RuntimeException,或者手动触发回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。
场景三:修饰符错误或异常类型不匹配
- 非
public方法:@Transactional只能加在public方法上。如果加在private或protected方法上,Spring AOP 在拦截时会直接放行,事务彻底失效。
- 默认只回滚
RuntimeException和Error:- 如果你方法里抛出了一个受检异常(比如
IOException或者自定义的非运行时异常),Spring 默认是不回滚的! - 血泪教训解决方案:在公司开发规范里,强制要求所有
@Transactional必须写成@Transactional(rollbackFor = Exception.class),不管抛什么异常通统回滚,防患于未然。
- 如果你方法里抛出了一个受检异常(比如
❓ 面试实战:除了以上这些,如果遇到嵌套事务(多方法互相调用都有事务),它是怎么控制的?(事务传播行为)【中高级】
频率:🔥🔥🔥
💡 一句话总结(先抛结论): 这就是 Spring 的事务传播行为(Propagation)。默认的策略是 REQUIRED(如果当前没事务,就建一个;如果外层有事务,就加入外层的,大家同生共死)。但我们在实际业务中最常用的是 REQUIRES_NEW(不管外层有没有,我自己必须新建一个独立的事务,死活不连累别人)。
📝 核心实战场景:日志记录不随主业务回滚
- 业务痛点:订单支付接口里,无论支付成功还是失败抛了异常(主事务回滚),我都必须在数据库里记录一条“支付流水请求日志”。
- 错误做法:如果你把
saveLog()和pay()都用默认的REQUIRED。当pay()抛异常时,外层事务大回滚,刚刚成功保存的saveLog()也会被无情地一起回滚掉!连追查报错的日志都没了。 - 正确做法: 在
LogService.saveLog()方法上,加上@Transactional(propagation = Propagation.REQUIRES_NEW)。- 原理:当执行到
saveLog时,Spring 会把外层的支付事务先挂起,然后新建一个全新的事务给saveLog用。 - 效果:
saveLog自己执行完立马commit落盘。随后即使外层支付业务抛异常导致外层事务回滚,它绝对不会影响到已经独立提交的日志记录。
- 原理:当执行到