从一个点来详细说说事务失效的场景及解决办法

在使用 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(...) 抛出异常,数据依然写入数据库,没有回滚。

 二、原因分析:自调用导致事务失效

2.1为什么自调用会导致事务不生效

问题的本质是: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() { … }
}

2.2正确的调用方式:通过代理对象调用事务方法

 为了解决这个问题,我在该类中注入了它自己的代理对象:

@Autowired
private FileService currentMediaFileServiceProxy;

public UploadFileResultDto uploadFile(...) {
    // ...
    currentMediaFileServiceProxy.addMediaFilesToDb(...); // ✅通过代理对象调用,事务才能生效
}

这样,事务就能被正确触发了。

2.3这种做法要注意的几点

  • 循环依赖风险
    如果你在构造函数或字段注入时直接注入自己(@Autowired private MyService myServiceProxy;),一般 Spring 会使用 CGLIB 把自己增强成代理类,所以破坏不了依赖注入。但如果你又在 @PostConstruct 里引用,还要注意不要触发循环初始化。通常把代理注入为字段就行。

  • 维护成本与可读性
    代码里既有 this.xxx(),又有 myServiceProxy.xxx(),容易让人混淆到底什么时候有事务、什么时候没事务。

虽然注入自身代理是一个可行的办法,但不是最优雅的方式。更推荐以下两种方案:

三、推荐解决方案

3.1 方法 1:将数据库操作抽离到另一个 Service 中

@Service
public class UploadService {
    @Autowired
    private MediaService mediaService;

    public void uploadFile(...) {
        // 网络操作
        // ...
        mediaService.addMediaFilesToDb(...);  // 通过另一个 Bean 间接调用,事务有效
    }
}

@Service
public class MediaService {
    @Transactional
    public void addMediaFilesToDb(...) {
        // 事务生效
    }
}

这样天然避免了自调用的问题,职责也更清晰。

3.2 方法 2:使用 AopContext.currentProxy()

前提是开启代理暴露:

@EnableAspectJAutoProxy(exposeProxy = true)

调用方式如下:

((FileService) AopContext.currentProxy()).addMediaFilesToDb(...);

虽然功能正确,但这种方式侵入性强,不推荐在大型项目中滥用。

AopContext.currentProxy()讲解可以看我下面这个文章

Spring Boot 中 AopContext.currentProxy() 的妙用:解决自调用切面失效问题-CSDN博客

四、知识点回顾 

  • Spring 事务基于 AOP 实现,必须通过代理对象调用方法才会触发事务切面。

  • 同类内部方法调用(this.xx())不会触发事务

  • @Transactional 默认只对 public 方法 生效。

  • 抛出的是 unchecked 异常(RuntimeException) 才会自动回滚,checked 异常需显式声明 rollbackFor

  • 长时间操作(如文件上传)不宜与事务混用,否则容易导致长事务阻塞。

4.1 长事务问题

提到“不想让网络文件传输占用数据库事务太长”。

  • 最佳实践:应当尽量缩短事务范围,将“只涉及数据库操作”的逻辑单独用一个带 @Transactional 的方法,网络 I/O 完全在事务之外完成。

  • 如果你把“网络传输”也包进事务,抛异常时就瞬间回滚,但事务会一直保持连接、锁等资源,容易导致死锁或资源浪费。

我现在的做法是:

  • uploadFile(...) 方法不加 @Transactional

  • 只给 addMediaFilesToDb(...) 方法加 @Transactional
    这样可以确保只有真正的数据库写操作在事务中,网络传输的时间不受事务影响,这样做是推荐做法

五、总结

 想让事务生效,必须确保 被代理的方法是通过 Spring 容器拿到的代理对象调用的

如果你在开发中遇到事务失效的问题,不妨检查一下是否是“自调用”导致的。通过注入自身代理抽离为单独 Service,都能有效解决。

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、评论交流 !你也可以把它分享给你身边正在踩 Spring 事务坑的朋友们

你可能感兴趣的:(数据库,mysql,java,事务,Transactional,sql,aop)