如果说事务隔离级别是数据库层面的"并发交通规则",那么事务传播属性 就是应用框架(如Spring)层面的"团队协作规则"------当多个事务方法嵌套调用时(比如Service A调用Service B,两者都标注了@Transactional),传播属性定义了"新任务是否独立开""能否加入现有团队""出错时如何收尾"等核心问题。
本文用"公司项目团队协作"的比喻,结合Spring的七种传播属性,从定义、场景、代码、调用关系到底层原理,彻底讲透事务传播的来龙去脉。
一、先搞懂:为什么需要"传播属性"?
想象你是项目经理,手下有多个团队(事务方法),现在要执行一个复杂项目(业务流程):
- 团队A负责"核心开发"(主事务),调用团队B的"测试"(子事务)、团队C的"部署"(子事务);
- 问题来了:如果"测试"失败,"核心开发"要不要回滚?"部署"能不能单独执行?
传播属性就是你的"团队调度手册",规定每个子团队(子事务)如何与主团队(主事务)协作:是加入主团队共进退,还是独立开小灶?是必须依赖主团队,还是干脆不进团队?
二、Spring七种传播属性:从"协作"到"独立"的六种角色
Spring定义了七种传播属性,按"协作紧密程度"从强到弱排序如下:
1. REQUIRED(默认):"加入团队,共进退"
📌 定义
如果当前存在事务(主团队),则子事务加入主事务 ,共享同一个"任务上下文"(数据库连接、锁、回滚规则);如果当前无事务,则新建主事务。
🎭 比喻:项目核心团队的"全员协作"
- 主团队(主事务)已成立(比如"核心开发"团队),子团队(子事务)"测试"加入主团队,共用一个项目计划(事务);
- 若"测试"失败(抛异常),整个项目(主事务)回滚,"核心开发"的成果也作废;
- 若"测试"成功,"部署"也加入主团队,最终一起提交(commit)。
💼 适用场景
绝大多数核心业务,比如"下单"流程(扣库存+创建订单+扣余额),所有步骤必须原子性(要么全成,要么全败)。
💻 代码示例
java
@Service
public class OrderService { // 主团队:核心开发
@Autowired private StockService stockService; // 子团队:测试
@Autowired private OrderDao orderDao;
@Transactional // 默认REQUIRED(可省略)
public void createOrder(Long goodsId, int num) {
stockService.deduct(goodsId, num); // 加入主事务,共享连接
orderDao.insert(new Order(goodsId, num)); // 同一事务
}
}
@Service
public class StockService { // 子团队:测试
@Autowired private StockDao stockDao;
@Transactional(propagation = Propagation.REQUIRED) // 显式声明(默认也是它)
public void deduct(Long goodsId, int num) {
stockDao.updateStock(goodsId, -num); // 失败时,整个createOrder回滚
}
}
⚙️ 底层原理
- 若主事务存在,子事务通过
ThreadLocal获取主事务的连接,加入同一事务上下文; - 若主事务不存在,Spring事务管理器(如
DataSourceTransactionManager)创建新连接,开启新事务。
2. SUPPORTS:"灵活参与,不强求"
📌 定义
如果当前存在事务,则子事务加入主事务 ;如果当前无事务,则以非事务方式执行(自动提交模式)。
🎭 比喻:项目"顾问团队"的灵活协作
- 主团队(主事务)存在时,顾问团队(子事务)加入,按项目计划执行(比如"风险评估");
- 若主团队不存在(比如临时咨询),顾问团队就单独行动,做完即止(无需事务)。
💼 适用场景
非核心读操作,比如"查询订单详情"(允许非事务执行以提升性能)。
💻 代码示例
java
@Service
public class OrderQueryService {
@Autowired private OrderDao orderDao;
@Transactional(propagation = Propagation.SUPPORTS) // 有事务则加入,无则非事务
public Order getOrder(Long id) {
return orderDao.selectById(id); // 读操作,无需强事务
}
}
3. MANDATORY:"必须加入,否则罢工"
📌 定义
子事务必须在已有事务中执行 ,如果当前无事务,则直接抛异常(IllegalTransactionStateException)。
🎭 比喻:项目"法务审核"的强制要求
- 法务团队(子事务)必须在主团队(主事务)成立后才能介入(比如"合同审批");
- 若没人牵头成立主团队(无主事务),法务团队直接拒绝工作(抛异常)。
💼 适用场景
核心业务的强制依赖,比如"支付回调"(必须由上层订单事务驱动,避免单独调用导致数据不一致)。
💻 代码示例
java
@Service
public class PaymentCallbackService {
@Transactional(propagation = Propagation.MANDATORY) // 必须有主事务
public void handleCallback(Long orderId, String status) {
if ("SUCCESS".equals(status)) {
orderDao.updateStatus(orderId, "PAID"); // 必须在订单事务中执行
}
}
}
4. REQUIRES_NEW:"独立开小灶,各管各的"
📌 定义
无论当前是否存在事务,子事务都新建独立事务 ,原事务(若存在)会被挂起(暂停),子事务执行完后恢复原事务。
🎭 比喻:项目"紧急插队任务"
- 主团队(主事务)正在执行"核心开发",突然来了一个"紧急修复bug"任务(子事务);
- 项目经理暂停主团队,让"紧急任务"团队独立开小灶(新建事务),做完后提交;
- 主团队恢复执行,两者互不影响(紧急任务成功/失败,主团队都可能继续)。
💼 适用场景
非核心但需独立记录的操作,比如"操作日志""短信通知"(即使主业务回滚,日志/通知仍需保存)。
💻 代码示例
java
@Service
public class OrderService { // 主团队:核心开发
@Autowired private LogService logService; // 子团队:紧急日志
@Autowired private OrderDao orderDao;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Long goodsId, int num) {
try {
orderDao.insert(new Order(goodsId, num));
logService.recordLog("订单创建成功"); // 独立事务
} catch (Exception e) {
logService.recordLog("订单创建失败:" + e.getMessage()); // 仍会提交
throw e; // 主事务回滚(订单插入撤销)
}
}
}
@Service
public class LogService { // 子团队:紧急日志(独立事务)
@Autowired private LogDao logDao;
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新建独立事务
public void recordLog(String content) {
logDao.insert(new Log(content)); // 独立提交,不受主事务回滚影响
}
}
⚙️ 底层原理
- Spring通过连接池挂起实现:暂停主事务的连接,从池中获取新连接开启子事务;
- 子事务提交/回滚后,恢复主事务的连接,继续执行。
5. NOT_SUPPORTED:"脱离团队,独自摸鱼"
📌 定义
子事务以非事务方式执行 ,若当前存在事务,则挂起主事务(暂停),执行完后恢复原事务。
🎭 比喻:项目"临时调研"的独立行动
- 主团队(主事务)正在开会(执行事务),调研小组(子事务)说"我们出去转转,不用管会议进度";
- 调研小组脱离团队独自行动(非事务执行),回来后主团队继续开会(恢复事务)。
💼 适用场景
耗时且无需事务的操作,比如"导出大数据报表"(避免长时间占用事务连接)。
💻 代码示例
java
@Service
public class ReportService {
@Autowired private DataExportDao exportDao;
@Transactional(propagation = Propagation.NOT_SUPPORTED) // 脱离事务
public void exportBigData() {
exportDao.queryAndWriteToFile("big_data.csv"); // 无需事务,避免长事务锁表
}
}
6. NEVER:"拒绝团队,独来独往"
📌 定义
子事务必须以非事务方式执行 ,若当前存在事务,则直接抛异常(IllegalTransactionStateException)。
🎭 比喻:项目"外部顾问"的原则
- 外部顾问(子事务)只接"独立咨询"(非事务),若项目组硬拉他加入团队(主事务),他直接拒绝(抛异常)。
💼 适用场景
禁止被事务包裹的工具类方法,比如"数据校验"(确保不被意外纳入事务导致锁等待)。
💻 代码示例
java
@Service
public class ValidatorService {
@Transactional(propagation = Propagation.NEVER) // 禁止在事务中执行
public void validateOrder(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("金额非法");
}
}
}
7. NESTED:"分阶段审批,可局部回滚"
📌 定义
如果当前存在事务,子事务在主事务中创建保存点(Savepoint),作为"子任务阶段";如果当前无事务,则等价于REQUIRED(新建事务)。
- 部分回滚:子事务失败仅回滚到保存点,不影响主事务其他部分;
- 整体提交:子事务成功提交后,修改对外层事务可见,但最终需主事务提交才持久化。
🎭 比喻:项目"分阶段审批流程"
- 主团队(主事务)启动项目,设置"阶段1审批"保存点(子事务);
- 阶段1(子事务)若失败,仅回滚阶段1(回到保存点),主团队可修改后重试;
- 阶段1成功,主团队继续"阶段2",最终一起提交。
💼 适用场景
批量操作的部分失败重试,比如"批量导入订单"(单条失败不影响其他)。
💻 代码示例
java
@Service
public class BatchOrderService { // 主团队:批量导入
@Autowired private OrderService orderService;
@Transactional(propagation = Propagation.REQUIRED)
public void batchImport(List<Order> orders) {
for (Order order : orders) {
try {
orderService.importSingle(order); // 子事务(NESTED)
} catch (Exception e) {
log.error("单条导入失败", e); // 仅回滚当前子事务
}
}
}
}
@Service
public class OrderService { // 子团队:单条导入(分阶段审批)
@Autowired private OrderDao orderDao;
@Transactional(propagation = Propagation.NESTED) // 基于保存点的子事务
public void importSingle(Order order) {
orderDao.insert(order); // 失败时,仅回滚此插入(回到保存点)
}
}
⚙️ 底层原理
- 依赖数据库的保存点(Savepoint) 机制(如MySQL的
SAVEPOINT、Oracle的SAVEPOINT); - 子事务执行时,Spring在数据库中设置一个保存点,失败时通过
ROLLBACK TO SAVEPOINT回滚到该点,不影响主事务其他操作。
三、核心原则:调用方与被调用方的"规则博弈"
理解了七种传播属性后,关键问题是:调用方A方法与被调用方B方法的传播属性如何互动?
3.1 一句话总结关系
被调用方B的传播属性是"行为指令"(决定"怎么做"),调用方A的事务状态(是否有事务)是"环境条件"(决定"在什么环境下做")。
3.2 两种场景拆解
场景1:调用方A无事务(未开启事务,或A的传播属性导致未开启事务)
此时"当前不存在事务",B的行为完全由其传播属性决定:
| B的传播属性 | B的行为(当前无事务时) | 底层逻辑 |
|---|---|---|
| REQUIRED | 新建事务(B为主事务) | 无当前事务 → 按"无则新建"规则开启新事务 |
| SUPPORTS | 非事务执行(自动提交) | 无当前事务 → 按"无则非事务"规则执行 |
| MANDATORY | 抛异常(IllegalTransactionStateException) |
无当前事务 → 违反"必须有事务"规则 |
| REQUIRES_NEW | 新建独立事务(B为主事务) | 无当前事务 → 按"无论有无均新建"规则开启新事务 |
| NOT_SUPPORTED | 非事务执行(脱离事务) | 无当前事务 → 按"脱离事务"规则执行 |
| NEVER | 非事务执行(允许) | 无当前事务 → 符合"必须以非事务执行"规则 |
| NESTED | 新建事务(等价于REQUIRED) | 无当前事务 → 基于保存点的嵌套无意义,退化为REQUIRED |
场景2:调用方A有事务(A已开启事务,或A的上层调用者开启了事务)
此时"当前存在主事务T",B的行为由其传播属性结合"主事务T的存在"决定:
| B的传播属性 | B的行为(当前有主事务T时) | 底层逻辑 |
|---|---|---|
| REQUIRED | 加入主事务T(共享T的连接、锁、回滚规则) | 按"有则加入"规则,B成为T的一部分 |
| SUPPORTS | 加入主事务T(同REQUIRED) | 按"有则加入"规则,B使用T的连接 |
| MANDATORY | 加入主事务T(必须依赖T) | 按"必须有事务"规则,B直接使用T的连接 |
| REQUIRES_NEW | 挂起T → 新建独立事务T' → 执行B → 恢复T | 按"无论有无均新建"规则,B开启独立事务T' |
| NOT_SUPPORTED | 挂起T → 非事务执行B → 恢复T | 按"脱离事务"规则,B以非事务模式执行 |
| NEVER | 抛异常(IllegalTransactionStateException) |
按"必须以非事务执行"规则,当前有事务T,违反规则 |
| NESTED | 在T中创建保存点S → 执行B → 成功则提交到T,失败则回滚到S | 按"有则基于保存点"规则,B作为T的子事务(嵌套事务) |
3.3 关键结论
- B的行为听B的注解:被调用方B的传播属性决定其行为(加入、新建、独立等);
- B的环境看A有没有开事务:调用方A的事务状态(有无事务)是行为执行的"前提条件"。
举例验证:
- 若A无事务,B的传播属性为
REQUIRED→ B新建事务(因"无则新建"); - 若A有事务,B的传播属性为
REQUIRED→ B加入A的事务(因"有则加入"); - 若A有事务,B的传播属性为
REQUIRES_NEW→ B挂起A的事务,新建独立事务(因"无论有无均新建")。
四、底层实现:Spring如何用AOP"操控"事务传播?
传播属性的实现依赖Spring的AOP(面向切面编程) 和事务管理器 (如PlatformTransactionManager),核心步骤如下:
- 代理拦截 :当调用
@Transactional方法时,Spring通过动态代理拦截调用,检查当前事务状态(是否有活跃事务、传播属性要求); - 事务状态机:根据传播属性决定是否新建事务、挂起现有事务、设置保存点;
- 连接管理 :通过
TransactionSynchronizationManager绑定连接、保存点、事务状态到ThreadLocal,确保同一线程内事务上下文一致; - 异常处理 :根据异常类型(默认
RuntimeException回滚)决定提交/回滚,嵌套事务通过保存点实现局部回滚。
五、常见误区与实践建议
5.1 三大误区澄清
- 误区1 :"
@Transactional注解的方法一定开启事务"
→ 真相:传播属性为SUPPORTS/NOT_SUPPORTED/NEVER且无主事务时,不开启事务(自动提交模式)。 - 误区2 :"混淆
NESTED和REQUIRES_NEW"
→ 真相:NESTED依赖主事务(共享连接,基于保存点,部分回滚);REQUIRES_NEW独立事务(新连接,完全隔离)。 - 误区3 :"调用方A的传播属性决定被调用方B的行为"
→ 真相:A的传播属性仅决定A自身是否开启事务(前提),B的行为由B自身的传播属性(指令)决定。
5.2 四大实践建议
- 核心业务用REQUIRED(默认):确保原子性(如下单、转账);
- 非核心日志/通知用REQUIRES_NEW:独立记录(避免主事务回滚导致丢失);
- 批量操作优先用NESTED:支持单条失败局部回滚(如批量导入);
- 避免MANDATORY/NEVER滥用:仅在明确需要强制依赖或无事务时使用,防止抛异常中断流程。
六、总结:传播属性的本质是"事务边界的灵活编排"
事务传播属性是应用层对"事务如何协作"的精细化控制,通过七种行为(加入、独立、挂起、保存点等),让开发者能根据业务场景(核心/非核心、批量/单条、读/写)灵活定义事务边界。
记住:没有最好的传播属性,只有最适合业务场景的选择。理解每种传播属性的"协作规则",才能在复杂业务中既保证数据一致性,又避免不必要的性能损耗。
就像项目经理根据任务性质调度团队一样,用好传播属性,让你的代码"团队协作"更高效!