📘 一、前言
在 Spring 项目中,@Transactional 是最常见的事务注解,但也是最容易被误用的注解之一。
很多时候明明加上了 @Transactional,事务却根本没生效,最后只能靠打印日志发现一地鸡毛。
这篇文章总结了 6 种 @Transactional 常见失效场景,每一种都附上原因与解决方案。
🧩 二、@Transactional 生效的基础原理
Spring 的事务机制基于 AOP 动态代理 实现:
- 被代理的目标方法在调用时,由代理类拦截;
- 代理逻辑会在方法执行前开启事务,执行后提交或回滚;
- 因此,必须是通过代理调用方法,事务才会生效。
⚠️ 三、常见失效场景与解决方案
场景一:方法内部调用(自调用)
💥 现象:
在同一个类中,一个方法调用另一个带有 @Transactional 的方法:
@Service
public class UserService {
@Transactional
public void saveUser() {
// 插入数据库
}
public void createUser() {
saveUser(); // 自调用
}
}
👉 saveUser() 中的事务不会生效!
🎯 原因:
Spring AOP 代理无法拦截 同类内部方法调用,因为此调用不会经过代理对象。
✅ 解决方案:
- 将事务方法抽到另一个类中;
- 或通过
AopContext.currentProxy()获取代理对象:
((UserService) AopContext.currentProxy()).saveUser();
⚠️ 注意:需开启
exposeProxy = true
@EnableAspectJAutoProxy(exposeProxy = true)
场景二:调用方法不是 public
@Transactional
protected void updateUser() { ... }
👉 事务无效。
🎯 原因:
Spring AOP 默认只拦截 public 方法,private、protected 方法不会生成代理逻辑。
✅ 解决方案:
确保事务方法为 public。
场景三:异常未抛出或异常类型不匹配
@Transactional
public void transfer() {
try {
// do something
throw new IOException("error");
} catch (Exception e) {
// 吃掉异常
}
}
👉 事务不会回滚。
🎯 原因:
默认情况下,@Transactional 只会回滚 RuntimeException(及其子类) 。
而受检异常(如 IOException)不会触发回滚。
✅ 解决方案:
- 抛出 RuntimeException;
- 或通过属性声明回滚规则:
@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 CGLIB | JDK 仅代理接口,CGLIB 代理类 |
| 方法修饰符 | 必须 public | 否则代理增强失效 |
| 异常回滚类型 | 默认只回滚 RuntimeException | 可通过 rollbackFor 指定 |
| 自调用问题 | 是否通过代理对象调用 | 否则事务不生效 |
| 多线程调用 | 是否跨线程执行 | 子线程不继承事务上下文 |
🚀 五、最佳实践建议
-
事务方法尽量保持原子性与单一职责;
-
避免事务方法中调用异步逻辑;
-
明确异常边界,合理配置
rollbackFor; -
建议开启事务调试日志:
logging.level.org.springframework.transaction=DEBUG -
对复杂事务链路,使用 传播行为 + 嵌套事务策略 明确事务边界。
🏁 六、结语
@Transactional 是 Spring 的一大利器,但滥用或误用也最常见。
记住一句话:
“事务不是魔法,理解它的边界,才能写出可控的代码。”