💣 生产事故复盘:为什么你的 @Transactional 事务失效了?6 种经典场景大揭秘!

27 阅读3分钟

“老大,出大事了!刚才那个转账业务报错了,但是钱扣了,对方没收到!”
“不是加了 @Transactional 吗?怎么没回滚?”
“我……我也不知道啊……”

在 Spring Boot 开发中,事务管理是数据的生命线。我们习惯了随手打一个 @Transactional 注解,就以为万事大吉。然而,Spring 的声明式事务是基于 AOP(面向切面编程)  实现的,如果使用姿势不对,事务就会静悄悄地失效,酿成大错。

今天,我们来一次**“排雷行动”**,盘点 6 种最容易导致事务失效的场景,看看你中招了没?

场景一:同类内部调用 (Self-Invocation) 💀  (最坑!)

这是新手最容易犯的错,也是面试最高频考点。

❌ 错误代码

@Service
public class OrderService {

    public void createOrder() {
        // ... 逻辑 ...
        // 在同一个类中,调用本类的事务方法
        this.saveOrder(); 
    }

    @Transactional
    public void saveOrder() {
        // ... 数据库操作 ...
        throw new RuntimeException("发生异常");
    }
}

💥 原因分析
Spring 的事务是基于 动态代理 (Proxy)  实现的。
只有通过 代理对象 调用方法时,事务拦截器才会生效。
而在 createOrder 方法中,使用 this.saveOrder() 是对象内部直接调用,绕过了代理对象,导致事务 AOP 切面根本没执行!

✅ 解决方案

  1. 注入自己 (推荐)

    @Autowired
    private OrderService self; // 注入代理对象
    
    public void createOrder() {
        self.saveOrder(); // 走代理调用
    }
    
  2. AopContext (黑科技)

    ((OrderService) AopContext.currentProxy()).saveOrder();
    

场景二:异常被“吃”掉了 (Swallowed Exception) 🍬

❌ 错误代码

@Transactional
public void updateUser() {
    try {
        userMapper.update(user);
        int i = 1 / 0; // 模拟异常
    } catch (Exception e) {
        log.error("更新失败", e);
        // 异常被捕获了,没有抛出
    }
}

💥 原因分析
Spring 事务回滚的触发条件是:方法抛出异常
如果你在方法内部用 try-catch 把异常捕获并吞掉了,Spring 就会认为“一切正常”,自然不会触发回滚,导致数据不一致。

✅ 解决方案

  1. 继续抛出:在 catch 块中 throw new RuntimeException(e);。

  2. 手动回滚

    catch (Exception e) {
        log.error("更新失败", e);
        // 手动标记回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    

场景三:异常类型不匹配 (Checked Exception) 🎭

❌ 错误代码

@Transactional // 默认只回滚 RuntimeException 和 Error
public void readFile() throws IOException {
    userMapper.insert(user);
    // IOException 是受检异常 (Checked Exception)
    throw new IOException("文件读取失败"); 
}

💥 原因分析
默认情况下,Spring 只在遇到 RuntimeException (运行时异常) 或 Error 时才回滚。
对于 IOException、SQLException 等 Checked Exception,Spring 默认是不回滚的!

✅ 解决方案
显式指定回滚异常类型:

@Transactional(rollbackFor = Exception.class)

场景四:方法修饰符不是 Public 🔒

❌ 错误代码

@Transactional
protected void saveLog() { // protected 或 private
    // ...
}

💥 原因分析
Spring 默认的事务拦截器(AbstractFallbackTransactionAttributeSource)规定,注解只能加在 public 方法上。如果加在 private、protected 方法上,会被直接忽略,不会报错,但事务无效。

✅ 解决方案
把方法改成 public。

场景五:数据库引擎不支持事务 💾

❌ 错误场景
MySQL 表使用了 MyISAM 引擎。

💥 原因分析
MyISAM 引擎根本不支持事务!这是数据库层面的硬伤,Spring 再强也救不了。

✅ 解决方案
修改数据库引擎为 InnoDB。

ALTER TABLE user ENGINE = InnoDB;

场景六:事务传播行为配置错误 📡

❌ 错误代码

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void doSomething() {
    // ...
}

💥 原因分析
Propagation.NOT_SUPPORTED 的意思是:以非事务方式运行,如果当前存在事务,就把当前事务挂起。
如果你手滑配成了这个,或者 NEVER,那事务肯定就没了。

✅ 解决方案
通常使用默认的 REQUIRED (如果当前没有事务,就新建一个;如果有,就加入) 即可。

💡 架构师总结

@Transactional 虽然方便,但它不是银弹。
作为开发者,我们要时刻保持敬畏之心:

  1. 写完事务代码,一定要测一下回滚!  (单元测试里手动抛个异常试试)。
  2. 尽量缩小事务粒度:不要把网络请求(RPC、HTTP)放在事务里,否则会占用数据库连接,导致连接池耗尽。
  3. 推荐配置:养成好习惯,所有事务注解统一写成 @Transactional(rollbackFor = Exception.class)。

希望这篇文章能帮你避开这些坑,让你的系统稳如磐石!