在使用 Spring 事务时,我们可能会遇到这样一个情况:
明明方法上加了
@Transactional
,也抛出了异常,为什么数据库却没有回滚?
我最近在实际开发中就踩到了这样一个坑。经过调试分析,发现事务没有生效的根源在于 —— 同类方法之间的直接调用跳过了 Spring 的代理机制。
这篇文章将还原这个问题的场景、分析原因,并给出最佳实践和通用解决方案。
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsQuery query, String localFilePath) {
// 1. 上传到 MinIO
boolean uploaded = addMediaFilesToMinIO(...);
// 2. 写入数据库
MediaFiles mediaFiles = addMediaFilesToDb(...); // 这个方法加了 @Transactional
// 3. 返回结果
return new UploadFileResultDto(...);
}
其中 addMediaFilesToDb(...)
方法添加了事务控制:
@Transactional
public MediaFiles addMediaFilesToDb(...) {
// 向数据库插入文件记录
}
按理说,如果上传后数据库操作出错,应该会回滚。但实际测试中,即使 addMediaFilesToDb(...)
抛出异常,数据依然写入数据库,没有回滚。
问题的本质是:Spring 的事务是基于 AOP 代理机制实现的。
Spring 默认使用基于代理的 AOP(JDK 动态代理或 CGLIB)。当你在方法 A 中直接调用同类的另一个方法 B,实际上是“内部调用”,并不是通过 Spring 容器为该 Bean 创建的代理实例去调用方法 B,因此 B 上的事务注解不会被触发。
只有通过代理对象(也就是 Spring 容器托管的 Bean 引用)去调用带 @Transactional
的方法,Spring 才会在方法 B 的“入口”织入事务切面,才能正常开启/提交/回滚事务。
当你直接使用 this.addMediaFilesToDb(...)
这种方式调用时,调用路径会跳过 Spring 创建的代理对象,从而导致 @Transactional
失效。
@Service
public class MyService {
// 直接 this.doTransactional() 不会触发事务
public void outer() {
this.doTransactional();
}
@Transactional
public void doTransactional() { … }
}
为了解决这个问题,我在该类中注入了它自己的代理对象:
@Autowired
private FileService currentMediaFileServiceProxy;
public UploadFileResultDto uploadFile(...) {
// ...
currentMediaFileServiceProxy.addMediaFilesToDb(...); // ✅通过代理对象调用,事务才能生效
}
这样,事务就能被正确触发了。
循环依赖风险
如果你在构造函数或字段注入时直接注入自己(@Autowired private MyService myServiceProxy;
),一般 Spring 会使用 CGLIB 把自己增强成代理类,所以破坏不了依赖注入。但如果你又在 @PostConstruct
里引用,还要注意不要触发循环初始化。通常把代理注入为字段就行。
维护成本与可读性
代码里既有 this.xxx()
,又有 myServiceProxy.xxx()
,容易让人混淆到底什么时候有事务、什么时候没事务。
虽然注入自身代理是一个可行的办法,但不是最优雅的方式。更推荐以下两种方案:
@Service
public class UploadService {
@Autowired
private MediaService mediaService;
public void uploadFile(...) {
// 网络操作
// ...
mediaService.addMediaFilesToDb(...); // 通过另一个 Bean 间接调用,事务有效
}
}
@Service
public class MediaService {
@Transactional
public void addMediaFilesToDb(...) {
// 事务生效
}
}
这样天然避免了自调用的问题,职责也更清晰。
前提是开启代理暴露:
@EnableAspectJAutoProxy(exposeProxy = true)
调用方式如下:
((FileService) AopContext.currentProxy()).addMediaFilesToDb(...);
虽然功能正确,但这种方式侵入性强,不推荐在大型项目中滥用。
AopContext.currentProxy()讲解可以看我下面这个文章
Spring Boot 中 AopContext.currentProxy() 的妙用:解决自调用切面失效问题-CSDN博客
Spring 事务基于 AOP 实现,必须通过代理对象调用方法才会触发事务切面。
同类内部方法调用(this.xx())不会触发事务。
@Transactional
默认只对 public 方法 生效。
抛出的是 unchecked 异常(RuntimeException) 才会自动回滚,checked 异常需显式声明 rollbackFor
。
长时间操作(如文件上传)不宜与事务混用,否则容易导致长事务阻塞。
提到“不想让网络文件传输占用数据库事务太长”。
最佳实践:应当尽量缩短事务范围,将“只涉及数据库操作”的逻辑单独用一个带 @Transactional
的方法,网络 I/O 完全在事务之外完成。
如果你把“网络传输”也包进事务,抛异常时就瞬间回滚,但事务会一直保持连接、锁等资源,容易导致死锁或资源浪费。
我现在的做法是:
uploadFile(...)
方法不加 @Transactional
只给 addMediaFilesToDb(...)
方法加 @Transactional
这样可以确保只有真正的数据库写操作在事务中,网络传输的时间不受事务影响,这样做是推荐做法。
想让事务生效,必须确保 被代理的方法是通过 Spring 容器拿到的代理对象调用的。
如果你在开发中遇到事务失效的问题,不妨检查一下是否是“自调用”导致的。通过注入自身代理或抽离为单独 Service,都能有效解决。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、评论交流 !你也可以把它分享给你身边正在踩 Spring 事务坑的朋友们