Spring 事务失效:@Transactional 自调用不生效的原因与 4 大失效场景
本文通过一个「注册送积分」的真实案例,讲清楚 Spring 声明式事务的底层机制(AOP 动态代理),并系统梳理
@Transactional失效的 4 大场景与对应解决方案。读完你会明白:为什么注解明明加了,事务却不生效。
一、问题案例
用户注册接口需要做两件事:写入 user 表、写入 point 表(送 500 积分)。期望两者在同一事务中,任何一步失败都整体回滚。
@ServicepublicclassUserService{@AutowiredprivateUserMapperuserMapper;@AutowiredprivatePointMapperpointMapper;@Transactionalpublicvoidregister(Useruser){userMapper.insert(user);// 写 user 表this.addPoints(user.getId());// 送积分}@TransactionalpublicvoidaddPoints(LonguserId){pointMapper.insert(userId,500);}}线上某天 point 表插入失败,预期 user 表也回滚。但结果是:user 表数据留下了,积分却没到账。问题就出在this.addPoints()这一行。
二、原理详解:事务靠代理生效
Spring 的声明式事务不是靠方法本身起作用的,而是靠AOP 动态代理。
- 你在别处通过
@Autowired注入的UserService,其实是一个代理对象(JDK 动态代理或 CGLIB 代理)。 - 调用
register时,代理对象先开启事务,再执行你的业务逻辑,最后提交或回滚。 - 但
this.addPoints()里的this指向的是原始对象(目标对象),不是代理对象。直接this调用跳过了代理,addPoints上的@Transactional等于没写,它既开启不了自己的事务,也加入不了register的事务。
一句话总结:同类内部自调用,事务注解全部失效。
三、实战代码:三种解决方案
方案一:拆到独立 Bean(最推荐)
把事务方法拆到另一个 Service,通过注入走代理,职责也更清晰:
@ServicepublicclassPointService{@AutowiredprivatePointMapperpointMapper;@TransactionalpublicvoidaddPoints(LonguserId){pointMapper.insert(userId,500);}}@ServicepublicclassUserService{@AutowiredprivatePointServicepointService;@Transactionalpublicvoidregister(Useruser){userMapper.insert(user);pointService.addPoints(user.getId());// 走代理,事务生效}}方案二:注入自己(self-injection)
@ServicepublicclassUserService{@AutowiredprivateUserServiceself;// 注入自己的代理对象@Transactionalpublicvoidregister(Useruser){userMapper.insert(user);self.addPoints(user.getId());// 通过 self 走代理}}能用,但自我注入比较绕,容易让后来维护的人看懵。
方案三:AopContext.currentProxy()
((UserService)AopContext.currentProxy()).addPoints(user.getId());需要先开启@EnableAspectJAutoProxy(exposeProxy = true),侵入性强,不推荐。
四、@Transactional 失效的 4 大场景
| 场景 | 原因 | 解决 |
|---|---|---|
| 同类自调用 | this调用绕过代理 | 拆 Bean 或注入自己 |
| 方法非 public | 代理只能拦截 public 方法 | 改为 public |
| 异常被 catch 吞掉 | 异常没抛出,代理感知不到 | 不要吞异常,或手动回滚 |
| 受检异常 | 默认只对 RuntimeException 回滚 | 配置 rollbackFor,或统一抛运行时异常 |
五、总结
@Transactional生效的本质是「通过代理对象调用」。一旦绕过代理——最典型的就是同类this自调用——事务就静默失效。排查事务失效时,先确认调用是否经过了代理,再看方法可见性、异常类型和传播行为。
我是无羡(小剑),全栈偏后端的独立开发者。
作品集:无羡 · 独立开发者作品集
如果对你有帮助,欢迎点赞、收藏、关注。