Spring 事务传播机制
Spring 事务传播机制可使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 事务传播机制的级别包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,
- 如果当前存在事务,则加入该事务;
- 如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS:
- 如果当前存在事务,则加入该事务;
- 如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)
- 如果当前存在事务,则加入该事务;
- 如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示
创建一个新的事务- 如果当前存在事务,则把当前事务挂起。
- 也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以
非事务方式运行- 如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以
非事务方式运行- 如果当前存在事务,则抛出异常。
- Propagation.NESTED:
- 如果当前存在事务,则创建一个事务作为当前事务的
嵌套事务来运行; - 嵌套事务发生异常后会局部回滚,不会影响外部事务(但外部事务会影响嵌套事务);
- 如果当前没有事务,则创建一个新的事务。
- 如果当前存在事务,则创建一个事务作为当前事务的
嵌套事务 NESTED 的回滚策略(关键逻辑)
- NESTED 内部出现异常 → 仅仅回滚到其保存点
- 主事务 T1 中之前的操作 可以不受影响
示例:
T1:
A() // 正常
B() nested // 内部异常,只回滚 B 的保存点
A() 的数据可以保留。
但是重要前提:
如果异常继续往外抛出 → 最终 T1 也会回滚!!!
所以 NESTED 的效果完全依赖于外层方法是否捕获异常。
异常传播对 NESTED 的影响(极重要)
NESTED 的“局部回滚能力”只有在以下条件中才能生效:
只有在内部 try-catch 掉异常时,NESTED 才能保持外层事务不回滚
否则异常继续抛出:
NESTED → 外层 → 最终 T1 肯定回滚
所以:
正确用法:
try {
logService.saveLog(); // nested
} catch(Exception e) {
// swallow
}
错误用法:
logService.saveLog(); // nested,但异常向外抛出 → 外层 T1 一定回滚
REQUIRED:所有操作绑定在一个事务中,只能整体成功或整体失败。
NESTED:在主事务中划分可单独回滚的“子单元”,可实现“局部失败、整体成功”,但前提是异常必须在内部捕获。
Spring 的核心机制:异常未被处理 → 事务标记 rollback-only ; 无论传播行为是 REQUIRED、REQUIRES
Spring Data JPA 不支持 Propagation.NESTED。
Hibernate(JPA 默认实现)不支持保存点(Savepoint)机制,因此无法真正实现嵌套事务。
在 JPA 环境中使用 @Transactional(propagation = Propagation.NESTED) Spring 会尝试创建一个 savepoint,但 HibernateTransactionManager 没有实现该能力。- 要么直接退化成 REQUIRED(加入同一个事务);要么抛出异常:不支持 savepoint。
JPA 要想做到子操作失败不影响主事务,唯一可行方式是:使用 REQUIRES_NEW 代替 NESTED
隔离级别
- SERIALIZABLE 串行化
- 避免所有并发问题
- 性能最差
- REPEATABLE READ(可重复读)
- MySQL 默认的隔离级别
- 避免脏读、不可重复读;可能会出现幻读
- MySQL 中使用 MVCC + Next-Key Lock 可以避免幻读
- READ COMMITTED(读已提交)
- 避免脏读;可能出现 不可重复读和幻读
- READ UNCOMMITTED(读未提交)
- 脏读、不可重复读、幻读都会发生
READ COMMITTED(读已提交)
同一事务内:
- 每次查询都能看到别人已经提交的数据(每次读都创建新的快照)
- 不会脏读
- 会出现不可重复读
- 会出现幻读
事务A:
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 读到 100
事务B:
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
事务A 再查:
SELECT balance FROM account WHERE id = 1; -- 这次读到 200
同一事务内两次读取结果不一致,这就是 不可重复读。
REPEATABLE READ(可重复读)
同一事务内:
- 第一次查询创建快照(Snapshot) ,后续读都基于这个快照
- 即使别的事务提交了数据,当前事务也看不到
- 不可重复读不会发生
- MySQL InnoDB 中不会发生幻读
事务A:
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 读到 100
事务B:
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
事务A 再查:
SELECT balance FROM account WHERE id = 1; -- 仍然读到 100
结果一致,可重复读实现了。
MySQL 为什么在可重复读下没有幻读?
多数数据库 REPEATABLE READ 会出现幻读,但 MySQL InnoDB 使用 MVCC + Next-Key Lock(间隙锁) 解决了幻读问题。
事务A:
BEGIN;
SELECT * FROM orders WHERE amount > 100;
事务B 插入符合条件的数据:
INSERT INTO orders (amount) VALUES (200);
COMMIT;
事务A 再查:
SELECT * FROM orders WHERE amount > 100;
在 MySQL 中,事务A 看不到事务B插入的新行,因为间隙锁阻止了产生幻读。
场景选择
使用 READ COMMITTED 的场景
- 高并发 OLTP 系统(避免长时间持锁)
- 查询结果允许变化(比如订单列表、日志、动态内容)
- 避免锁争用、提高吞吐量更重要
使用 REPEATABLE READ 的场景
- 金融类、余额扣减类场景
- 需要确保“事务内看到的数据一致”
- 需要避免不可重复读的情况