spring事务失效的几种业务场景

Spring 事务失效的核心原因是 AOP 动态代理未触发事务配置不合法异常未被感知底层不支持事务。以下是最常见的失效场景:

一、最常见:内部方法调用(同一类内无代理转发)

场景描述

同一个类中,无事务的方法 A 调用有事务的方法 B,此时方法 B 的事务会失效。

失效原因

Spring 事务基于动态代理(JDK/CGLIB),只有通过 代理对象 调用目标方法时,AOP 切面才会触发事务逻辑。而同一类内的方法调用是 this.方法()(目标对象自身调用),跳过了代理对象,事务切面无法介入。

示例代码

typescript 复制代码
@Service
public class UserService {

    // 无事务方法
    public void updateUserWithoutTx() {
        // 内部调用有事务方法(this 调用,无代理)
        this.updateUserWithTx(); // 事务失效
    }

    // 有事务方法
    @Transactional
    public void updateUserWithTx() {
        // 数据库操作(无事务支持)
        userMapper.update();
    }
}

解决方案

  1. 拆分方法到不同类(通过依赖注入调用代理对象);
  2. 自注入代理对象(@Autowired private UserService thisProxy;,需开启 exposeProxy = true);
  3. 通过 AopContext.currentProxy() 获取代理对象调用。

二、方法访问权限非 public

场景描述

@Transactional 注解加在 非 public 方法(private/protected/default)上。

失效原因

Spring 事务拦截器(TransactionInterceptor)的源码中,会判断目标方法的访问权限:仅 public 方法会触发事务 。非 public 方法的 Method.getModifiers() 不满足 Modifier.isPublic(modifiers),事务切面直接跳过。

示例代码

typescript 复制代码
@Service
public class UserService {

    // 错误:private 方法加事务(失效)
    @Transactional
    private void updateUser() {
        userMapper.update();
    }
}

解决方案

将事务方法改为 public 访问权限。

三、异常被手动捕获(未抛出到代理层)

场景描述

事务方法内部捕获了异常(try-catch),且未重新抛出异常。

失效原因

Spring 事务默认通过 感知未捕获的异常 来触发回滚(RuntimeException/Error)。如果异常被手动捕获且未抛出,代理层无法感知异常,会认为方法执行成功,直接提交事务。

示例代码

java 复制代码
@Service
public class UserService {

    @Transactional
    public void updateUser() {
        try {
            userMapper.update();
            int i = 1 / 0; // 抛出 RuntimeException
        } catch (Exception e) {
            // 错误:捕获异常但未抛出,事务无法感知
            log.error("更新失败", e);
        }
    }
}

解决方案

  1. 捕获后重新抛出异常(throw new RuntimeException(e));
  2. 手动触发回滚(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly())。

四、抛出非检查异常(默认不回滚的异常)

场景描述

事务方法抛出 检查异常 (如 IOExceptionSQLException),且未通过 rollbackFor 配置指定回滚异常。

失效原因

Spring 事务默认只回滚 RuntimeExceptionError(非检查异常)。对于检查异常(编译时必须捕获的异常),默认不会触发回滚,事务会正常提交。

示例代码

java 复制代码
@Service
public class UserService {

    // 错误:抛出 IOException(检查异常),默认不回滚
    @Transactional
    public void updateUser() throws IOException {
        userMapper.update();
        throw new IOException("文件读取失败");
    }
}

解决方案

通过 rollbackFor 显式指定回滚异常:

java 复制代码
@Transactional(rollbackFor = Exception.class) // 捕获所有异常回滚
public void updateUser() throws IOException {
    // ...
}

五、目标类 / 方法为 final(无法生成代理)

场景描述

  1. 事务所在类是 final 修饰的;
  2. 事务方法是 final 修饰的。

失效原因

Spring 动态代理的两种实现:

  • JDK 动态代理:基于接口,目标类需实现接口(final 类无法被代理);
  • CGLIB 动态代理:基于类继承,final 类无法被继承,final 方法无法被重写。

无论哪种方式,final 修饰都会导致代理对象无法生成或无法重写目标方法,事务切面无法切入。

示例代码

java 复制代码
// 错误:final 类(无法生成 CGLIB 代理)
@Service
public final class UserService {

    // 错误:final 方法(无法被 CGLIB 重写)
    @Transactional
    public final void updateUser() {
        userMapper.update();
    }
}

解决方案

移除 final 修饰符(类和事务方法均不能为 final)。

六、Bean 未被 Spring 容器管理

场景描述

事务所在的类未通过 @Service@Component 等注解交给 Spring 管理,而是手动 new 出来的对象。

失效原因

Spring 事务依赖容器生成的 代理 Bean ,手动 new 的对象是原始目标对象,未被 Spring 增强(无代理),自然没有事务支持。

示例代码

java 复制代码
// 错误:未加 @Service,未被 Spring 管理
public class UserService {

    @Transactional
    public void updateUser() {
        userMapper.update();
    }
}

// 调用时手动 new(无事务)
UserService userService = new UserService();
userService.updateUser();

解决方案

  1. 给类加 @Service 注解,通过 @Autowired 注入使用;
  2. 确保对象是从 Spring 容器中获取的(而非手动实例化)。

七、事务管理器配置错误 / 缺失

场景描述

  1. 未配置 PlatformTransactionManager Bean;
  2. 多数据源时,事务管理器指定错误;
  3. 未开启事务支持(非 Spring Boot 项目)。

失效原因

Spring 事务需要 PlatformTransactionManager(如 DataSourceTransactionManager)来管理事务生命周期(开启 / 提交 / 回滚)。无配置或配置错误时,事务注解无法生效。

常见错误场景

  1. 非 Spring Boot 项目未加 @EnableTransactionManagement 注解;
  2. 多数据源时,未通过 @Transactional(transactionManager = "xxxTxManager") 指定对应的事务管理器;
  3. 数据源未注入事务管理器(如 DataSourceTransactionManager 未传入 dataSource)。

解决方案

  1. Spring Boot 项目:引入 spring-boot-starter-jdbcspring-boot-starter-data-jpa,自动配置 DataSourceTransactionManager
  2. 非 Spring Boot 项目:添加 @EnableTransactionManagement,并手动配置事务管理器:
less 复制代码
@Configuration
@EnableTransactionManagement
public class TxConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

八、事务传播机制配置不当

场景描述

事务方法的传播机制配置为 不支持事务 的类型,如 NOT_SUPPORTEDNEVER

失效原因

Spring 事务传播机制定义了方法间调用的事务行为,以下传播机制会导致事务失效:

  • Propagation.NOT_SUPPORTED:以非事务方式执行,若当前存在事务则挂起;
  • Propagation.NEVER:以非事务方式执行,若当前存在事务则抛出异常;
  • Propagation.SUPPORTS:若当前无事务,则以非事务方式执行(仅依赖外层事务)。

示例代码

java 复制代码
@Service
public class UserService {

    // 错误:传播机制为 NOT_SUPPORTED(不支持事务)
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateUser() {
        userMapper.update();
    }
}

解决方案

使用支持事务的传播机制(默认 Propagation.REQUIRED 即可):

java 复制代码
@Transactional(propagation = Propagation.REQUIRED) // 默认值,无需显式配置
public void updateUser() {
    // ...
}

九、数据库本身不支持事务

场景描述

使用的数据库 / 表引擎不支持事务(如 MySQL 的 MyISAM 引擎)。

失效原因

Spring 事务是 基于数据库事务的上层封装,若底层数据库不支持事务,即使 Spring 配置正确,也无法实现事务特性。

常见情况

  • MySQL 表引擎为 MyISAM(默认 InnoDB 支持事务);
  • 部分内存数据库(如 H2 非事务模式)。

解决方案

将数据库表引擎改为支持事务的类型(如 MySQL 改为 InnoDB)。

十、多线程调用(事务上下文不共享)

场景描述

事务方法 A 中启动新线程,调用事务方法 B,方法 B 的事务与 A 独立,且 A 的异常不会导致 B 回滚。

失效原因

Spring 事务上下文(TransactionSynchronizationManager)是 线程局部变量(ThreadLocal) ,线程间不共享。新线程中调用的事务方法会创建独立的事务,与主线程事务无关。

示例代码

typescript 复制代码
@Service
public class UserService {

    @Transactional
    public void updateUserA() {
        userMapper.updateA();
        // 新线程调用事务方法 B
        new Thread(() -> updateUserB()).start();
        int i = 1 / 0; // 主线程抛出异常,A 回滚,但 B 已提交
    }

    @Transactional
    public void updateUserB() {
        userMapper.updateB();
    }
}

若需多线程事务一致性,需使用 分布式事务框架(如 Seata、Atomikos),或通过消息队列实现最终一致性。

总结:事务失效的核心排查思路

  1. 检查方法是否是 public,类 / 方法是否为 final
  2. 检查是否是同一类内的内部调用(this 调用);
  3. 检查异常是否被捕获未抛出,或是否是未配置 rollbackFor 的检查异常;
  4. 检查 Bean 是否被 Spring 管理(是否有 @Service 等注解,是否通过 @Autowired 注入);
  5. 检查事务管理器是否配置正确(是否有 PlatformTransactionManager Bean,多数据源是否指定事务管理器);
  6. 检查数据库 / 表是否支持事务(如 MySQL 引擎是否为 InnoDB)。
相关推荐
Lee川6 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川9 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i11 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有12 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有12 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫13 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫13 小时前
Handler基本概念
面试
Wect13 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼14 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼14 小时前
Next.js 企业级落地
前端·javascript·面试