一、事务传播机制核心判断思路
1. 三个问题
- 外层调用方有没有事务?
- 内层失败的时候,要不要影响外层事务?(内层抛异常,外层要不要回滚)
- 内层是否需要独立提交 / 回滚?还是只是子事务?
2. 判断流程图
3. 具体传播机制使用规则
1) REQUIRED(默认)
- 规则:有就加入外层事务;没有自己新建。
- 场景:serviceA 开启事务调用 serviceB,A、B 在同一个事务。B 抛异常,A 一起回滚;A 抛异常 B 也回滚。
- 适合:大部分新增修改业务(CRUD),绝大多数情况不用手动改传播属性。
2) SUPPORTS
- 规则:如果外层有事务就用外层事务;没有事务就直接非事务运行。
- 适合:select 查询方法,不需要强制事务。
3) MANDATORY
- 禁止外层无事务调用,防止漏开事务。一般框架内部使用,业务代码很少写。
- 场景:内部核心更新方法,要求调用者必须开启事务,否则抛IllegalTransactionStateException。
- 适用:内层一定要跑在事务里面,外层没事务直接报错
4) REQUIRES_NEW
- 规则:调用 B 的时候,挂起 A 的事务,开启 B 全新独立事务;B 提交 / 回滚完全独立;B 结束,恢复 A 的挂起事务。
- 适用:内层需要独立事务,内层失败不影响外层
- 典型业务场景:操作日志、审计日志、错误记录。主业务失败回滚,但是错误日志必须入库保存,不能跟着业务一起回滚。
- 坑:如果不 catch,内层抛出异常直接抛给外层,外层也会回滚!内层独立事务回滚了,外层代码必须捕获内层异常。
5) NOT_SUPPORTED
- 规则:如果外层有事务,先挂起外层事务,本方法非事务执行。方法不能运行在事务中
- 场景:大批量数据导入,大批量查询,不想占用数据库事务连接,避免长事务。
6) NEVER
- 规则:禁止外层存在事务,如果外层有事务直接抛异常。
- 适用:业务几乎不用,用于校验。
7) NESTED(JDBC savepoint 保存点)
- 规则:嵌套事务。
- 内层回滚:只回滚到 savepoint,外层事务还可以继续提交;
- 外层回滚:整个全部回滚,内层提交也跟着回滚。
- 适合场景:一个大业务,分多个子步骤;子步骤可以单独回滚,但外层整体失败全部回滚。
- 限制:只支持 JDBC 事务;不支持 JTA 分布式事务。MySQL InnoDB 支持 savepoint。
- 和 REQUIRES_NEW 区别:不是新开独立事务,是外层事务里面设置保存点 savepoint
- REQUIRES_NEW vs NESTED:
- REQUIRES_NEW:完全独立两个事务,内层提交不受外层控制;外层回滚,内层已经提交的数据不受影响。
- NESTED:属于同一个物理事务,设置保存点;外层一旦回滚,嵌套部分全部回滚。
二、事务使用场景
事务的核心原则:
- 事务用来保证一组数据库操作「要么全部成功,要么全部失败」(ACID);
- 单条 SQL 天然是事务,不需要额外加 @Transactional。
注:MySQL InnoDB:每一条 DML(insert/update/delete)本身就是一个自动提交的独立事务。
1.需要事务的场景
业务需要多条数据库操作,逻辑上必须原子绑定,一部分成功一部分失败是错误数据。
1) 多写操作,业务要求原子性
- 例:转账:A 扣钱、B 加钱;扣钱成功,加钱失败,数据不能出现 A 钱少了 B 没多。
- 例:订单业务:创建订单、扣库存、生成流水、扣余额;任意一步失败,全部回滚。
- 例:主表 + 子表同时保存:保存订单 + 保存订单明细;不能订单存成功,明细丢失。
2) 读 + 写组合,需要数据一致性
先查询,再根据查询结果做更新;过程中不允许其他线程修改这条数据。
例:扣库存:先查库存 > 0,再扣减。不加事务会出现**超卖**。
注意:
- 单纯 select 不会加锁;
- 需要锁要配合@Transactional+ 悲观锁 / select ... for update或者乐观锁版本号
3)业务失败必须回滚中间全部变更
调用多个 Mapper/DAO,中间抛出异常,前面已经入库的数据必须撤销。
4) 需要控制隔离级别、避免脏读 / 不可重复读
业务对读取数据一致性要求高,需要指定隔离级别。
5) 需要用到传播行为特性
比如日志需要独立事务 REQUIRES_NEW;嵌套业务需要 NESTED保存点
2.不需要事务的场景
1) 只有单条 SQL(insert /update/delete)
// 单条更新,数据库本身自动事务,不需要 @Transactional userMapper.updateById(user);就算不加注解,这条语句要么成功要么失败,不会半截执行。再加事务属于多余开销,开启事务、提交都有性能损耗
误区:所有 service 方法统一加 @Transactional,造成不必要事务开启,连接占用时间变长,高并发下加剧数据库锁、长事务问题。
2) 纯查询方法,没有任何写操作
- 普通列表查询、详情查询,只读,没有新增修改删除。
- 不要随便加@Transactional(readOnly = true),不是不能加,但不是必须。
补充:
- readOnly=true作用:给底层数据库驱动一个 hint,部分驱动会做优化;不会锁表,不能防止数据被别的线程修改。
- 如果只是简单查询,直接去掉注解,性能更好。
3) 业务允许部分成功部分失败
批量导入,允许一部分成功、一部分失败,失败记录单独记录日志,不希望全部回滚。
这种场景反而不能加大事务;捕获单条异常,跳过错误,继续处理下一条。
4) 纯内存逻辑,完全不操作数据库
方法只有参数校验、调用 RPC、调用 http、内存计算,没有 DB 操作,加事务毫无意义
3. 灰色场景(容易踩坑)
1) 方法里面有 DB 操作,也有 RPC/HTTP 调用
@Transactional public void createOrder(){ orderMapper.insert(order); // 远程调用,网络超时卡住 remotePayApi.call(); }RPC 网络 IO 放在事务内部 → 长事务大坑。数据库连接长期不释放,锁持有时间变长,数据库压力暴涨。
正确:把远程调用移出事务边界,事务尽量小,只包裹数据库操作。
2) 循环批量处理
// 错误:大事务,全部在一个事务,数据量大会回滚日志暴涨,锁时间长 @Transactional public void batchSave(List<data> list){ for(Data d : list){ mapper.insert(d); } }- 如果要求全部成功全部失败:保留事务。
- 如果允许部分失败:去掉外层大事务,分批提交,捕获异常。
三、经典业务场景
1.下单主业务失败,希望错误日志一定要落库,日志不能回滚?
👉 REQUIRES_NEW,内层异常必须 try‑catch。
2.一个大事务,里面某个子步骤失败只回滚子步骤,外层可以继续提交;外层失败全部回滚?
👉 NESTED 嵌套事务
3.查询接口,外层有事务就参与事务,没有事务就直接查询?
👉 SUPPORTS
4.内部更新方法,强制调用者必须开启事务,否则直接报错,防止漏事务?
👉 MANDATORY
5.批量导入几万条数据,不想长事务,外层有事务要挂起?
👉 NOT_SUPPORTED
四、坑点
1. 传播机制生效前提:方法必须是外部调用,不能本类内部调用!
A 类的 a () 方法调用本类的 b (),b 上 @Transactional 传播机制完全失效,因为不走 AOP 代理对象。要注入自己或者拆分成不同 Service。
2.REQUIRES_NEW 一定要捕获内层异常。
如果 REQUIRES_NEW 方法抛出异常没有 catch,异常向上抛,外层事务依然会标记回滚。内层事务已经回滚,外层也回滚。
3. NESTED 底层是 JDBC savepoint,Mybatis+MySQL InnoDB 可用;分布式 JTA 环境不支持。
4.REQUIRES_NEW 会挂起事务,占用数据库连接;大量使用会消耗连接池,不要滥用。