视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在企业级开发中,事务管理是数据一致性的生命线。但很多开发者对@Transactional的理解仅停留在“加了就能回滚”,却在真实场景中频频踩坑:
- 为什么方法内部调用事务不生效?
- 为什么 try-catch 后事务没回滚?
- 多个
@Transactional方法嵌套时,到底用哪个事务?
今天我们就通过7 种传播行为 + 5 大实战陷阱 + 源码解析,彻底掌握 Spring 事务的底层逻辑!
一、需求场景:用户下单涉及多个操作
@Service public class OrderService { @Autowired private AccountService accountService; @Transactional public void createOrder(Long userId, BigDecimal amount) { // 1. 扣减账户余额 accountService.deductBalance(userId, amount); // 2. 创建订单 orderMapper.insert(new Order(userId, amount)); // 3. 发送消息(可能失败) messageService.sendOrderCreated(userId); } }问题来了:
- 如果
sendOrderCreated()抛异常,账户扣款会回滚吗? - 如果
deductBalance()内部也有@Transactional,事务如何传播?
答案取决于事务传播机制(Propagation)!
二、Spring 事务的 7 种传播行为(重点!)
| 传播行为 | 说明 | 典型场景 |
|---|---|---|
| REQUIRED(默认) | 如果当前有事务,加入;否则新建 | 绝大多数业务 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 日志、审计、独立操作 |
| SUPPORTS | 有事务则用,无则非事务执行 | 查询类方法 |
| NOT_SUPPORTED | 挂起事务,以非事务方式执行 | 高性能读、外部回调 |
| MANDATORY | 必须在事务中执行,否则抛异常 | 强一致性子操作 |
| NEVER | 不能在事务中执行,否则抛异常 | 幂等校验、缓存更新 |
| NESTED | 嵌套事务(依赖数据库 savepoint) | 部分回滚需求 |
💡最常用的是 REQUIRED 和 REQUIRES_NEW!
三、实战演示:REQUIRED vs REQUIRES_NEW
场景:主流程扣款 + 子流程记日志
@Service public class OrderService { @Autowired private LogService logService; @Transactional(propagation = Propagation.REQUIRED) public void createOrder() { orderMapper.insert(...); logService.log("订单创建"); // 调用日志服务 throw new RuntimeException("模拟失败"); } } @Service public class LogService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void log(String msg) { logMapper.insert(msg); // 独立事务 } }✅ 结果:
- 订单插入回滚(主事务失败);
- 日志记录成功提交(子事务独立)。
🔥 如果
log()也用REQUIRED,则日志也会回滚!
四、五大高频陷阱与解决方案
❌ 陷阱1️⃣:方法内部调用,事务失效!
@Service public class UserService { public void register(User user) { this.saveUser(user); // 直接调用,事务不生效! } @Transactional public void saveUser(User user) { userMapper.insert(user); throw new RuntimeException(); } }原因:
Spring 事务基于AOP 代理。register()调用saveUser()是this. 调用,绕过了代理对象,事务拦截器未触发。
✅ 解决方案:
- 方案1:注入自己(不推荐)
@Autowired private UserService self; self.saveUser(user); - 方案2:拆到另一个 Service(推荐)
- 方案3:使用
AopContext.currentProxy()(需开启 exposeProxy)
❌ 陷阱2️⃣:try-catch 吞掉异常,事务不回滚!
@Transactional public void transfer() { try { accountMapper.deduct(...); accountMapper.add(...); } catch (Exception e) { log.error("转账失败", e); // 忘记 throw!事务不会回滚! } }✅ 正确做法:
} catch (Exception e) { log.error("...", e); throw e; // 必须抛出! }或显式回滚:
@Transactional(rollbackFor = Exception.class) public void transfer() { try { ... } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }⚠️ 默认只对RuntimeException 和 Error回滚!检查异常(如 IOException)不会回滚!
❌ 陷阱3️⃣:非 public 方法,事务失效!
@Transactional protected void saveUser() { ... } // protected 方法,代理无效!✅ 必须是public 方法!
❌ 陷阱4️⃣:异步方法中事务失效!
@Transactional @Async public void asyncProcess() { // 事务不会生效!因为 @Async 和 @Transactional 代理冲突 }✅ 解决:在调用处加事务,异步方法内不加:
@Transactional public void process() { asyncService.doWork(); // 异步方法无 @Transactional } @Service public class AsyncService { @Async public void doWork() { ... } }❌ 陷阱5️⃣:MySQL 表引擎不是 InnoDB!
MyISAM 不支持事务!确保表引擎为InnoDB。
五、源码级原理:事务如何工作?
- 启动时:
@EnableTransactionManagement注册BeanPostProcessor; - 创建 Bean 时:为带
@Transactional的类生成CGLib 代理; - 方法调用时:
- 代理拦截方法;
- 从
TransactionManager获取连接; - 设置
autoCommit=false; - 执行业务逻辑;
- 无异常 → commit;有异常 → rollback。
🔑 关键:同一个数据库连接在整个事务中复用(通过 ThreadLocal)。
六、面试加分回答
问:REQUIRES_NEW 和 NESTED 有什么区别?
✅ 回答:
- REQUIRES_NEW:完全独立的新事务,提交/回滚互不影响;
- NESTED:嵌套在当前事务中,使用数据库savepoint实现部分回滚。
- 外层回滚 → 内层也回滚;
- 内层回滚 → 只回滚到 savepoint,外层可继续提交。
但NESTED 依赖数据库支持(如 MySQL InnoDB),而 REQUIRES_NEW 是通用方案。
问:事务失效的根本原因是什么?
✅ 回答:
核心是代理未生效。
Spring 事务基于 AOP 代理,只有通过代理对象调用 public 方法时,
拦截器才能织入事务逻辑。
自调用、非 public、静态方法、final 方法等都会导致代理失效。
七、最佳实践建议
- ✅ 事务方法必须是public;
- ✅ 避免自调用,拆分到不同 Service;
- ✅ 显式指定
rollbackFor = Exception.class; - ✅ 谨慎使用
REQUIRES_NEW,避免连接耗尽; - ✅ 事务方法尽量短小,减少锁持有时间;
- ✅ 读多写少场景,可用
readOnly = true提升性能。
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!