作为后端开发,Spring 事务是日常工作的基础,但不少人只会用 @Transactional 注解加个 rollbackFor,对底层的事务传播行为一知半解。直到遇到"嵌套调用事务不回滚""重复提交导致数据异常"等问题,才发现对传播行为的理解不足会踩大坑。
其实事务传播行为的核心很简单:当一个带有事务的方法,调用另一个方法时,如何决定新方法的事务边界(是复用当前事务,还是新建事务,或是不参与事务)。Spring 定义了 7 种标准传播行为,本文结合实际业务场景,逐一拆解每种行为的用法、代码示例和适用场景,帮你彻底吃透。
先铺垫两个基础前提,避免理解偏差:
-
所有示例基于 Spring Boot 2.x+,依赖
spring-boot-starter-data-jpa或mybatis-plus(本文用 JPA 简化数据库操作); -
事务传播行为仅对
@Transactional注解修饰的方法生效,且必须通过 Spring 代理调用(同类方法内部调用需注意代理失效问题)。
一、Spring 7 种事务传播行为全解析
Spring 事务传播行为通过 propagation 属性配置,默认值为 REQUIRED。下面按"日常使用率"排序,逐一讲解。
1. REQUIRED(默认):如果有事务就复用,没有就新建
核心逻辑:这是最常用的传播行为,遵循"能复用则复用,无则新建"的原则。如果调用方已经存在事务,被调用方就加入当前事务,两者共用一个事务边界(要么一起提交,要么一起回滚);如果调用方没有事务,被调用方就新建一个独立事务。
业务场景:绝大多数核心业务流程,比如"创建订单+扣减库存",两者必须在同一事务中,要么都成功,要么都失败。
代码示例:
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private StockService stockService;
// 调用方:带有事务
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Integer count, Long userId) {
// 1. 创建订单
Order order = new Order();
order.setProductId(productId);
order.setCount(count);
order.setUserId(userId);
order.setStatus(1); // 待支付
orderRepository.save(order);
// 2. 调用扣减库存方法(复用当前事务)
stockService.deductStock(productId, count);
}
}
@Service
public class StockService {
@Autowired
private StockRepository stockRepository;
// 被调用方:传播行为为 REQUIRED(默认,可省略)
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void deductStock(Long productId, Integer count) {
Stock stock = stockRepository.findByProductId(productId)
.orElseThrow(() -> new RuntimeException("库存不存在"));
if (stock.getCount() < count) {
throw new RuntimeException("库存不足");
}
stock.setCount(stock.getCount() - count);
stockRepository.save(stock);
}
}
结果说明:
-
如果
deductStock抛出异常(如库存不足),createOrder的订单创建操作会一起回滚,不会出现"有订单无库存扣减"的脏数据; -
如果调用方
createOrder没有加@Transactional,deductStock会新建独立事务,仅库存扣减操作受事务控制。
2. SUPPORTS:如果有事务就复用,没有就无事务
核心逻辑:被调用方"被动"参与事务,不主动创建事务。如果调用方有事务,就加入其中;如果调用方没有事务,就以无事务方式执行。
业务场景:查询类方法,既可以在事务中执行(保证查询到未提交的事务数据,如分布式事务中的一致性查询),也可以独立执行(普通查询场景)。
代码示例:
java
@Service
public class OrderQueryService {
@Autowired
private OrderRepository orderRepository;
// 传播行为为 SUPPORTS,不主动创建事务
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Order getOrderById(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
}
}
// 调用场景1:调用方有事务
@Service
public class OrderOperateService {
@Autowired
private OrderQueryService queryService;
@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus(Long orderId, Integer status) {
// 复用当前事务查询订单(能查询到未提交的临时数据)
Order order = queryService.getOrderById(orderId);
order.setStatus(status);
orderRepository.save(order);
}
}
// 调用场景2:调用方无事务
@Controller
public class OrderController {
@Autowired
private OrderQueryService queryService;
@GetMapping("/order/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
// 无事务方式执行查询
return ResponseEntity.ok(queryService.getOrderById(id));
}
}
注意点:SUPPORTS 修饰的方法如果在无事务环境下执行,所有数据库操作都是自动提交的,无法回滚。
3. MANDATORY:必须在已有事务中执行,否则抛异常
核心逻辑 :被调用方强制要求调用方有事务,自身不新建事务。如果调用方没有事务,直接抛出 IllegalTransactionStateException 异常,拒绝执行。
业务场景:必须依赖调用方事务的操作,比如"订单状态变更日志记录",必须和订单状态变更在同一事务中,确保日志与业务操作一致,不允许独立执行。
代码示例:
java
@Service
public class OrderLogService {
@Autowired
private OrderLogRepository logRepository;
// 必须在已有事务中执行,否则抛异常
@Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
public void recordLog(Long orderId, Integer oldStatus, Integer newStatus) {
OrderLog log = new OrderLog();
log.setOrderId(orderId);
log.setOldStatus(oldStatus);
log.setNewStatus(newStatus);
log.setOperateTime(LocalDateTime.now());
logRepository.save(log);
}
}
// 正确调用:调用方有事务
@Service
public class OrderService {
@Autowired
private OrderLogService logService;
@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus(Long orderId, Integer newStatus) {
Order order = orderRepository.findById(orderId).orElseThrow();
Integer oldStatus = order.getStatus();
order.setStatus(newStatus);
orderRepository.save(order);
// 正常执行,复用当前事务
logService.recordLog(orderId, oldStatus, newStatus);
}
// 错误调用:调用方无事务
public void errorUpdateStatus(Long orderId, Integer newStatus) {
// 调用 recordLog 时会抛 IllegalTransactionStateException
logService.recordLog(orderId, 1, newStatus);
}
}
4. REQUIRES_NEW:无论是否有事务,都新建独立事务
核心逻辑:被调用方强制新建一个独立事务,与调用方事务完全隔离(两个事务互不影响,各自提交/回滚)。如果调用方已有事务,会先暂停当前事务,待新事务执行完成后,再恢复原事务。
业务场景:需要独立存在的操作,比如"订单创建失败后记录异常日志",即使订单创建事务回滚,日志也必须保留,不能被回滚影响。
代码示例:
java
@Service
public class OrderErrorLogService {
@Autowired
private OrderErrorLogRepository errorLogRepository;
// 新建独立事务,与调用方事务隔离
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void recordErrorLog(Long productId, Integer count, String errorMsg) {
OrderErrorLog errorLog = new OrderErrorLog();
errorLog.setProductId(productId);
errorLog.setCount(count);
errorLog.setErrorMsg(errorMsg);
errorLog.setCreateTime(LocalDateTime.now());
errorLogRepository.save(errorLog);
}
}
@Service
public class OrderService {
@Autowired
private OrderErrorLogService errorLogService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Integer count, Long userId) {
try {
// 模拟订单创建失败(如库存不足)
throw new RuntimeException("订单创建失败:库存不足");
} catch (Exception e) {
// 记录错误日志,新建独立事务,即使当前事务回滚,日志也会保留
errorLogService.recordErrorLog(productId, count, e.getMessage());
// 重新抛出异常,让当前事务回滚
throw e;
}
}
}
结果说明 :createOrder 事务回滚,但 recordErrorLog 新建的独立事务会正常提交,错误日志成功保存,实现"业务回滚但日志留存"的需求。
5. NOT_SUPPORTED:无论是否有事务,都以无事务方式执行
核心逻辑:被调用方拒绝参与任何事务。如果调用方有事务,会先暂停当前事务,待被调用方无事务执行完成后,再恢复原事务;如果调用方无事务,直接正常执行。
业务场景:不需要事务的耗时操作,比如"订单创建后发送短信通知",即使通知失败,也不能影响订单创建事务的提交;或者不允许在事务中执行的操作(如批量数据同步,避免长时间占用事务资源)。
代码示例:
java
@Service
public class SmsService {
// 拒绝参与事务,以无事务方式执行
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendOrderSms(Long userId, Long orderId) {
// 模拟短信发送(耗时操作,无事务)
try {
Thread.sleep(1000);
System.out.println("向用户" + userId + "发送订单" + orderId + "创建成功短信");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("短信发送失败");
}
}
}
@Service
public class OrderService {
@Autowired
private SmsService smsService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Integer count, Long userId) {
// 1. 创建订单(事务内操作)
Order order = new Order();
// ... 订单赋值逻辑
orderRepository.save(order);
// 2. 发送短信(无事务,即使失败也不影响订单提交)
try {
smsService.sendOrderSms(userId, order.getId());
} catch (Exception e) {
// 仅记录异常,不回滚订单事务
System.err.println("短信发送失败:" + e.getMessage());
}
}
}
6. NEVER:必须在无事务环境下执行,否则抛异常
核心逻辑 :被调用方严格禁止在事务中执行。如果调用方有事务,直接抛出 IllegalTransactionStateException 异常;如果调用方无事务,正常执行。
业务场景:完全不允许事务控制的操作,比如"数据归档脚本""第三方接口调用(自身已保证幂等)",避免事务长时间占用资源或导致数据一致性问题。
代码示例:
java
@Service
public class DataArchiveService {
// 必须无事务执行,有事务则抛异常
@Transactional(propagation = Propagation.NEVER)
public void archiveOldOrder(LocalDateTime endTime) {
// 模拟归档3个月前的订单数据(无事务,避免长时间锁表)
List<Order> oldOrders = orderRepository.findByCreateTimeBefore(endTime);
// ... 归档逻辑
}
}
// 错误调用:调用方有事务
@Service
public class OrderService {
@Autowired
private DataArchiveService archiveService;
@Transactional(rollbackFor = Exception.class)
public void doArchive() {
// 调用 archiveOldOrder 时会抛异常,因为当前有事务
archiveService.archiveOldOrder(LocalDateTime.now().minusMonths(3));
}
}
7. NESTED:嵌套事务,依赖调用方事务
核心逻辑:被调用方在调用方事务内创建一个"嵌套子事务",子事务依赖于父事务(调用方事务)。父事务提交时,子事务才会提交;父事务回滚时,子事务必然回滚;但子事务回滚时,父事务可以选择继续执行(不会被子事务回滚影响)。
注意:嵌套事务依赖数据库支持(如 MySQL 的 SAVEPOINT 保存点机制),并非所有数据库都支持;与 REQUIRES_NEW 的区别是:NESTED 是子事务,与父事务同属一个事务上下文;REQUIRES_NEW 是完全独立的事务。
业务场景:父事务中包含可选操作,子事务失败不影响父事务核心逻辑,比如"创建订单时尝试扣减优惠券",优惠券扣减失败(子事务回滚),但订单创建(父事务)可以继续执行。
代码示例:
java
@Service
public class CouponService {
@Autowired
private CouponRepository couponRepository;
// 嵌套事务,依赖调用方事务
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void deductCoupon(Long couponId, Long userId) {
Coupon coupon = couponRepository.findByIdAndUserId(couponId, userId)
.orElseThrow(() -> new RuntimeException("优惠券不存在"));
if (coupon.getIsUsed()) {
throw new RuntimeException("优惠券已使用");
}
coupon.setIsUsed(true);
couponRepository.save(coupon);
}
}
@Service
public class OrderService {
@Autowired
private CouponService couponService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Integer count, Long userId, Long couponId) {
// 1. 创建订单(父事务核心逻辑)
Order order = new Order();
// ... 订单赋值逻辑
orderRepository.save(order);
// 2. 尝试扣减优惠券(子事务,失败不影响订单)
try {
couponService.deductCoupon(couponId, userId);
} catch (Exception e) {
// 子事务回滚,父事务继续执行
System.err.println("优惠券扣减失败:" + e.getMessage());
}
}
}
结果说明:如果优惠券扣减失败(子事务回滚),订单创建操作(父事务)依然会正常提交;如果订单创建失败(父事务回滚),优惠券扣减操作(子事务)也会跟着回滚。
二、常见误区与实战建议
1. 同类方法内部调用,传播行为失效
Spring 事务基于动态代理实现,同类方法内部调用时,不会经过代理,导致 @Transactional 注解失效,传播行为自然不生效。
java
@Service
public class OrderService {
// 错误示例:内部调用,传播行为失效
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
// 内部调用 deductStock,@Transactional 注解失效
this.deductStock();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deductStock() {
// ... 库存扣减逻辑
}
}
解决方案:通过 Spring 上下文获取自身代理对象调用,或拆分方法到不同 Service 类。
2. 传播行为与隔离级别区分开
不少人会混淆"传播行为"和"隔离级别":传播行为控制的是"事务之间的调用关系",隔离级别控制的是"事务内部对数据的可见性"(如脏读、不可重复读),两者互不影响,可独立配置。
3. 优先使用默认传播行为,按需选型
日常开发中,REQUIRED(默认)能覆盖 80% 以上场景;需要独立事务用 REQUIRES_NEW;查询方法用 SUPPORTS;严格依赖/禁止事务用 MANDATORY/NEVER;嵌套事务谨慎使用(依赖数据库支持)。
三、总结
Spring 事务传播行为的本质是"事务边界的控制规则",核心是解决"多方法调用时事务如何协同"的问题。掌握每种行为的适用场景,结合实际业务选择,才能避免事务漏洞(如数据不一致、事务失效、资源浪费)。
建议实际开发中,先明确"方法间事务是否需要协同",再选择对应的传播行为,同时搭配 rollbackFor(指定回滚异常类型)、readOnly(查询优化)等属性,让事务控制更精准、高效。