6.1 事务管理:从手动提交到声明式 本节摘要:事务管的是"要么都做,要么都不做"。本节用一个转账案例出发:先纠正 autoCommit 的普遍误解,写出纯 MyBatis 手动事务的完整代码,看懂异常回滚的时机,再对照 5.5 的 Spring 声明式写法,最后清点事务失效的高发场景。 复盘室的第一台手术是账户转账:A 账户扣 500,B 账户加 500。两条 UPDATE,中间任何一步失败,钱就凭空消失或凭空多出。事务就是给这两步上的保险——同生共死。 先纠正一个误解:autoCommit 不是"省一行 commit" 不少人把 当偷懒技巧——不用写 commit 了。语义恰恰相反:autoCommit 打开时,每条语句都是独立事务。
本节摘要:事务管的是"要么都做,要么都不做"。本节用一个转账案例出发:先纠正 autoCommit 的普遍误解,写出纯 MyBatis 手动事务的完整代码,看懂异常回滚的时机,再对照 5.5 的 Spring 声明式写法,最后清点事务失效的高发场景。
复盘室的第一台手术是账户转账:A 账户扣 500,B 账户加 500。两条 UPDATE,中间任何一步失败,钱就凭空消失或凭空多出。事务就是给这两步上的保险——同生共死。
不少人把 factory.openSession(true) 当偷懒技巧——不用写 commit 了。语义恰恰相反:autoCommit 打开时,每条语句都是独立事务。转账的两步更新从此各写各的账,第一步成功第二步失败,扣掉的钱不会回来。它适合的是单语句写操作,恰恰不适合"多步同生共死"的场景。
默认的 openSession() 才是事务模式:连接 autoCommit 关闭,所有语句挂在一个未提交的事务里,直到你 commit 或 rollback。
纯 MyBatis 环境下,事务的三件套是 try-catch-finally:
// 转账:扣款与入账必须同生共死 try (SqlSession session = factory.openSession()) { // 默认即事务模式 try { AccountMapper mapper = session.getMapper(AccountMapper.class); mapper.deduct(fromId, amount); // 第一步:扣款 mapper.deposit(toId, amount); // 第二步:入账 session.commit(); // 两步都成功,才一起生效 } catch (Exception e) { session.rollback(); // 任何一步失败,全部撤销 throw e; // 回滚后继续抛出,让上层感知失败 } } // try-with-resources 自动 close
三个时机要点:
throw e 把失败报告出去;吞掉异常的回滚等于静默丢单。
5.5 集成完成后,同样的故事只剩一个注解:
@Service public class TransferService { @Autowired private AccountMapper accountMapper; @Transactional // 边界在这里 public void transfer(Long fromId, Long toId, BigDecimal amount) { accountMapper.deduct(fromId, amount); accountMapper.deposit(toId, amount); } // 正常返回自动 commit;RuntimeException 自动 rollback }
底层换成了 DataSourceTransactionManager + SqlSessionTemplate:方法入口开启事务并把连接绑定到当前线程,方法内借到的每个会话都复用这条连接,出口统一提交或回滚。默认传播行为 REQUIRED 让事务方法嵌套调用时合并进外层事务,需要独立提交的子操作才显式指定 REQUIRES_NEW。
声明式事务靠代理实现,绕过代理的调用一律失效:
| 场景 | 原因 | 判别口诀 |
|---|---|---|
| 同类内部方法直接调用 | this 调用不走代理,注解形同虚设 | 事务方法必须从 Bean 外部调进来 |
| 方法不是 public | 代理只增强 public 方法 | 非 public 上的 @Transactional 是摆设 |
| 吞了异常没抛出 | 代理看不到异常,按成功提交 | catch 之后要么 throw,要么手动 rollback |
| 抛的是受检异常 | 默认只回滚 RuntimeException | 加 rollbackFor = Exception.class |
| 数据源没配事务管理器 | 没有 DataSourceTransactionManager 接管 | 启动时确认事务管理器 Bean 存在 |
⚠️ 常见坑:整类上标 @Transactional 会让所有 public 方法都进事务,包括纯查询——查询进事务浪费连接占用时间,长事务还会放大锁竞争。查询方法移出事务边界,写方法才标注。
💡 关键直觉:事务边界画在"业务动作"上,不在"单条 SQL"上。transfer 是一个业务动作,它的边界就该罩住扣款与入账两步;边界画小了同生共死失效,画大了长事务拖垮连接池。
同生共死保住了正确性。下一节换维护性视角:一份坏味道清单收拢代码规范,再搭一套不依赖测试库的 Mapper 单元测试。