“老大,出大事了!刚才那个转账业务报错了,但是钱扣了,对方没收到!”
“不是加了 @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 切面根本没执行!
✅ 解决方案:
-
注入自己 (推荐) :
@Autowired private OrderService self; // 注入代理对象 public void createOrder() { self.saveOrder(); // 走代理调用 } -
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 就会认为“一切正常”,自然不会触发回滚,导致数据不一致。
✅ 解决方案:
-
继续抛出:在 catch 块中 throw new RuntimeException(e);。
-
手动回滚:
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 虽然方便,但它不是银弹。
作为开发者,我们要时刻保持敬畏之心:
- 写完事务代码,一定要测一下回滚! (单元测试里手动抛个异常试试)。
- 尽量缩小事务粒度:不要把网络请求(RPC、HTTP)放在事务里,否则会占用数据库连接,导致连接池耗尽。
- 推荐配置:养成好习惯,所有事务注解统一写成 @Transactional(rollbackFor = Exception.class)。
希望这篇文章能帮你避开这些坑,让你的系统稳如磐石!