@Transactional 失效的 6 种典型场景(内部调用、自调用、异常类型等)

163 阅读3分钟

📘 一、前言

在 Spring 项目中,@Transactional 是最常见的事务注解,但也是最容易被误用的注解之一。
很多时候明明加上了 @Transactional,事务却根本没生效,最后只能靠打印日志发现一地鸡毛。

这篇文章总结了 6 种 @Transactional 常见失效场景,每一种都附上原因与解决方案。


🧩 二、@Transactional 生效的基础原理

Spring 的事务机制基于 AOP 动态代理 实现:

  • 被代理的目标方法在调用时,由代理类拦截;
  • 代理逻辑会在方法执行前开启事务,执行后提交或回滚;
  • 因此,必须是通过代理调用方法,事务才会生效。

⚠️ 三、常见失效场景与解决方案

场景一:方法内部调用(自调用)

💥 现象:

在同一个类中,一个方法调用另一个带有 @Transactional 的方法:

@Service
public class UserService {
    @Transactional
    public void saveUser() {
        // 插入数据库
    }

    public void createUser() {
        saveUser(); // 自调用
    }
}

👉 saveUser() 中的事务不会生效!

🎯 原因:

Spring AOP 代理无法拦截 同类内部方法调用,因为此调用不会经过代理对象。

✅ 解决方案:

  1. 将事务方法抽到另一个类中
  2. 或通过 AopContext.currentProxy() 获取代理对象:
((UserService) AopContext.currentProxy()).saveUser();

⚠️ 注意:需开启 exposeProxy = true
@EnableAspectJAutoProxy(exposeProxy = true)


场景二:调用方法不是 public

@Transactional
protected void updateUser() { ... }

👉 事务无效。

🎯 原因:

Spring AOP 默认只拦截 public 方法,privateprotected 方法不会生成代理逻辑。

✅ 解决方案:

确保事务方法为 public


场景三:异常未抛出或异常类型不匹配

@Transactional
public void transfer() {
    try {
        // do something
        throw new IOException("error");
    } catch (Exception e) {
        // 吃掉异常
    }
}

👉 事务不会回滚。

🎯 原因:

默认情况下,@Transactional 只会回滚 RuntimeException(及其子类)
而受检异常(如 IOException)不会触发回滚。

✅ 解决方案:

  1. 抛出 RuntimeException;
  2. 或通过属性声明回滚规则:
@Transactional(rollbackFor = Exception.class)

场景四:事务方法被 final 修饰或类被 final 修饰

💥 现象:

@Service
public final class OrderService { ... }  // 类 final

或:

@Transactional
public final void processOrder() { ... } // 方法 final

🎯 原因:

CGLIB 动态代理通过子类继承实现代理。
final 类或方法无法被继承,因此代理增强失效。

✅ 解决方案:

不要将带事务的方法或类声明为 final


场景五:多线程环境下调用

@Transactional
public void saveData() {
    new Thread(() -> userRepository.save(...)).start();
}

👉 子线程中的数据库操作不在事务范围内。

🎯 原因:

Spring 的事务是基于线程绑定的 ThreadLocal 实现的。
新线程不会继承主线程的事务上下文。

✅ 解决方案:

  • 避免在事务方法中手动创建线程;
  • 若确实需要异步操作,使用 @Async + 事务补偿机制

场景六:事务传播级别或代理配置不当

💥 现象:

在一个大事务中嵌套调用多个子事务,结果子事务没有回滚。

@Transactional
public void mainProcess() {
    subService.doPart();  // 传播行为 REQUIRED
}

🎯 原因:

默认传播行为 REQUIRED 会加入外层事务。
若外层事务提交,则内层异常也不会单独回滚。

✅ 解决方案:

根据业务语义设置传播级别:

@Transactional(propagation = Propagation.REQUIRES_NEW)

🧠 四、排查思路总结

排查项检查点说明
AOP代理类型JDK vs CGLIBJDK 仅代理接口,CGLIB 代理类
方法修饰符必须 public否则代理增强失效
异常回滚类型默认只回滚 RuntimeException可通过 rollbackFor 指定
自调用问题是否通过代理对象调用否则事务不生效
多线程调用是否跨线程执行子线程不继承事务上下文

🚀 五、最佳实践建议

  1. 事务方法尽量保持原子性单一职责

  2. 避免事务方法中调用异步逻辑;

  3. 明确异常边界,合理配置 rollbackFor

  4. 建议开启事务调试日志:

    logging.level.org.springframework.transaction=DEBUG
    
  5. 对复杂事务链路,使用 传播行为 + 嵌套事务策略 明确事务边界。


🏁 六、结语

@Transactional 是 Spring 的一大利器,但滥用或误用也最常见。
记住一句话:

“事务不是魔法,理解它的边界,才能写出可控的代码。”