java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring事务失效解决

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

作者: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 锁来控制并发。

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

为了同时满足“锁包事务”和“事务生效”两个条件,我们必须在 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 代理对象在调用目标方法后,会检查方法执行过程中是否抛出了异常。

解决方案

  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事务失效解决内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文