Java面试之Spring事务失效的场景分析

@Transactional在private方法上不生效,因Spring事务代理机制仅拦截public方法;self-invocation、异常类型未配置rollbackFor、传播行为不当也会导致事务失效。

为什么@Transactional在private方法上不生效

Spring 的事务代理机制只对 public 方法起作用,因为 AOP 代理(无论是 JDK 动态代理还是 CGLIB)默认不会拦截 private、protected 或 package-private 方法调用。@Transactional 注解本质是通过代理对象在方法执行前后织入事务管理逻辑,而 private 方法无法被代理对象重写或拦截。

常见错误现象:把 @Transactional 加在 private 方法上,运行时完全无事务行为,且无任何警告或异常。

  • 必须确保目标方法是 public 修饰的
  • 避免在同一个类内 self-invocation(即 this.method() 调用),哪怕该 method 是 public + @Transactional,也会绕过代理,事务失效
  • 若需内部调用带事务的方法,可注入自身 Bean(@Autowired 当前类类型)再调用,或改用 TransactionTemplate

同一类中非事务方法调用@Transactional方法为何没事务

这是 self-invocation 的典型场景。当一个类的 methodA()(无注解)直接调用本类的 methodB()(有 @Transactional),JVM 执行的是当前对象的原始方法,不经过 Spring 代理对象,事务切面根本不会触发。

使用场景:Service 类中拆分逻辑,主入口无事务,子逻辑需要事务控制——这种设计天然破坏代理链。

  • self-invocation 不受代理控制,和访问修饰符无关,即使 methodB 是 public 也无效
  • 解决方式之一是将 methodB 抽到另一个 @Service 类中,通过依赖注入调用
  • 或使用 AopContext.currentProxy() 强制走代理(需开启 expose-proxy="true"),但侵入性强,不推荐

RuntimeException以外的异常未回滚怎么办

@Transactional 默认只对 RuntimeException 及其子类、Error 回滚;对 Exception 的其他子类(如 IOExceptionSQLException)不会自动回滚,除非显式配置。

常见错误现象:捕获了业务异常但没抛出 RuntimeException,或抛出了 checked exception 却未声明 rollbackFor,导致数据已写库但事务未回滚。

  • 检查是否遗漏 rollbackFor = Exception.class 或具体异常类型,例如 rollbackFor = IOException.class
  • 注意 noRollbackFor 会覆盖默认行为,慎用
  • 避免在事务方法内吞掉异常(空 catch),这等于主动放弃回滚机会

事务传播行为配置不当导致嵌套调用失效

当方法 A(REQUIRED)调用方法 B(REQUIRES_NEW),B 会挂起 A 的事务并新建事务;但如果 B 是 SUPPORTSNOT_SUPPORTED,则可能不启用事务,甚至挂起已有事务,造成意料外的提交/回滚范围。

性能与一致性影响:错误的传播

行为可能导致事务粒度失控,比如本该统一回滚的操作被拆成多个独立事务。

  • 默认传播行为是 REQUIRED,适用于大多数场景
  • REQUIRES_NEW 适合日志记录、审计等必须独立提交的逻辑,但会增加数据库连接开销
  • NEVERNOT_SUPPORTED 会脱离事务上下文,务必确认业务允许无事务执行
@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        // 正常事务内操作
        orderMapper.insert(order);
        // 若 sendNotification() 是本类 private 或 this. 调用,事务不延伸至此
        sendNotification(order);
    }

    @Transactional(rollbackFor = Exception.class)
    public void sendNotification(Order order) throws Exception {
        // 显式声明 rollbackFor,应对 checked exception
        smsClient.send(order.getPhone(), "OK");
    }
}
事务失效往往不是单点问题,而是代理机制、异常类型、调用路径、传播行为共同作用的结果。最容易被忽略的是 self-invocation 和异常分类处理——这两处不报错、不告警,却让事务形同虚设。