Spring事务失效的6种常见典型场景分析与解决方案

 更新时间:2026年02月08日 11:30:38   作者:GRF久睡成瘾  
Spring事务失效的原因多种多样,常见的问题包括方法可见性、异常处理、传播行为配置、代理机制等,这篇文章主要介绍了Spring事务失效的6种常见典型场景分析与解决方案的相关资料,需要的朋友可以参考下

前言

在 Spring 应用程序开发中,声明式事务(@Transactional)是保证数据一致性的核心机制。然而,在实际应用中,开发者常遇到注解已添加但事务未回滚的情况。本文基于最近在领券业务中遇到的并发与事务冲突问题,深入分析 Spring 事务失效的六种典型场景,并提供相应的解决方案。

场景一:事务方法访问权限非 public

问题描述

@Transactional 注解应用于非 public 修饰的方法(如 protectedprivate 或包级私有方法)时,事务将失效。

示例代码

@Service
public class UserService {
    
    @Transactional
    protected void updateUser(User user) {
        // 业务逻辑
    }
}

原因分析

Spring 的声明式事务依赖于 AOP。Spring AOP 的默认实现(无论是 JDK 动态代理还是 CGLIB)通常要求目标方法必须是public,以便代理对象能够正确拦截并增强该方法。

此外,Spring 源码中的 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute 方法显式规定了仅处理 public 方法:

if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null; // 非 public 方法返回 null,不应用事务配置
}

解决方案

确保被 @Transactional 注解修饰的方法访问权限为 public

场景二:同一类内部方法自调用

这是开发中最易被忽视的失效原因。

问题描述

在一个没有事务注解的普通方法内部,直接调用同一类中被 @Transactional 注解修饰的方法,事务将失效。

示例代码

@Service
public class UserCouponServiceImpl implements IUserCouponService {
    
    // 入口方法,无事务注解,但在内部调用了事务方法
    @Override
    public void receiveCoupon(Long couponId) {
        // ... 前置校验
        
        // 【关键点】内部直接调用事务方法,事务失效
        this.saveUserCouponAndUpdateCoupon(coupon, userId);
    }
    
    @Transactional
    public void saveUserCouponAndUpdateCoupon(Coupon coupon, Long userId){
        // 数据库操作:扣减库存、保存记录
        couponMapper.plusIssueNum(couponId);
        save(userCoupon);
    }
}

原因分析:为什么this调用会导致事务失效?

Spring 的声明式事务是基于 AOP动态代理 实现的。

  1. 代理对象的拦截机制:当 Spring 容器启动时,会为使用了 @Transactional 的 Bean 创建一个代理对象。这个代理对象持有一个指向原始目标对象(Target,即 UserCouponServiceImpl 实例)的引用。
  2. 外部调用的流程:当 Controller 调用 userCouponService.receiveCoupon(...) 时,实际上是在调用代理对象的方法。代理对象会检查该方法是否有 @Transactional 注解。
    • 如果有,代理对象会在调用目标方法前开启事务,调用后提交事务。
    • 如果没有(如 receiveCoupon),代理对象会直接将请求转发给原始目标对象。
  3. 内部调用的陷阱:一旦进入原始目标对象的方法内部(如 receiveCoupon 执行中),代码执行流就已经脱离了代理对象的控制。此时,代码中直接调用的 saveUserCouponAndUpdateCoupon(...) 等同于 this.saveUserCouponAndUpdateCoupon(...)。这里的 this 指向的是原始目标对象本身,而不是代理对象。
  4. 结论:由于绕过了代理对象直接使用service对象,Spring 的事务拦截器无法介入,最终导致AOP实现的事务逻辑根本没有执行。

解决方案

方案 A:直接给入口方法添加事务注解(常规解法)

最简单的解决方法是给入口方法 receiveCoupon 也添加 @Transactional 注解。这样,事务在进入 receiveCoupon 时就已经开启,后续的调用都在同一个事务上下文中运行。

@Transactional // 简单粗暴,直接加事务
public void receiveCoupon(Long couponId) {
    saveUserCouponAndUpdateCoupon(coupon, userId);
}

但是,在并发场景下,这种方案往往不可行。

方案 B:强制使用代理对象调用(高并发场景推荐)

在我的领券业务中,为了防止超卖,我们需要使用 synchronized 锁来控制并发。

  • 如果使用方案 A:事务包裹了锁(Transaction 包含 synchronized)。
    • 执行顺序:开启事务 -> 加锁 -> 执行业务 -> 解锁 -> 提交事务
    • 风险:线程 A 解锁后,事务尚未提交。此时线程 B 获取锁并读取数据,读到的仍然是旧数据(因为线程 A 的事务还没提交),导致锁失效,发生超卖。
  • 为了解决锁失效:我们必须保证锁的范围大于事务(synchronized 包含 Transaction)。
    • 执行顺序:加锁 -> 开启事务 -> 执行业务 -> 提交事务 -> 解锁
    • 这就要求 receiveCoupon 方法不能加事务注解(它是加锁的地方),只有内部调用的 saveUserCouponAndUpdateCoupon 方法需要加事务。

这就回到了最初的问题:内部调用会导致事务失效。

为了同时满足“锁包事务”和“事务生效”两个条件,我们必须在 receiveCoupon 内部,手动获取当前的代理对象来调用事务方法,强行让调用逻辑重新经过 Spring AOP 的拦截器链。

实现步骤:

  1. 引入 AspectJ 依赖

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    
  2. 开启代理暴露(在启动类或配置类):

    @EnableAspectJAutoProxy(exposeProxy = true)
    
  3. 使用 AopContext 获取代理对象并调用
    参考 UserCouponServiceImpl.java 中的实现:

    public void receiveCoupon(Long couponId) {
        // ... 省略校验逻辑
        
        synchronized (userId.toString().intern()) {
            // 【核心代码】从 ThreadLocal 中获取当前 AOP 代理对象
            IUserCouponService proxy = (IUserCouponService) AopContext.currentProxy();
            
            // 通过代理对象调用,触发事务切面逻辑
            proxy.saveUserCouponAndUpdateCoupon(coupon, userId);
        }
    }
    

通过这种在service调用的方法使用代理对象调用的方式,我们既控制了事务的粒度(只包裹核心数据库操作),又避免了内部调用绕过代理机制导致的事务失效,完美解决了高并发下的数据一致性问题。

场景三:事务方法内部捕获异常且未抛出

问题描述

在事务方法内部使用 try-catch 块捕获了异常,且在 catch 块中未再次抛出异常,导致事务提交而非回滚。

示例代码

@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        try {
            insertOrder();
            reduceStock(); // 假设此处抛出异常
        } catch (Exception e) {
            e.printStackTrace();
            // 异常被吞噬,未向外抛出
        }
    }
}

原因分析

Spring AOP 代理对象在调用目标方法后,会检查方法执行过程中是否抛出了异常。

  • 如果捕获到异常,且异常类型符合回滚规则,则执行回滚。
  • 如果目标方法内部自行处理了异常(即 catch 后未抛出),代理对象将认为方法执行成功,从而提交事务。

解决方案

  1. 避免吞噬异常:在 catch 块处理完日志或其他逻辑后,务必将异常再次抛出。
  2. 手动标记回滚:如果业务逻辑要求不能抛出异常,则必须在 catch 块中手动标记事务状态为回滚:
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    

场景四:异常类型不匹配

问题描述

方法抛出了异常,但异常类型是检查型异常(Checked Exception,如 IOExceptionSQLException),而 @Transactional 使用了默认配置。

示例代码

@Service
public class OrderService {

    @Transactional // 使用默认配置
    public void createOrder() throws IOException {
        insertOrder();
        if (errorCondition) {
            throw new IOException("IO Error");
        }
    }
}

原因分析

Spring 的 @Transactional 注解默认配置的 rollbackFor 属性仅包含 RuntimeExceptionError。这意味着,对于所有继承自 Exception 但非 RuntimeException 的检查型异常,Spring 默认不会触发回滚。

解决方案

显式配置 rollbackFor 属性,建议指定为 Exception.class 以覆盖所有异常类型:

@Transactional(rollbackFor = Exception.class)

场景五:事务传播行为配置错误

问题描述

在嵌套事务场景中,内部方法的传播行为配置导致其事务独立于外部事务,从而破坏了整体原子性。

示例代码

@Service
public class OrderService {
    @Transactional
    public void createOrder(){
        insertOrder();
        try {
            stockService.reduceStock(); // 即使外部回滚,此方法可能已提交
        } catch (Exception e) {
            // ...
        }
        throw new RuntimeException("Error");
    }
}

@Service
public class StockService {
    // REQUIRES_NEW 开启独立事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reduceStock() {
        // ...
    }
}

原因分析

Propagation.REQUIRES_NEW 策略会挂起当前事务,并开启一个新的物理事务。即使外部调用方(createOrder)后续发生异常并回滚,reduceStock 方法的独立事务一旦提交,其数据变更将永久生效,导致数据不一致。

解决方案

根据业务一致性需求正确选择传播行为。对于大多数需要保证原子性的组合操作,应使用默认的 Propagation.REQUIRED,确保所有方法在同一个逻辑事务中运行。

场景六:类未被 Spring 容器管理

问题描述

调用 @Transactional 方法的对象实例并非由 Spring 容器创建和管理。

示例代码

// 缺少 @Service 或 @Component 注解
public class OrderService {
    @Transactional
    public void createOrder() {
        // ...
    }
}

或者:

OrderService service = new OrderService(); // 手动 new 实例
service.createOrder();

原因分析

Spring 的声明式事务完全依赖于 IoC 容器对 Bean 的生命周期管理和 AOP 代理生成。如果一个类没有被注册为 Spring Bean(缺少 @Service@Component 等注解),或者对象是通过 new 关键字手动实例化的,Spring 容器无法感知该对象,也就无法为其创建代理并织入事务切面逻辑。

解决方案

  1. 确保业务类上添加了 @Service@Component 等组件注解。
  2. 在其他组件中使用该类时,必须通过依赖注入(@Autowired 或构造器注入)获取实例,严禁手动实例化。

总结

Spring 事务失效问题通常源于对 Spring AOP 代理机制理解的偏差。在排查此类问题时,应重点关注以下三个维度:

  1. 代理机制:是否存在对象自调用、类是否被容器管理、方法可见性是否合规。
  2. 异常处理:异常是否被捕获吞噬、异常类型是否在回滚范围内。
  3. 事务配置:传播行为是否符合业务预期。

在我的项目领券业务场景中,我们为了兼顾并发控制(synchronized)和事务原子性,采用了手动获取代理对象(AopContext.currentProxy())的方案,有效解决了自调用导致的事务失效问题。

到此这篇关于Spring事务失效的6种常见典型场景分析与解决方案的文章就介绍到这了,更多相关Spring事务失效解决内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java 反射和动态代理详解及实例代码

    java 反射和动态代理详解及实例代码

    这篇文章主要介绍了java 反射和动态代理详解及实例代码的相关资料,需要的朋友可以参考下
    2016-09-09
  • Spring Security内存中认证的实现

    Spring Security内存中认证的实现

    本文主要介绍了Spring Security内存中认证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-11-11
  • Java中json使用方法_动力节点Java学院整理

    Java中json使用方法_动力节点Java学院整理

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式, json是个非常重要的数据结构,在web开发中应用十分广泛。下面通过本文给大家讲解Java中json使用方法,感兴趣的朋友一起看看吧
    2017-07-07
  • Java static 与 final关键字实例详解

    Java static 与 final关键字实例详解

    本文详细介绍了Java中的static和final关键字,包括它们的本质、内存分配、线程安全问题以及在类加载过程中的内存变化,通过举例和解释,感兴趣的朋友跟随小编一起看看吧
    2026-01-01
  • java实现word转pdf or直接生成pdf文件

    java实现word转pdf or直接生成pdf文件

    这篇文章主要介绍了java实现word转pdf or直接生成pdf文件方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • Eclipse项目出现红色叹号的解决方法

    Eclipse项目出现红色叹号的解决方法

    eclipse工程前面出现红色叹号都是由于eclipse项目、eclipse工程中,缺少了一些jar包等文件引起的,这篇文章主要给大家介绍了关于Eclipse项目出现红色叹号的解决方法,需要的朋友可以参考下
    2023-11-11
  • Jackson的用法实例分析

    Jackson的用法实例分析

    这篇文章主要介绍了Jackson的用法实例分析,用于处理Java的json格式数据非常实用,需要的朋友可以参考下
    2014-08-08
  • Java中Map接口使用以及有关集合的面试知识点汇总

    Java中Map接口使用以及有关集合的面试知识点汇总

    在java面试过程中,Map时常会被作为一个面试点来问,下面这篇文章主要给大家介绍了关于Java中Map接口使用以及有关集合的面试知识点汇总的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • Java 如何快速实现一个连接池

    Java 如何快速实现一个连接池

    有没有一个通用的库可以快速实现一个线程池呢?得益于 Java 完善的生态,前人们针对这种需要开发了一个通用库:Apache Commons Pool(下文简称 ACP)。本质上来说,ACP 库提供的是管理对象池的通用能力,当然也可以用来管理连接池了!
    2021-05-05
  • Java 如何实现POST(x-www-form-urlencoded)请求

    Java 如何实现POST(x-www-form-urlencoded)请求

    这篇文章主要介绍了Java 实现POST(x-www-form-urlencoded)请求,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10

最新评论