@Transactional 的传播行为
定义与作用
propagation 是 @Transactional 的核心属性之一,用于定义当前方法的事务边界如何与已存在的事务上下文交互。在多层 Service 调用的企业级应用中,不同业务方法对事务的需求各不相同:有些方法需要独立提交,有些方法需要加入外层事务,有些方法则明确要求在无事务环境中执行。
Spring 将事务传播行为抽象为 7 种策略,由 org.springframework.transaction.annotation.Propagation 枚举定义。理解传播行为是掌握 Spring 事务管理的关键,也是面试中区分"会用"与"精通"的分水岭。
适用位置与常用属性
propagation 属性只能在 @Transactional 注解的方法级别或类级别使用,与 isolation、timeout 等属性并列配置:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String operation) { ... }
七种传播行为对比表
| 传播行为 | 当前无事务 | 当前有事务 | 核心语义 |
|---|---|---|---|
| REQUIRED(默认) | 新建事务 | 加入当前事务 | "随大流",最常用 |
| REQUIRES_NEW | 新建事务 | 挂起当前事务,新建独立事务 | "我行我素",必须独立提交 |
| SUPPORTS | 非事务执行 | 加入当前事务 | "可有可无",查询方法适用 |
| NOT_SUPPORTED | 非事务执行 | 挂起当前事务,非事务执行 | "拒绝事务",发送通知适用 |
| MANDATORY | 抛出 IllegalTransactionStateException | 加入当前事务 | "强制要求",必须在外层事务中调用 |
| NEVER | 非事务执行 | 抛出 IllegalTransactionStateException | "严禁事务",纯查询适用 |
| NESTED | 新建事务 | 在当前事务中创建 savepoint 嵌套事务 | "局部回滚",部分失败可独立回滚 |
重要区别:
REQUIRES_NEW是挂起外层事务后创建完全独立的新事务,两个事务的提交/回滚互不影响。NESTED是在当前事务中创建一个savepoint,嵌套事务回滚时只回滚到 savepoint,外层事务仍可继续提交。
核心原理:传播行为的判定流程
Spring 的 TransactionInterceptor 在方法调用前,通过 PlatformTransactionManager.getTransaction() 根据传播行为决定事务策略:
REQUIRED vs REQUIRES_NEW 时序图
关键观察:REQUIRES_NEW 的审计日志事务(Tx2)在订单事务(Tx1)提交前就已经独立提交。即使后续 Tx1 回滚,审计日志仍然保留。
NESTED 的 savepoint 机制
数据库支持限制:
NESTED依赖数据库的 savepoint 支持(JDBC 3.0+)。若底层驱动不支持 savepoint,Spring 会降级为REQUIRES_NEW行为。
完整示例:飞翔科技订单与审计系统
场景简述
飞翔科技公司的电商平台有两个核心服务:
- OrderService:处理订单创建,需要事务保证订单、库存、余额的一致性;
- AuditService:记录操作审计日志,无论订单是否成功,日志都必须持久化。
小崔(后端开发)需要为这两个服务配置不同的事务传播行为,以满足业务需求。
操作前:数据库表结构与初始数据
-- 订单表
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product_name VARCHAR(100),
amount DECIMAL(10,2),
status VARCHAR(20)
);
-- 库存表
CREATE TABLE inventory (
product_id INT PRIMARY KEY,
stock INT NOT NULL
);
-- 审计日志表
CREATE TABLE audit_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
operation VARCHAR(100),
detail TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 初始数据
INSERT INTO inventory (product_id, stock) VALUES (10086, 10);
Service 层:不同传播行为的配置
package com.feixiang.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private AuditService auditService;
// 订单创建:REQUIRED(默认),加入或新建事务
@Transactional
public void createOrder(int userId, int productId, String productName, double amount) {
// 1. 扣减库存
int updated = jdbcTemplate.update(
"UPDATE inventory SET stock = stock - 1 WHERE product_id = ? AND stock > 0",
productId
);
if (updated == 0) {
throw new IllegalStateException("库存不足,商品ID:" + productId);
}
// 2. 插入订单
jdbcTemplate.update(
"INSERT INTO orders (user_id, product_name, amount, status) VALUES (?, ?, ?, ?)",
userId, productName, amount, "CREATED"
);
System.out.println("[OrderService] 订单创建成功,用户:" + userId);
// 3. 记录审计日志(独立事务,不受订单回滚影响)
auditService.logOperation("CREATE_ORDER", "用户" + userId + "创建订单,商品:" + productName);
// 4. 模拟异常:触发订单回滚,但审计日志已独立提交
if (amount > 10000) {
throw new RuntimeException("订单金额超限,触发回滚测试");
}
}
}
package com.feixiang.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AuditService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 审计日志:REQUIRES_NEW,独立事务,必须提交
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation(String operation, String detail) {
jdbcTemplate.update(
"INSERT INTO audit_log (operation, detail) VALUES (?, ?)",
operation, detail
);
System.out.println("[AuditService] 审计日志已记录:" + operation);
}
// 查询审计日志:SUPPORTS,有事务则加入,无则非事务查询
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public int countLogs(String operation) {
return jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM audit_log WHERE operation = ?",
Integer.class, operation
);
}
// 发送通知:NOT_SUPPORTED,挂起事务,非事务执行
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendNotification(String message) {
// 模拟发送短信/邮件,不需要事务
System.out.println("[Notification] 发送通知:" + message);
}
}
测试执行与结果
package com.feixiang;
import com.feixiang.service.AuditService;
import com.feixiang.service.OrderService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class PropagationTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext("com.feixiang");
OrderService orderService = ctx.getBean(OrderService.class);
AuditService auditService = ctx.getBean(AuditService.class);
// 场景一:正常订单(金额 200,不超限)
try {
orderService.createOrder(2024001, 10086, "机械键盘", 200.00);
} catch (Exception e) {
System.err.println("场景一异常:" + e.getMessage());
}
// 场景二:金额超限,触发订单回滚,观察审计日志是否保留
try {
orderService.createOrder(2024002, 10086, "高端显卡", 20000.00);
} catch (Exception e) {
System.err.println("场景二异常:" + e.getMessage());
}
// 场景三:查询审计日志数量
int logCount = auditService.countLogs("CREATE_ORDER");
System.out.println("[查询结果] CREATE_ORDER 审计日志数量:" + logCount);
ctx.close();
}
}
操作后:控制台输出与数据变化
场景一(正常订单)控制台输出:
[OrderService] 订单创建成功,用户:2024001
[AuditService] 审计日志已记录:CREATE_ORDER
场景一数据库状态:
orders 表:
| order_id | user_id | product_name | amount | status |
|---|---|---|---|---|
| 1 | 2024001 | 机械键盘 | 200.00 | CREATED |
inventory 表:
| product_id | stock |
|---|---|
| 10086 | 9 |
audit_log 表:
| id | operation | detail | created_at |
|---|---|---|---|
| 1 | CREATE_ORDER | 用户2024001创建订单,商品:机械键盘 | ... |
场景二(金额超限,触发回滚)控制台输出:
[OrderService] 订单创建成功,用户:2024002
[AuditService] 审计日志已记录:CREATE_ORDER
场景二异常:订单金额超限,触发回滚测试
场景二数据库状态:
orders 表:无新增记录(订单回滚)。
inventory 表:
| product_id | stock |
|---|---|
| 10086 | 9(未变化,回滚生效) |
audit_log 表:
| id | operation | detail | created_at |
|---|---|---|---|
| 1 | CREATE_ORDER | 用户2024001创建订单,商品:机械键盘 | ... |
| 2 | CREATE_ORDER | 用户2024002创建订单,商品:高端显卡 | ... |
关键验证:尽管订单事务回滚了,但 audit_log 表中的第二条记录仍然存在。这证明了 REQUIRES_NEW 的审计日志事务是独立提交的,不受外层订单事务回滚的影响。
场景三(查询审计日志):
[查询结果] CREATE_ORDER 审计日志数量:2
SUPPORTS 传播行为在此场景下以非事务方式执行查询,因为没有外层事务上下文。
易错场景与面试考点
反例一:误用 REQUIRED 导致审计日志随主事务回滚
@Service
public class OrderService {
@Autowired
private AuditService auditService;
@Transactional
public void createOrder(...) {
// ... 订单逻辑
auditService.logOperation("CREATE_ORDER", detail); // AuditService 使用 REQUIRED
// ... 后续异常导致回滚
}
}
@Service
public class AuditService {
// 错误:使用默认 REQUIRED,审计日志会加入外层事务
@Transactional
public void logOperation(String operation, String detail) {
// 插入审计日志
}
}
问题分析:AuditService.logOperation() 使用默认的 REQUIRED,会加入 OrderService 的现有事务。当订单事务回滚时,审计日志也会被回滚,导致"操作发生了但无记录"的合规风险。审计日志必须独立持久化,应使用 REQUIRES_NEW。
反例二:REQUIRES_NEW 的异常向上传播导致外层事务回滚
@Service
public class OrderService {
@Transactional
public void createOrder(...) {
// ... 订单逻辑
auditService.logOperation("CREATE_ORDER", detail);
// 如果 logOperation 抛出异常,异常会向上传播到 OrderService
// 导致 OrderService 的事务也回滚!
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation(String operation, String detail) {
// 如果此处抛出异常...
throw new RuntimeException("审计系统故障");
}
}
问题分析:REQUIRES_NEW 确保内层事务独立提交,但无法隔离异常传播。如果 AuditService 抛出的异常未被 OrderService catch,异常会继续向上传播,触发 Spring 的默认回滚规则(RuntimeException),导致外层订单事务也回滚。
正确写法:
@Transactional
public void createOrder(...) {
// ... 订单逻辑
try {
auditService.logOperation("CREATE_ORDER", detail);
} catch (Exception e) {
// 审计失败不影响主业务,记录日志即可
System.err.println("审计记录失败:" + e.getMessage());
}
// ... 继续执行
}
反例三:NESTED 在不支持 savepoint 的数据库上降级
@Service
public class PaymentService {
@Transactional(propagation = Propagation.NESTED)
public void processPartialPayment(...) {
// 期望:失败时只回滚这部分,外层继续
}
}
问题分析:NESTED 依赖 JDBC savepoint。某些旧版数据库驱动或特定数据源(如某些连接池的 XA 场景)可能不支持 savepoint。此时 Spring 会降级为 REQUIRES_NEW,行为从"局部回滚"变为"完全独立事务",可能导致与预期不符的数据状态。生产环境使用 NESTED 前,必须验证底层驱动的 savepoint 支持。
面试高频考点
Q1:REQUIRED 和 REQUIRES_NEW 的本质区别是什么?
REQUIRED是"加入或新建":有事务则加入,无则新建,内外层共享同一个物理事务,同生共死。REQUIRES_NEW是"挂起并新建":无论当前是否有事务,都创建全新的独立事务,外层事务被挂起,内层事务独立提交或回滚,互不影响。
Q2:NESTED 与 REQUIRES_NEW 有什么区别?
NESTED在当前事务中创建一个 savepoint,嵌套事务回滚时只回滚到该 savepoint,外层事务仍可继续并提交。REQUIRES_NEW是创建完全独立的物理事务,两个事务有各自的事务状态和资源。NESTED需要数据库支持 savepoint,且内层异常若未被捕获仍会触发外层回滚;REQUIRES_NEW的内层事务提交不受外层影响,但异常传播仍可能触发外层回滚。
Q3:MANDATORY 的典型使用场景是什么?
MANDATORY要求调用方必须已经开启事务,否则抛出异常。典型场景是核心业务校验方法:例如订单金额校验、库存预占等操作,必须在外层事务的保护下执行,防止非事务调用导致数据不一致。它起到一种"契约检查"的作用,强制上层调用者提供事务上下文。
Q4:SUPPORTS 和 NOT_SUPPORTED 有什么区别?
SUPPORTS是"随遇而安":有事务则加入,无则非事务执行,常用于查询方法。NOT_SUPPORTED是"主动排斥":有事务则挂起,无则非事务执行,常用于不需要事务且不希望被外层事务拖累的操作(如发送通知、调用外部 HTTP 接口)。NOT_SUPPORTED会显式挂起现有事务,而SUPPORTS不会。