Spring事务的传播机制
一、最常用的三种传播机制
1. REQUIRED(默认,最常用)
这是Spring的默认传播行为,也是使用最多的。行为规则:- 如果当前存在事务,就加入这个事务
- 如果当前没有事务,就新建一个事务
- 转账方法开启了事务A
- 扣款方法用REQUIRED,会加入事务A
- 加款方法用REQUIRED,也会加入事务A
- 三个方法在同一个事务中,任何一个失败,全部回滚
2. REQUIRES_NEW(独立事务)
这个也很常用,特别是在需要独立提交的场景。行为规则:- 无论当前是否存在事务,都会新建一个事务
- 如果当前存在事务,会把当前事务挂起
- 新事务和外层事务完全独立,互不影响
- 修改订单方法开启了事务A
- 记录日志方法用REQUIRES_NEW,会暂停事务A,开启新事务B
- 日志记录完成,事务B提交,然后恢复事务A
- 即使订单修改失败回滚了,日志依然保存成功
3. SUPPORTS(支持事务)
行为规则:- 如果当前存在事务,就加入这个事务
- 如果当前没有事务,就以非事务方式执行
- 如果在一个事务流程中调用查询,可以读到事务中未提交的数据(可重复读)
- 如果单独调用查询,不需要事务,减少开销
二、不常用但需要理解的四种
4. MANDATORY(强制要求事务)
行为规则:- 如果当前存在事务,就加入这个事务
- 如果当前没有事务,就抛出异常
5. NOT_SUPPORTED(不支持事务)
行为规则:- 如果当前存在事务,就把事务挂起
- 总是以非事务方式执行
- 用户下单成功后发送邮件通知
- 发送邮件方法用NOT_SUPPORTED
- 即使在事务中调用,也会暂停事务,以非事务方式发送邮件
- 这样邮件发送的耗时不会占用数据库事务时间
6. NEVER(禁止事务)
行为规则:- 总是以非事务方式执行
- 如果当前存在事务,就抛出异常
7. NESTED(嵌套事务)
这个比较特殊,也是面试的重点。行为规则:- 如果当前存在事务,就在嵌套事务中执行
- 如果当前没有事务,行为同REQUIRED,创建新事务
- 使用数据库的Savepoint保存点机制
- 内层事务是外层事务的一部分
- 内层可以独立回滚到保存点,不影响外层
- 但外层回滚,内层也会回滚
- 内层是外层的一部分,用的是同一个物理事务
- 外层回滚,内层必然回滚
- 内层回滚,可以只回滚到保存点,外层可以继续
- 只用一个数据库连接
- 内层是完全独立的事务
- 外层回滚,不影响内层(已经提交了)
- 内层回滚,不影响外层
- 需要两个数据库连接
- 外层事务处理整个批量操作
- 每条数据用NESTED处理
- 某条数据失败,只回滚这条,记录失败原因
- 其他数据继续处理
- 最后外层事务统一提交
三、实际项目中的使用建议
根据我的实践经验:使用频率排序:- REQUIRED(90%的场景) - 默认就用它
- REQUIRES_NEW(8%的场景) - 日志记录、独立操作
- NESTED(1%的场景) - 批量处理允许部分失败
- 其他几种(1%的场景) - 几乎不用
- 创建订单
- 扣减库存
- 扣减余额
- 增加积分
- 用户修改敏感数据
- 记录审计日志用REQUIRES_NEW
- 无论业务成功失败,日志都要保存
- 批量导入、批量更新
- 允许部分失败,记录失败信息
- 成功的部分要提交
四、常见的坑和误区
误区1:内部调用事务失效
这是最常见的问题!如果在同一个类中,methodA调用methodB,methodB的事务注解会失效。因为没有走代理,直接是this.methodB()。解决方案:- 把methodB提取到另一个Service
- 或者自己注入自己(不优雅)
- 或者用AspectJ的编译时织入
误区2:异常被捕获导致不回滚
事务只对RuntimeException和Error默认回滚,对受检异常(CheckedException)不回滚。而且如果你catch了异常,事务也不会回滚。我之前就遇到过:一个转账方法,里面catch了异常只打了个日志,结果扣款成功了但加款失败了,事务没回滚,造成了数据不一致。正确做法:- 要么不catch,让异常抛出去
- 要么catch后手动抛出RuntimeException
- 或者用rollbackFor指定回滚的异常类型
误区3:REQUIRES_NEW的连接池问题
REQUIRES_NEW会暂停外层事务,开启新事务,需要两个数据库连接。如果连接池配置太小,高并发时可能导致连接池耗尽,产生死锁。我在生产环境就遇到过这个问题:连接池只配了10个连接,高峰期出现大量REQUIRES_NEW,导致连接等待超时。解决方案:- 合理评估连接池大小
- 减少REQUIRES_NEW的使用
- 考虑用异步方式替代
误区4:NESTED的数据库支持问题
不是所有数据库都支持Savepoint。使用前要确认数据库版本是否支持。而且NESTED只能用在支持JDBC的情况下,如果用JTA分布式事务,NESTED不可用。五、面试加分项:事务隔离级别
面试官可能会顺便问事务的隔离级别,这是配合传播机制一起使用的。Spring支持5种隔离级别:- DEFAULT:使用数据库默认隔离级别(MySQL是REPEATABLE_READ)
- READ_UNCOMMITTED:读未提交,脏读、不可重复读、幻读都可能发生
- READ_COMMITTED:读已提交,避免脏读(Oracle默认)
- REPEATABLE_READ:可重复读,避免脏读和不可重复读(MySQL默认)
- SERIALIZABLE:串行化,最高级别,性能最差
总结
核心要点:- REQUIRED是默认且最常用的,90%场景用它
- REQUIRES_NEW用于需要独立提交的场景,如日志记录
- NESTED用于批量操作允许部分失败
- 其他几种很少用,了解即可
- 传播机制解决的是事务方法互相调用的问题
- 要避免内部调用导致事务失效
- 要注意异常处理,不要catch后不抛出
- REQUIRES_NEW要注意连接池大小
在实际项目中,我们更应该关注的是事务边界的设计,在合适的地方开启事务,控制事务粒度,避免大事务,而不是过度使用各种传播级别。保持简单,用好REQUIRED和REQUIRES_NEW就能解决大部分问题。
Spring事务传播机制对比表
| 传播机制 | 当前存在事务 | 当前无事务 | 是否新建物理事务 | 外层回滚对内层影响 | 内层回滚对外层影响 | 使用频率 | 典型场景 |
|---|---|---|---|---|---|---|---|
| REQUIRED<br>(默认) | 加入当前事务 | 新建事务 | 否(共用) | 内层一起回滚 | 外层也回滚 | ⭐⭐⭐⭐⭐<br>90% | 普通业务操作<br>保证整体原子性 |
| REQUIRES_NEW | 挂起当前事务<br>新建独立事务 | 新建事务 | 是(独立) | 不影响<br>(已提交) | 不影响外层 | ⭐⭐⭐⭐<br>8% | 操作日志记录<br>独立提交的操作 |
| NESTED | 在嵌套事务中执行<br>(Savepoint) | 新建事务<br>(同REQUIRED) | 否(共用,但有保存点) | 内层一起回滚 | 可以捕获处理<br>回滚到保存点 | ⭐⭐<br>1% | 批量操作<br>允许部分失败 |
| SUPPORTS | 加入当前事务 | 非事务方式执行 | 否 | 内层一起回滚 | 外层也回滚 | ⭐<br><1% | 查询操作 |
| NOT_SUPPORTED | 挂起当前事务<br>非事务方式执行 | 非事务方式执行 | 否 | 无影响 | 无影响 | ⭐<br><1% | 发送邮件<br>调用外部接口 |
| MANDATORY | 加入当前事务 | 抛出异常 | 否 | 内层一起回滚 | 外层也回滚 | ⭐<br>几乎不用 | 强制要求在<br>事务中调用 |
| NEVER | 抛出异常 | 非事务方式执行 | 否 | - | 无影响 | ⭐<br>几乎不用 | 强制不能在<br>事务中调用 |

浙公网安备 33010602011771号