3. 事务失效场景:自调用问题、非public方法、异常类型不匹配、代理机制失效

在 Spring 声明式事务中,@Transactional 注解并非“万能开关”。开发环境中最常见的事务失效问题集中在四个核心场景:自调用、非 public 方法、异常类型不匹配、代理机制失效。理解这些场景的底层原理,是调试 Kafka 消息链路事务边界的前提。

3.1 四大失效场景全景对比

失效场景 根本原因 典型表现 影响范围
自调用 同一类中方法 A 调用方法 B,绕过代理对象 方法 B 的 @Transactional 不生效 高
非 public 方法 Spring AOP 默认只拦截 public 方法 private/protected 方法事务不开启 中
异常类型不匹配 默认只回滚 RuntimeException 和 Error checked 异常提交而非回滚 高
代理机制失效 类未通过 Spring 容器管理,或使用 final 类/方法 @Transactional 完全被忽略 极高

3.2 自调用问题:最隐蔽的陷阱

自调用是开发环境中最容易忽视的事务失效场景。当 Service 类内部方法 A(无事务)调用方法 B(有 @Transactional)时,调用的是 this.methodB(),即原始对象的方法,而非 Spring 生成的代理对象。代理对象上的事务拦截逻辑被完全跳过。

典型错误代码:
@Service
public class OrderService {

    public void processOrder(Order order) {
        // 自调用:this.saveOrder() 绕过代理
        saveOrder(order);
    }

    @Transactional
    public void saveOrder(Order order) {
        // 事务不会生效!
        orderRepository.save(order);
    }
}
解决方案:注入自身代理,或拆分到不同 Service 类。推荐使用 AopContext.currentProxy() 获取当前代理对象。
@Service
public class OrderService {

    @Autowired
    private OrderService self; // 注入自身代理

    public void processOrder(Order order) {
        self.saveOrder(order); // 通过代理调用
    }

    @Transactional
    public void saveOrder(Order order) {
        orderRepository.save(order);
    }
}
自调用 vs 代理调用流程对比 自调用(事务失效) processOrder() this.saveOrder() 直接调用原始对象方法 事务拦截器被跳过 数据库操作无事务保护 代理调用(事务生效) processOrder() self.saveOrder() 通过注入的代理对象调用 事务拦截器正常生效 开启事务 → 提交/回滚

3.3 非 public 方法:AOP 的默认限制

Spring AOP 基于 JDK 动态代理或 CGLIB 实现,默认只拦截 public 方法。将 @Transactional 放在 private、protected 或 default 方法上,事务不会生效,且不会抛出任何警告。

避坑提示:即使使用 CGLIB 代理(基于子类继承),protected 方法理论上可被拦截,但 Spring 官方文档明确建议只将 @Transactional 放在 public 方法上。非 public 方法的事务行为不可靠,且在不同 Spring 版本中表现不一致。

3.4 异常类型不匹配:默认回滚策略

Spring 事务默认只对 RuntimeException 和 Error 执行回滚。如果方法抛出 checked 异常(如 IOException、SQLException),事务会正常提交,导致数据不一致。

失效示例:
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) throws IOException {
    // 扣款成功
    debit(from, amount);
    // 调用外部服务失败,抛出 IOException(checked 异常)
    throw new IOException("外部服务异常");
    // 事务提交!扣款未回滚
}

解决方案:在 @Transactional 中显式指定回滚异常类型:

@Transactional(rollbackFor = {IOException.class, BusinessException.class})
public void transferMoney(Account from, Account to, BigDecimal amount) throws IOException {
    // 扣款
    debit(from, amount);
    // 抛出 IOException 时,事务回滚
    throw new IOException("外部服务异常");
}

3.5 代理机制失效:容器管理与 final 限制

Spring 事务代理依赖于 IoC 容器管理 Bean。以下情况会导致代理机制完全失效:

  • 手动 new 对象:new OrderService() 创建的对象不受 Spring 管理,@Transactional 被忽略。
  • final 类或 final 方法:CGLIB 通过子类继承生成代理,final 类无法被继承,final 方法无法被重写。
  • 静态方法:静态方法不属于对象实例,无法被 AOP 拦截。
  • 同一类中非事务方法调用事务方法:即自调用问题,本质也是代理机制失效。
代理机制失效的四种场景 @Transactional 失效 手动 new 对象 脱离 Spring 容器 final 类/方法 CGLIB 无法继承 静态方法 不属于对象实例 自调用 this.method() 绕过代理

3.6 开发环境调试清单

在开发环境中排查事务失效问题时,建议按以下清单逐项检查:

  1. 检查方法修饰符:是否为 public?
  2. 检查调用方式:是否通过代理对象调用?是否存在自调用?
  3. 检查异常类型:方法抛出的异常是否在 rollbackFor 列表中?
  4. 检查 Bean 管理:类是否被 Spring 容器管理(是否标注 @Service/@Component)?
  5. 检查代理方式:是否使用了 final 关键字?是否启用了 @EnableAspectJAutoProxy?
  6. 检查事务管理器:是否配置了正确的 PlatformTransactionManager?
调试技巧:在 IDE 中设置断点,观察 TransactionInterceptor.invoke() 是否被调用。如果未进入该方法,说明事务代理未生效,应优先排查代理机制问题。