欢迎光临
我们一直在努力

【Spring】声明式 @Transactional 注解失效的 8 大原因及避坑指南


文章目录

    • 一、引言
    • 二、前置知识:Spring 事务原理速览
    • 三、八大失效场景逐一拆解
      • 3.1 场景一:数据库引擎不支持事务
      • 3.2 场景二:类内部方法调用(最常见!⭐⭐⭐⭐⭐)
      • 3.3 场景三:非 public 方法
      • 3.4 场景四:异常类型不匹配
      • 3.5 场景五:异常被 catch 吞掉(常见!⭐⭐⭐⭐)
      • 3.6 场景六:多线程环境
      • 3.7 场景七:事务传播行为配置错误
      • 3.8 场景八:非 Spring 管理的 Bean 中使用
    • 四、快速排查指南
    • 五、最佳实践总结
      • 推荐的事务注解模板
      • 代码组织建议
      • 单元测试中验证事务
    • 六、总结
    • 参考

在这里插入图片描述

一、引言

在 Spring Boot 项目中使用事务非常简单——加个 @Transactional 就搞定。但"简单"往往意味着"容易出错"。在实际项目中,事务不回滚的 bug 屡见不鲜,究其原因,大多数是对 Spring 事务的底层实现机制理解不够。

Spring 声明式事务的核心依赖是 AOP 动态代理。你用 @Transactional 标记的方法,Spring 会为它生成一个代理对象,在方法执行前后自动开启和提交/回滚事务。理解了这个前提,就能推导出一个关键结论:

只有通过 Spring 代理对象调用的方法,事务注解才会生效。

本文将从这一核心原理出发,梳理 8 大常见失效场景,每个场景附带可运行的代码示例和正确的修复方式。


二、前置知识:Spring 事务原理速览

在深入失效场景之前,先用一张图理解 Spring 事务的工作链路:

核心流程如下:

  • Spring 启动时扫描 @Transactional 注解,为对应 Bean 生成代理对象(JDK 动态代理或 CGLIB 代理)
  • 调用方拿到的是代理对象,而不是原始对象
  • 代理对象调用 TransactionInterceptor,它从 PlatformTransactionManager 获取数据库连接,开启事务
  • 执行目标方法
  • 根据方法执行结果(是否抛出异常)决定 commit 还是 rollback
  • 事务状态(Connection)绑定在 ThreadLocal 上,这解释了为什么多线程场景下事务会失效。

    了解这个流程后,下面逐一剖析失效场景。


    三、八大失效场景逐一拆解

    3.1 场景一:数据库引擎不支持事务

    这是最容易被忽略的"低级错误"。MySQL 的 MyISAM 引擎不支持事务,如果你的表是 MyISAM,加再多 @Transactional 也无济于事。

    // 错误示例:表使用 MyISAM 引擎
    // CREATE TABLE user (id BIGINT, name VARCHAR(50)) ENGINE=MyISAM;

    @Service
    public class UserService {

    @Transactional
    public void createUser() {
    userMapper.insert(new User(1L, "张三")); // 执行成功
    int i = 1 / 0; // 抛出异常
    // 预期回滚,但 MyISAM 不支持事务,第一条数据已持久化!
    }
    }

    排查方式:执行 SHOW TABLE STATUS FROM 数据库名 查看 Engine 列,确认是 InnoDB。

    修复方式:将表引擎改为 InnoDB。

    ALTER TABLE user ENGINE = InnoDB;


    3.2 场景二:类内部方法调用(最常见!⭐⭐⭐⭐⭐)

    这是实际项目中出现频率最高的事务失效场景。直接看代码:

    @Service
    public class OrderService {

    // ❌ 错误写法:外部调用 createOrder(),事务不生效
    public void createOrder(Order order) {
    // 非事务方法直接调用同类中的事务方法
    this.saveOrder(order); // ← this 调用,绕过了代理!
    sendNotify(order);
    }

    @Transactional
    public void saveOrder(Order order) {
    orderMapper.insert(order);
    // 假设这里抛异常,数据不会回滚!
    }

    private void sendNotify(Order order) {
    // 发送通知…
    }
    }

    原理分析:外部调用 orderService.createOrder() 时,拿到的是 CGLIB 代理对象。createOrder() 没有事务注解,代理对象直接调用目标对象的方法。而目标对象内部的 this.saveOrder() 是 Java 原生的方法调用,它根本不知道代理对象的存在,@Transactional 自然不生效。

    • 自调用机制:当类中的方法调用同一个类中的另一个@Transactional 方法时,事务可能不会生效。这是因为事务注解是通过 AOP 实现的,而 Spring 的 AOP 代理机制在这种情况下不会被触发。

    三种正确的修复方式:

    // ✅ 方式一:调用方也标记 @Transactional(推荐,最简单)
    @Service
    public class OrderService {

    @Transactional // 让外层方法也加入事务管理
    public void createOrder(Order order) {
    saveOrder(order); // 此时事务已在外层开启,内层复用同一事务
    sendNotify(order);
    }

    public void saveOrder(Order order) {
    orderMapper.insert(order);
    // 异常会回滚,因为事务在外层已开启
    }

    private void sendNotify(Order order) { /* … */ }
    }

    // ✅ 方式二:将事务方法提取到独立的 Service(推荐,职责清晰)
    @Service
    public class OrderService {

    @Autowired
    private OrderPersistenceService persistenceService; // 注入另一个 Bean

    public void createOrder(Order order) {
    persistenceService.saveOrder(order); // ← 跨 Bean 调用,走代理,事务生效
    sendNotify(order);
    }
    }

    @Service
    public class OrderPersistenceService {

    @Transactional
    public void saveOrder(Order order) {
    orderMapper.insert(order);
    // 异常会回滚 ✓
    }
    }

    // ✅ 方式三:通过 AopContext.currentProxy() 获取代理对象
    @Service
    public class OrderService {

    public void createOrder(Order order) {
    // 拿到当前类的代理对象,再调用事务方法
    ((OrderService) AopContext.currentProxy()).saveOrder(order);
    sendNotify(order);
    }

    @Transactional
    public void saveOrder(Order order) {
    orderMapper.insert(order);
    }
    }

    // 需要在启动类或配置类中开启 exposeProxy:
    // @EnableAspectJAutoProxy(exposeProxy = true)

    建议:优先使用方法二(提取独立 Service),职责单一、可测试性强。方法一简单但会让外层方法变重,适合逻辑不复杂的场景。


    3.3 场景三:非 public 方法

    Spring 事务注解默认只对 public 方法生效。如果把 @Transactional 放在 private 或 protected 方法上,不会有任何事务。

    @Service
    public class UserService {

    // ❌ 错误:private 方法上的事务不生效
    @Transactional
    private void savePrivate(User user) {
    userMapper.insert(user);
    }

    // ❌ 错误:protected 方法上的事务默认也不生效
    @Transactional
    protected void saveProtected(User user) {
    userMapper.insert(user);
    }

    // ✅ 正确:只能放在 public 方法上
    @Transactional
    public void save(User user) {
    userMapper.insert(user);
    }
    }

    源码依据:Spring 的 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute() 方法中有这样一段逻辑:

    // org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource
    protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
    // 如果方法不是 public 的,且 allowPublicMethodsOnly() 返回 true(默认为 true)
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null; // ← 直接返回 null,不应用事务
    }
    // …
    }

    Spring 团队的设计理由是:非 public 方法不应该暴露为事务边界。如果你确实有特殊需求,可以自定义 TransactionAttributeSource,但强烈不推荐。


    3.4 场景四:异常类型不匹配

    @Transactional 默认只对 RuntimeException 和 Error 进行回滚。当你抛出一个 checked exception(如 IOException、自定义受检异常)时,事务不会回滚。

    @Service
    public class UserService {

    // ❌ 错误:抛出 checked exception,事务不会回滚
    @Transactional
    public void batchImport(InputStream input) throws IOException {
    userMapper.insert(new User(1L, "张三"));
    // 模拟读取异常
    if (input == null) {
    throw new IOException("文件读取失败"); // ← checked exception,默认不回滚!
    }
    }

    // ✅ 正确:显式指定 rollbackFor
    @Transactional(rollbackFor = Exception.class) // 所有异常都回滚
    public void batchImportCorrect(InputStream input) throws IOException {
    userMapper.insert(new User(2L, "李四"));
    if (input == null) {
    throw new IOException("文件读取失败"); // 这次会回滚 ✓
    }
    }

    // ✅ 更精确的写法:只回滚你关心的特定异常
    @Transactional(rollbackFor = {IOException.class, BusinessException.class})
    public void batchImportPrecise(InputStream input) throws IOException {
    // …
    }
    }

    最佳实践:项目中统一使用 @Transactional(rollbackFor = Exception.class),或者在全局配置中设置默认回滚策略,避免遗漏。


    3.5 场景五:异常被 catch 吞掉(常见!⭐⭐⭐⭐)

    这是仅次于"内部方法调用"的高频失效场景。你把异常 catch 住了但没重新抛出,事务管理器根本不知道发生了异常,自然就不会回滚。

    @Service
    public class OrderService {

    // ❌ 错误:catch 后吞掉异常,事务不回滚
    @Transactional
    public void createOrder(Order order) {
    try {
    orderMapper.insert(order);
    int i = 1 / 0; // 抛出 ArithmeticException
    // 其他业务逻辑…
    } catch (Exception e) {
    log.error("创建订单失败", e);
    // ← 异常被吞了,方法正常返回,Spring 提交事务!
    }
    }
    }

    为什么会这样:Spring 的事务拦截器在目标方法执行后,检查是否有未捕获的异常从方法中抛出。try-catch 兜底后异常不再往外抛,拦截器认为方法正常结束,于是 commit。

    三种正确的修复方式:

    @Slf4j
    @Service
    public class OrderService {

    // ✅ 方式一:catch 之后重新抛出(运行时异常)
    @Transactional
    public void createOrderV1(Order order) {
    try {
    orderMapper.insert(order);
    int i = 1 / 0;
    } catch (Exception e) {
    log.error("创建订单失败", e);
    throw new RuntimeException("创建订单失败", e); // 重新抛出
    }
    }

    // ✅ 方式二:手动标记回滚(适合需要返回错误码而非抛异常的场景)
    @Transactional
    public void createOrderV2(Order order) {
    try {
    orderMapper.insert(order);
    int i = 1 / 0;
    } catch (Exception e) {
    log.error("创建订单失败", e);
    // 手动标记当前事务需要回滚
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    // 此时可以返回业务错误码,不需要抛异常
    }
    }

    // ✅ 方式三:精确 catch,只处理能处理的异常(推荐)
    @Transactional
    public void createOrderV3(Order order) {
    try {
    orderMapper.insert(order);
    // 调用外部服务,允许失败
    remoteNotifyService.send(order);
    } catch (RemoteNotifyException e) {
    // 通知服务失败不应该回滚订单事务
    log.warn("通知发送失败,但订单正常创建", e);
    // 不重新抛出 = 通知失败不影响订单持久化
    }
    // RuntimeException 不要 catch,让它自然抛出触发回滚
    }
    }

    核心原则:只有你明确知道某个异常不应该导致事务回滚时,才 catch 它。如果你不确定,不要 catch,让它自然抛到代理层。


    3.6 场景六:多线程环境

    @Transactional 的事务状态通过 ThreadLocal 绑定到当前线程。当你开启新线程执行数据库操作时,新线程里没有事务上下文,自然也享受不到事务保护。

    @Service
    public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    // ❌ 错误:新线程中的方法没有事务
    @Transactional
    public void createOrderAsync(Order order) {
    orderMapper.insert(order); // 这条在当前线程的事务中

    // 在子线程中执行数据库操作——事务不传递!
    new Thread(() -> {
    orderMapper.insertDetail(order.getDetail()); // ← 子线程,事务不生效!
    }).start();

    // 模拟主线程异常
    int i = 1 / 0;
    // 结果:order 会回滚,但 orderDetail 已经写入了(在子线程的独立连接中)
    }
    }

    为什么会这样:主线程的事务是把数据库连接(Connection)存在 ThreadLocal 中。子线程是一个全新的 Thread,它的 ThreadLocal 是空的,所以 orderMapper.insertDetail() 会从连接池拿一个新的 Connection,以非事务模式(auto-commit)执行。即使主线程回滚了,子线程写入的数据早已持久化。

    更隐蔽的变体(使用 @Async):

    @Slf4j
    @Service
    public class OrderService {

    @Autowired
    private OrderDetailService detailService;

    @Transactional
    public void createOrder(Order order) {
    orderMapper.insert(order);
    // @Async 方法在独立线程池中执行,事务不传递!
    detailService.saveDetailAsync(order.getDetail());

    int i = 1 / 0;
    // 同样:order 回滚,detail 不回滚
    }
    }

    @Service
    class OrderDetailService {
    @Async // ← 异步 = 新线程,事务不传递
    @Transactional // ← 这个事务在新的独立线程中,与主线程事务无关
    public void saveDetailAsync(OrderDetail detail) {
    detailMapper.insert(detail);
    }
    }

    正确做法:

    // ✅ 方式一:如果需要保证原子性,不要用多线程
    @Transactional
    public void createOrder(Order order) {
    orderMapper.insert(order);
    orderMapper.insertDetail(order.getDetail()); // 同步执行,共享同一事务
    // 两条数据同时成功或同时回滚 ✓
    }

    // ✅ 方式二:使用分布式事务方案(Seata、RocketMQ 事务消息等)
    // ✅ 方式三:改成最终一致性方案——主事务提交后,通过消息队列异步处理
    @Transactional
    public void createOrder(Order order) {
    orderMapper.insert(order);
    orderMapper.insertDetail(order.getDetail());
    // 事务提交成功后再发消息
    // TransactionSynchronizationManager.registerSynchronization(…)
    }


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

    Spring 提供了 7 种传播行为(Propagation),配置不当会让事务"看起来失效"。

    @Service
    public class UserService {

    @Autowired
    private LogService logService;

    // ❌ 错误:外部事务存在时,logService 挂起事务再执行,插入的日志不回滚
    @Transactional
    public void deleteUser(Long userId) {
    userMapper.delete(userId);
    logService.recordDeleteLog(userId); // 调用另一个事务方法

    int i = 1 / 0; // 异常!user 删除会回滚,但日志已写入
    }
    }

    @Service
    class LogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW) // ← 挂起当前事务,新建独立事务
    public void recordDeleteLog(Long userId) {
    logMapper.insert(new OperationLog(userId, "DELETE"));
    // 这个事务已提交,外部回滚不影响它
    }
    }

    这不是"失效",而是"设计如此"。Propagation.REQUIRES_NEW 的语义就是新建一个独立的物理事务。你需要根据业务需求选择合适的传播行为:

    传播行为含义适用场景
    REQUIRED(默认) 有事务则加入,没有则新建 绝大多数场景
    REQUIRES_NEW 总是新建事务,挂起当前事务 日志记录、审计等不应因业务回滚而撤销的操作
    NESTED 嵌套事务,内部回滚不影响外部 部分回滚时,需要保存点(savepoint)
    SUPPORTS 有事务就加入,没有就以非事务运行 查询为主的只读操作
    NOT_SUPPORTED 以非事务方式运行,挂起当前事务 不需要事务的操作
    MANDATORY 必须在事务中运行,否则抛异常 强制调用方提供事务
    NEVER 必须在非事务中运行,否则抛异常 强制无事务执行

    排查技巧:开启 Spring 事务日志,观察事务的创建和提交/回滚行为:

    # application.yml 或 application.properties
    logging:
    level:
    org.springframework.transaction.interceptor: DEBUG
    org.springframework.orm.jpa: DEBUG


    3.8 场景八:非 Spring 管理的 Bean 中使用

    @Transactional 只在 Spring 容器管理的 Bean 中生效。以下几种情况注解会"静默失效":

    // ❌ 情况1:直接 new 出来的对象
    public class SomeController {
    @Autowired
    private ApplicationContext context;

    public void handle() {
    // 直接 new,不在 Spring 容器中,@Transactional 无效
    OrderService service = new OrderService();
    service.createOrder(order); // 事务不生效!
    }
    }

    // ❌ 情况2:在 @Configuration 类的方法上加 @Transactional
    @Configuration
    public class AppConfig {
    @Transactional // ← 无效!配置类的 Bean 初始化方法不是业务调用入口
    public DataSource dataSource() {
    return new HikariDataSource();
    }
    }

    // ❌ 情况3:工具类的 static 方法
    public class OrderUtils {
    @Transactional // ← 无效!static 方法不能被 AOP 代理拦截
    public static void saveOrder(Order order) {
    // …
    }
    }

    根本原因:Spring AOP 代理只能拦截 Spring 容器中 Bean 的实例方法调用。new 出来的对象、static 方法、配置类的 @Bean 方法都不满足这个前提。


    四、快速排查指南

    遇到事务不生效时,按以下清单逐项自查:

    检查项排查命令 / 方法
    ① 数据库引擎 SHOW TABLE STATUS 确认 InnoDB
    ② 调用链路 是否跨 Bean 调用?是否用了 this.?
    ③ 方法修饰符 方法是否为 public?
    ④ 异常类型 是否 catch 吞了?抛的是否 RuntimeException?
    ⑤ 线程环境 是否涉及 new Thread() 或 @Async?
    ⑥ 传播行为 @Transactional 的 propagation 属性是否正确?
    ⑦ Bean 管理 类是否由 Spring 容器管理?是否是 new 出来的?
    ⑧ 事务日志 开启 DEBUG 日志观察事务行为

    最直接的诊断手段——开启日志:

    logging:
    level:
    org.springframework.transaction.interceptor: DEBUG

    你会看到类似这样的输出:

    Creating new transaction with name [com.example.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    Opened new Connection [conn1] for JDBC transaction
    Initiating transaction commit

    如果加了 @Transactional 但日志里完全没有 Creating new transaction,说明注解没生效,按清单排查。


    五、最佳实践总结

    推荐的事务注解模板

    @Service
    @Slf4j
    public class OrderService {

    /**
    * 标准事务方法模板
    * – rollbackFor = Exception.class:确保任何异常都回滚
    * – timeout:防止长事务锁表
    * – readOnly = true:只读操作标记,提升性能
    */

    @Transactional(
    rollbackFor = Exception.class,
    timeout = 30,
    readOnly = true)
    public Order getOrder(Long orderId) {
    return orderMapper.selectById(orderId);
    }

    @Transactional(
    rollbackFor = Exception.class,
    timeout = 30)
    public void createOrder(Order order) {
    // 写操作…
    }
    }

    代码组织建议

  • Service 方法要么全有事务,要么全没有。不要在同一个 Service 里混合 @Transactional 和非事务的 public 方法,避免"内部调用"陷阱。
  • 事务边界放在 Service 层,不要在 Controller 层加 @Transactional(Controller 拿到的是已脱离事务上下文的代理)。
  • **写操作加 **rollbackFor = Exception.class,读操作加 readOnly = true。
  • 避免在事务方法中调用外部 HTTP/RPC 服务——外部调用耗时长,会拉长事务持有数据库连接的时间,可能导致连接池耗尽。
  • 只 catch 你能处理的异常,不确定的让它往上抛。
  • 单元测试中验证事务

    @SpringBootTest
    class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderMapper orderMapper;

    @Test
    @DisplayName("异常时应回滚订单数据")
    void shouldRollbackOnException() {
    // 使用 assertThrows 确认异常被抛出
    assertThrows(RuntimeException.class, () -> {
    orderService.createOrderWithException(); // 内部会抛异常
    });

    // 事务回滚后,数据库中应查不到数据
    List<Order> orders = orderMapper.selectAll();
    assertTrue(orders.isEmpty(), "事务应回滚,表应为空");
    }
    }


    六、总结

    Spring 声明式事务"失效"的根本原因只有一句话:事务的方法没有被 Spring 的代理对象拦截到。围绕这条主线,本文梳理了 8 个典型的失效场景:

  • 数据库引擎——MyISAM 天生不支持事务
  • 内部调用——this.method() 绕过了代理(最常见)
  • 非 public——Spring 默认忽略非公开方法的事务注解
  • 异常类型——checked exception 默认不回滚
  • 吞异常——catch 后没重新抛出,事务管理器无感知
  • 多线程——ThreadLocal 绑定的连接无法跨线程传递
  • 传播行为——REQUIRES_NEW 等配置导致"预期外"的独立事务
  • 非 Spring Bean——new 出来的对象、static 方法不在 AOP 管辖范围
  • 下次遇到事务不回滚的问题,对照这份清单逐一排查,配合 DEBUG 级别日志,通常几分钟就能定位到根因。

    参考

    • Spring 官方文档 – Transaction Management
    • Spring Transaction 源码分析(AbstractPlatformTransactionManager)
    • https://juejin.cn/post/7482620993283768361
    • https://www.cnblogs.com/haof31/p/19164274
    • Spring 声明式事务:原理、使用及失效场景详解_spring声明式事务-CSDN博客
    赞(0)
    未经允许不得转载:171主机测评 » 【Spring】声明式 @Transactional 注解失效的 8 大原因及避坑指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址