Spring事务失效的本质是 Spring的声明式事务管理(@Transactional)基于AOP(动态代理)实现,当你的调用方式破坏了它的代理机制时,事务就不会生效。

核心原理回顾

首先,要理解失效,必须知道它如何工作。当你在一个方法上标注 @Transactional 时,Spring会为该Bean创建一个代理对象。当你调用这个代理对象的方法时,代理会在方法执行前开启事务,在方法执行后提交或回滚事务。如果你绕过了这个代理对象,直接调用真实对象的方法,事务增强就不会发生。


事务失效的常见场景

1. 事务方法非Public修饰(最常见陷阱之一)

  • 原因: Spring的AOP代理(无论是JDK动态代理还是CGLIB)默认情况下只能对公共方法(public) 进行代理。这是由Spring的AbstractFallbackTransactionAttributeSource类决定的,它在计算事务属性时,非public方法会返回null,导致不会为该方法创建代理事务逻辑。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Service
    public class UserService {
    @Transactional
    private void createUserPrivate(User user) { // 错误!private方法
    userDao.insert(user);
    // 其他操作...
    }

    @Transactional
    protected void updateUserProtected(User user) { // 错误!protected方法
    userDao.update(user);
    }
    }
  • 解决方案: 确保所有@Transactional注解的方法都是public的。

2. 自调用(Invocations Within the Same Class)

  • 原因: 这是最隐蔽和常见的陷阱。在一个Bean的内部,方法A调用带有@Transactional注解的方法B,此时调用的this.methodB()是真实对象的方法,而不是代理对象的方法,因此事务注解不会生效。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Service
    public class OrderService {

    public void placeOrder(Order order) {
    // ... 一些业务逻辑
    this.updateOrderStatus(order); // 自调用,事务失效!
    // 这里的 this 是真实对象,不是代理对象
    }

    @Transactional
    public void updateOrderStatus(Order order) {
    // 更新订单状态
    orderDao.update(order);
    }
    }
  • 解决方案:
    1. (推荐)将方法B抽取到另一个Bean中: 这样通过注入的Bean调用,必然是代理对象。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Service
      public class OrderService {
      @Autowired
      private TransactionalService transactionalService; // 注入另一个Bean

      public void placeOrder(Order order) {
      // ... 一些业务逻辑
      transactionalService.updateOrderStatus(order); // 通过代理调用,事务有效
      }
      }

      @Service
      public class TransactionalService {
      @Transactional
      public void updateOrderStatus(Order order) {
      // ...
      }
      }
    2. 通过AopContext获取当前代理对象(不推荐,需要额外配置):
      1
      2
      3
      4
      5
      // 首先在配置中开启暴露代理 @EnableAspectJAutoProxy(exposeProxy = true)
      public void placeOrder(Order order) {
      // ...
      ((OrderService) AopContext.currentProxy()).updateOrderStatus(order);
      }

3. 异常类型不正确或被捕获

  • 原因: @Transactional 默认只在抛出未检查的异常(即RuntimeException及其子类)和Error时才会回滚。如果抛出了已检查异常(如ExceptionIOException等),事务默认会提交。
    更糟糕的是,如果你在方法内直接try-catch了异常并且没有重新抛出,那么代理对象根本看不到任何异常,会认为方法执行成功,从而提交事务。
  • 示例1 - 抛出检查异常:
    1
    2
    3
    4
    5
    6
    7
    @Transactional
    public void createUser(User user) throws Exception {
    userDao.insert(user);
    if (someCondition) {
    throw new Exception("这是一个检查异常"); // 事务会提交,不会回滚!
    }
    }
  • 示例2 - 异常被吞掉:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Transactional
    public void createUser(User user) {
    try {
    userDao.insert(user);
    someOperationThatMightFail(); // 这里可能抛出RuntimeException
    } catch (Exception e) {
    // 捕获了异常,没有重新抛出!
    log.error("Error occurred", e);
    // 代理认为方法正常执行,会提交事务
    }
    }
  • 解决方案:
    1. 修改回滚的异常类型: 使用 @Transactional(rollbackFor = Exception.class),指定在遇到任何Exception时都回滚。
    2. 正确处理异常: 在catch块中,如果需要回滚,必须手动抛出异常(通常是抛出一个RuntimeException)。
      1
      2
      3
      4
      catch (Exception e) {
      log.error("Error occurred", e);
      throw new RuntimeException(e); // 或者自定义的运行时异常
      }

4. 数据库引擎不支持事务

  • 原因: 如果你使用的是MySQL,并且数据表使用的存储引擎是MyISAM,那么事务功能是无效的,因为MyISAM本身就不支持事务。
  • 解决方案: 将数据库表的存储引擎改为InnoDB

5. 错误的传播特性(Propagation)

  • 原因: propagation属性配置不当。例如:
    • @Transactional(propagation = Propagation.NOT_SUPPORTED): 表示以非事务方式运行,挂起当前事务。
    • @Transactional(propagation = Propagation.NEVER): 表示不能存在事务,否则抛出异常。
    • 在一个没有事务的方法中,调用 Propagation.MANDATORY(强制要求存在事务)的方法,会抛出异常。
  • 解决方案: 根据业务逻辑正确配置传播行为。最常用的是REQUIRED(默认)和REQUIRES_NEW

6. 未被Spring管理的Bean(即没有使用Spring注解)

  • 原因: 你在一个普通的、没有被@Component, @Service, @Controller等注解的类上使用@Transactional,Spring根本不知道它的存在,自然不会为它创建代理。
  • 示例:
    1
    2
    3
    4
    public class CommonService { // 缺少 @Service 等注解
    @Transactional // 无效
    public void save() { ... }
    }
  • 解决方案: 确保你的类是一个Spring Bean。

7. 多数据源配置中,事务管理器配置错误

  • 原因: 在多个数据源的项目中,你可能配置了多个PlatformTransactionManager Bean。如果你在使用@Transactional时没有指定使用哪个事务管理器,Spring会使用默认的(按类型注入,如果找到多个就会出问题)。
  • 解决方案: 在使用@Transactional注解时,通过valuetransactionManager属性明确指定使用哪个事务管理器Bean的名字。
    1
    2
    @Transactional("orderTransactionManager") // 指定使用名为orderTransactionManager的事务管理器
    public void processOrder() { ... }

总结与排查 checklist

当遇到事务失效时,可以按以下顺序排查:

  1. 看方法是否是public
  2. 看是不是自调用(同一个类中方法A调方法B)。
  3. 看异常是否被正确抛出(是不是检查异常?是不是被catch吞掉了?)。
  4. 看Bean是否被Spring管理(有没有@Service等注解)。
  5. 看数据库引擎(是否是InnoDB)。
  6. 看传播特性配置(是不是配了NOT_SUPPORTED, NEVER等)。
  7. 看多数据源配置(是否指定了正确的事务管理器)。