Spring 事务传播机制
引言:事务传播的 "隐形之手"
在分布式系统和复杂业务场景中,事务管理是保证数据一致性的核心环节。想象这样一个场景:用户下单时,系统需要扣减库存、创建订单、扣减余额,这三个操作必须同时成功或同时失败。但如果这三个操作分别位于不同的服务或方法中,如何确保它们处于同一个事务中?
这就是 Spring 事务传播机制要解决的问题。它像一只 "隐形之手",在方法调用之间协调事务的创建、加入和结束,确保数据操作的一致性。理解 Spring 事务传播机制,不仅能帮助你避免常见的事务问题,还能让你在复杂业务场景中设计出更健壮的系统。
本文将从基础概念到实战案例,全面解析 Spring 的 7 种事务传播行为,带你彻底掌握这一核心技术。
一、事务与事务传播的基础概念
1.1 什么是事务?
事务(Transaction)是数据库操作的基本单元,它是一系列操作的集合,这些操作要么全部成功,要么全部失败,不存在部分成功的情况。
事务具有 ACID 特性:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成
- 一致性(Consistency):事务执行前后,数据库的状态保持一致
- 隔离性(Isolation):多个事务并发执行时,彼此不会相互干扰
- 持久性(Durability):事务一旦提交,其结果就是永久性的
1.2 什么是事务传播机制?
当一个事务方法调用另一个事务方法时,Spring 需要决定如何处理这两个方法的事务关系:是使用同一个事务,还是创建新的事务?事务传播机制(Transaction Propagation)就是定义这种处理规则的机制。
事务传播机制解决的核心问题是:当多个事务方法嵌套调用时,如何管理事务的边界和行为。
1.3 Spring 中事务传播机制的定义
Spring 在Propagation
枚举类中定义了 7 种事务传播行为:
public enum Propagation {REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),NEVER(TransactionDefinition.PROPAGATION_NEVER),NESTED(TransactionDefinition.PROPAGATION_NESTED);private final int value;Propagation(int value) {this.value = value;}public int value() {return this.value;}
}
这些传播行为定义了内部方法如何与外部方法的事务进行交互,是 Spring 事务管理的核心。
二、Spring 事务传播机制的实现原理
要理解事务传播机制,首先需要了解 Spring 是如何管理事务的。
2.1 Spring 事务管理的核心组件
- PlatformTransactionManager:事务管理的核心接口,定义了事务的创建、提交和回滚方法
- TransactionDefinition:定义事务的属性,包括传播行为、隔离级别、超时时间等
- TransactionStatus:代表当前事务的状态,提供了事务的状态信息和操作方法
2.2 事务传播的工作流程
当一个标注了@Transactional
的方法被调用时,Spring 会通过 AOP 拦截该方法,执行以下步骤:
事务传播机制的核心逻辑就在步骤 D 中,Spring 会根据当前是否存在事务以及指定的传播行为,决定如何处理事务。
2.3 事务的绑定与传播
Spring 通过ThreadLocal
将事务与当前线程绑定,实现事务的传播:
// 简化版的事务绑定逻辑
public class TransactionSynchronizationManager {// 存储当前线程的事务连接private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");// 存储当前线程的事务状态private static final ThreadLocal<TransactionStatus> transactionStatus =new NamedThreadLocal<>("Current transaction status");// 绑定资源到当前线程public static void bindResource(Object key, Object value) {Map<Object, Object> map = resources.get();if (map == null) {map = new HashMap<>();resources.set(map);}map.put(key, value);}// 从当前线程获取资源public static Object getResource(Object key) {Map<Object, Object> map = resources.get();return (map != null ? map.get(key) : null);}// 从当前线程解绑资源public static Object unbindResource(Object key) {Map<Object, Object> map = resources.get();if (map == null) {return null;}return map.remove(key);}
}
当方法调用时,Spring 会检查当前线程是否已绑定事务资源:
- 如果有,说明当前已有事务在运行
- 如果没有,说明需要创建新事务
这种线程绑定机制是事务传播的基础,使得嵌套调用的方法能够感知到外部方法的事务。
三、7 种事务传播行为详解
Spring 定义了 7 种事务传播行为,每种行为都有其特定的使用场景和行为规则。
3.1 REQUIRED:默认的传播行为
行为定义:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
适用场景:大多数业务场景,这是 Spring 的默认传播行为。
示例代码:
/*** 订单服务* @author ken*/
@Service
@Slf4j
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate InventoryService inventoryService;/*** 创建订单(无事务)*/public void createOrderWithoutTx(Order order) {log.info("创建订单: {}", order);orderMapper.insert(order);// 调用扣减库存方法(REQUIRED)inventoryService.reduceStock(order.getProductId(), order.getQuantity());}/*** 创建订单(有事务)*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderWithTx(Order order) {log.info("创建订单: {}", order);orderMapper.insert(order);// 调用扣减库存方法(REQUIRED)inventoryService.reduceStock(order.getProductId(), order.getQuantity());}
}/*** 库存服务* @author ken*/
@Service
@Slf4j
public class InventoryService {@Autowiredprivate InventoryMapper inventoryMapper;/*** 扣减库存*/@Transactional(propagation = Propagation.REQUIRED)public void reduceStock(Long productId, Integer quantity) {log.info("扣减库存, 商品ID: {}, 数量: {}", productId, quantity);Inventory inventory = inventoryMapper.selectById(productId);if (inventory.getStock() < quantity) {throw new InsufficientStockException("库存不足");}inventory.setStock(inventory.getStock() - quantity);inventoryMapper.updateById(inventory);}
}
执行结果分析:
调用
createOrderWithoutTx()
:- 外部方法没有事务
- 内部方法
reduceStock()
会创建新事务 - 如果内部方法抛出异常,只有库存操作会回滚,订单会保留(数据不一致)
调用
createOrderWithTx()
:- 外部方法创建新事务
- 内部方法
reduceStock()
加入外部事务 - 如果内部方法抛出异常,整个事务会回滚,订单和库存操作都会撤销(数据一致)
3.2 SUPPORTS:支持当前事务
行为定义:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式运行。
适用场景:适用于那些 "可选事务" 的方法,这些方法在有事务时就参与事务,没有事务时也能正常运行。例如查询操作,通常不需要事务,但如果已经在事务中执行,也可以参与事务。
示例代码:
/*** 统计服务* @author ken*/
@Service
@Slf4j
public class StatisticsService {@Autowiredprivate StatisticsMapper statisticsMapper;/*** 更新统计数据* 支持事务,但不强制要求*/@Transactional(propagation = Propagation.SUPPORTS)public void updateStatistics(Long productId) {log.info("更新商品统计数据, 商品ID: {}", productId);Statistics stats = statisticsMapper.selectById(productId);if (stats == null) {stats = new Statistics();stats.setProductId(productId);stats.setSaleCount(1);statisticsMapper.insert(stats);} else {stats.setSaleCount(stats.getSaleCount() + 1);statisticsMapper.updateById(stats);}}
}// 在OrderService中添加调用
@Service
@Slf4j
public class OrderService {// ... 其他代码省略 .../*** 创建订单(有事务)*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderWithTx(Order order) {log.info("创建订单: {}", order);orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());// 调用统计服务(SUPPORTS)statisticsService.updateStatistics(order.getProductId());}/*** 查询订单(无事务)*/public Order queryOrder(Long orderId) {Order order = orderMapper.selectById(orderId);// 调用统计服务(SUPPORTS)statisticsService.updateStatistics(order.getProductId());return order;}
}
执行结果分析:
在
createOrderWithTx()
中调用updateStatistics()
:- 外部存在事务,统计方法加入该事务
- 如果订单创建失败回滚,统计数据也会回滚
在
queryOrder()
中调用updateStatistics()
:- 外部没有事务,统计方法以非事务方式运行
- 统计数据会直接提交,不受其他操作影响
3.3 MANDATORY:必须在事务中运行
行为定义:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
适用场景:适用于那些必须在事务中执行的操作,确保这些操作不会在无事务的环境中执行。例如关键的金融操作,必须在事务保护下执行。
示例代码:
/*** 支付服务* @author ken*/
@Service
@Slf4j
public class PaymentService {@Autowiredprivate PaymentMapper paymentMapper;/*** 记录支付信息* 必须在事务中运行*/@Transactional(propagation = Propagation.MANDATORY)public void recordPayment(Payment payment) {log.info("记录支付信息: {}", payment);paymentMapper.insert(payment);}
}// 在OrderService中添加调用
@Service
@Slf4j
public class OrderService {// ... 其他代码省略 .../*** 创建订单并支付(有事务)*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderAndPay(Order order, Payment payment) {log.info("创建订单并支付");orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());// 调用支付服务(MANDATORY)paymentService.recordPayment(payment);}/*** 创建订单并支付(无事务)*/public void createOrderAndPayWithoutTx(Order order, Payment payment) {log.info("创建订单并支付(无事务)");orderMapper.insert(order);// 调用支付服务(MANDATORY)paymentService.recordPayment(payment);}
}
执行结果分析:
调用
createOrderAndPay()
:- 外部存在事务,支付方法加入该事务
- 所有操作在同一个事务中,保证数据一致性
调用
createOrderAndPayWithoutTx()
:- 外部没有事务,支付方法会抛出
IllegalTransactionStateException
- 订单会被插入,但支付记录不会被插入,且会抛出异常
- 外部没有事务,支付方法会抛出
3.4 REQUIRES_NEW:创建新事务
行为定义:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。
适用场景:适用于那些需要独立事务的操作,这些操作无论外部事务成功与否都应该被提交。例如日志记录、操作审计等,即使主业务失败,这些记录也需要保留。
示例代码:
/*** 日志服务* @author ken*/
@Service
@Slf4j
public class LogService {@Autowiredprivate OperationLogMapper logMapper;/*** 记录操作日志* 使用独立事务*/@Transactional(propagation = Propagation.REQUIRES_NEW)public void recordLog(OperationLog log) {log.info("记录操作日志: {}", log);logMapper.insert(log);}
}// 在OrderService中添加调用
@Service
@Slf4j
public class OrderService {// ... 其他代码省略 .../*** 创建订单(有事务)*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderWithLog(Order order) {log.info("创建订单: {}", order);// 记录操作日志(REQUIRES_NEW)OperationLog log = new OperationLog();log.setOperation("CREATE_ORDER");log.setContent(JSON.toJSONString(order));logService.recordLog(log);orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());// 模拟业务异常if (order.getAmount() < 0) {throw new IllegalArgumentException("订单金额不能为负数");}}
}
执行结果分析:
当订单金额正常时:
- 外部事务正常提交
- 日志服务创建新事务并提交
- 订单、库存和日志都成功保存
当订单金额为负数(抛出异常)时:
- 外部事务回滚,订单和库存操作被撤销
- 日志服务的新事务不受影响,仍然成功提交
- 最终结果:日志被保存,订单和库存操作被回滚
3.5 NOT_SUPPORTED:不支持事务
行为定义:以非事务方式运行。如果当前存在事务,则将当前事务挂起。
适用场景:适用于那些不需要事务的操作,即使外部存在事务,也以非事务方式运行。例如一些耗时的查询操作,不需要事务支持,可以提高性能。
示例代码:
/*** 报表服务* @author ken*/
@Service
@Slf4j
public class ReportService {@Autowiredprivate ReportMapper reportMapper;/*** 生成销售报表* 不需要事务支持*/@Transactional(propagation = Propagation.NOT_SUPPORTED)public Report generateSalesReport(LocalDate startDate, LocalDate endDate) {log.info("生成销售报表, 开始日期: {}, 结束日期: {}", startDate, endDate);// 模拟耗时的报表生成过程try {Thread.sleep(2000);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("报表生成被中断");}Report report = new Report();report.setStartDate(startDate);report.setEndDate(endDate);report.setContent("销售报表内容...");reportMapper.insert(report);return report;}
}// 在OrderService中添加调用
@Service
@Slf4j
public class OrderService {// ... 其他代码省略 .../*** 创建订单并生成报表*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderAndGenerateReport(Order order) {log.info("创建订单并生成报表");orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());// 调用报表服务(NOT_SUPPORTED)reportService.generateSalesReport(LocalDate.now().minusDays(7), LocalDate.now());// 模拟异常if (order.getQuantity() > 100) {throw new IllegalArgumentException("订单数量过大");}}
}
执行结果分析:
当订单数量正常时:
- 外部事务正常提交,订单和库存操作生效
- 报表服务以非事务方式运行,报表记录被保存
当订单数量过大(抛出异常)时:
- 外部事务回滚,订单和库存操作被撤销
- 报表服务以非事务方式运行,不受外部事务影响,报表记录仍然被保存
3.6 NEVER:不允许事务
行为定义:以非事务方式运行。如果当前存在事务,则抛出异常。
适用场景:适用于那些绝对不能在事务中运行的操作。例如某些特定的统计或日志操作,如果在事务中运行可能会导致性能问题或数据不一致。
示例代码:
/*** 审计服务* @author ken*/
@Service
@Slf4j
public class AuditService {@Autowiredprivate AuditLogMapper auditLogMapper;/*** 记录审计日志* 不允许在事务中运行*/@Transactional(propagation = Propagation.NEVER)public void recordAuditLog(AuditLog log) {log.info("记录审计日志: {}", log);auditLogMapper.insert(log);}
}// 在OrderService中添加调用
@Service
@Slf4j
public class OrderService {// ... 其他代码省略 .../*** 创建订单并记录审计日志(有事务)*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderWithAudit(Order order) {log.info("创建订单并记录审计日志");orderMapper.insert(order);// 调用审计服务(NEVER)AuditLog log = new AuditLog();log.setOperation("CREATE_ORDER");log.setOperator("system");auditService.recordAuditLog(log);}/*** 创建订单并记录审计日志(无事务)*/public void createOrderWithAuditWithoutTx(Order order) {log.info("创建订单并记录审计日志(无事务)");orderMapper.insert(order);// 调用审计服务(NEVER)AuditLog log = new AuditLog();log.setOperation("CREATE_ORDER");log.setOperator("system");auditService.recordAuditLog(log);}
}
执行结果分析:
调用
createOrderWithAudit()
:- 外部存在事务
- 调用审计服务时会抛出
IllegalTransactionStateException
- 外部事务回滚,订单操作被撤销
调用
createOrderWithAuditWithoutTx()
:- 外部没有事务
- 审计服务正常执行,审计日志被记录
- 订单操作以非事务方式执行,直接提交
3.7 NESTED:嵌套事务
行为定义:如果当前存在事务,则创建一个嵌套事务(子事务),嵌套在当前事务中;如果当前没有事务,则创建一个新的事务。
嵌套事务与外部事务的关系:
- 子事务依赖于外部事务,只有外部事务提交后,子事务才能被提交
- 子事务可以独立回滚,回滚后不影响外部事务的继续执行
- 如果外部事务回滚,子事务也会被回滚
适用场景:适用于那些需要部分回滚能力的场景。例如复杂的订单处理,其中某些步骤失败可以回滚该步骤,但不影响其他步骤的继续执行。
示例代码:
/*** 积分服务* @author ken*/
@Service
@Slf4j
public class PointsService {@Autowiredprivate PointsMapper pointsMapper;/*** 增加用户积分* 使用嵌套事务*/@Transactional(propagation = Propagation.NESTED)public void addPoints(Long userId, Integer points) {log.info("增加用户积分, 用户ID: {}, 积分: {}", userId, points);PointsAccount account = pointsMapper.selectById(userId);if (account == null) {account = new PointsAccount();account.setUserId(userId);account.setPoints(points);pointsMapper.insert(account);} else {account.setPoints(account.getPoints() + points);pointsMapper.updateById(account);}// 模拟积分操作失败if (points < 0) {throw new IllegalArgumentException("积分不能为负数");}}
}// 在OrderService中添加调用
@Service
@Slf4j
public class OrderService {// ... 其他代码省略 .../*** 创建订单并增加积分*/@Transactional(propagation = Propagation.REQUIRED)public void createOrderAndAddPoints(Order order) {log.info("创建订单并增加积分");orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());try {// 调用积分服务(NESTED)pointsService.addPoints(order.getUserId(), order.getAmount().intValue() / 10);} catch (IllegalArgumentException e) {log.error("积分操作失败, 继续执行订单流程: {}", e.getMessage());// 捕获子事务异常, 外部事务继续执行}// 模拟外部事务是否回滚if (order.getAmount() > 10000) {throw new IllegalArgumentException("订单金额过大, 需要人工审核");}}
}
执行结果分析:
当订单金额正常且积分不为负时:
- 外部事务和嵌套事务都正常提交
- 订单、库存和积分操作都生效
当积分操作失败(积分 < 0)但订单金额正常时:
- 积分服务的嵌套事务回滚,积分操作被撤销
- 外部事务继续执行并提交,订单和库存操作生效
- 最终结果:订单和库存操作成功,积分操作失败
当订单金额过大(抛出异常)时:
- 无论积分操作是否成功,外部事务回滚
- 订单、库存和积分操作都被撤销
NESTED 与 REQUIRES_NEW 的区别:
- NESTED 是嵌套在外部事务中的子事务,依赖于外部事务
- REQUIRES_NEW 是完全独立的新事务,与外部事务没有依赖关系
- 外部事务回滚时,NESTED 子事务会被回滚,而 REQUIRES_NEW 事务不受影响
四、事务传播机制的实战案例
4.1 订单创建全流程事务管理
假设一个完整的订单创建流程包括以下步骤:
- 创建订单记录
- 扣减商品库存
- 扣减用户余额
- 增加用户积分
- 记录操作日志
我们需要设计合理的事务传播机制,确保数据一致性的同时,满足业务需求。
/*** 订单流程服务* 协调订单创建的完整流程* @author ken*/
@Service
@Slf4j
public class OrderProcessService {@Autowiredprivate OrderService orderService;@Autowiredprivate InventoryService inventoryService;@Autowiredprivate AccountService accountService;@Autowiredprivate PointsService pointsService;@Autowiredprivate LogService logService;/*** 完整的订单创建流程*/@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public Order createCompleteOrder(OrderCreateDTO dto) {log.info("开始处理订单创建流程: {}", dto);// 1. 创建订单记录 - 参与主事务Order order = new Order();order.setUserId(dto.getUserId());order.setProductId(dto.getProductId());order.setQuantity(dto.getQuantity());order.setAmount(dto.getAmount());order.setStatus(OrderStatus.PENDING);orderService.createOrder(order);try {// 2. 扣减商品库存 - 参与主事务inventoryService.reduceStock(dto.getProductId(), dto.getQuantity());// 3. 扣减用户余额 - 参与主事务accountService.reduceBalance(dto.getUserId(), dto.getAmount());// 4. 增加用户积分 - 嵌套事务,失败不影响主流程pointsService.addPoints(dto.getUserId(), dto.getAmount().intValue() / 10);} catch (InsufficientStockException | InsufficientBalanceException e) {log.error("订单创建失败: {}", e.getMessage());// 库存或余额不足,回滚整个事务throw e;} catch (Exception e) {log.error("非关键步骤失败: {}", e.getMessage());// 非关键步骤失败,记录日志,继续执行}// 5. 记录操作日志 - 独立事务,必须成功OperationLog log = new OperationLog();log.setOperation("CREATE_ORDER");log.setContent(JSON.toJSONString(order));logService.recordLog(log);// 6. 更新订单状态为成功order.setStatus(OrderStatus.SUCCESS);orderService.updateOrder(order);log.info("订单创建流程完成, 订单ID: {}", order.getId());return order;}
}/*** 账户服务* @author ken*/
@Service
@Slf4j
public class AccountService {@Autowiredprivate AccountMapper accountMapper;/*** 扣减用户余额*/@Transactional(propagation = Propagation.REQUIRED)public void reduceBalance(Long userId, BigDecimal amount) {log.info("扣减用户余额, 用户ID: {}, 金额: {}", userId, amount);Account account = accountMapper.selectById(userId);if (account.getBalance().compareTo(amount) < 0) {throw new InsufficientBalanceException("余额不足");}account.setBalance(account.getBalance().subtract(amount));accountMapper.updateById(account);}
}
各服务的事务传播配置:
OrderProcessService.createCompleteOrder()
:REQUIRED
- 主事务OrderService.createOrder()
:REQUIRED
- 参与主事务InventoryService.reduceStock()
:REQUIRED
- 参与主事务AccountService.reduceBalance()
:REQUIRED
- 参与主事务PointsService.addPoints()
:NESTED
- 嵌套事务,失败可回滚但不影响主流程LogService.recordLog()
:REQUIRES_NEW
- 独立事务,确保日志被记录
这种配置的优势:
- 核心业务(订单、库存、余额)在同一个事务中,确保数据一致性
- 积分操作使用嵌套事务,失败时只回滚积分部分,不影响核心业务
- 日志记录使用独立事务,无论主业务成功与否都能记录
4.2 分布式事务场景下的传播机制
在分布式系统中,多个服务之间的事务协调更为复杂。虽然 Spring 的事务传播机制主要针对单库事务,但可以与分布式事务解决方案结合使用。
/*** 分布式订单服务* 协调多个微服务完成订单创建* @author ken*/
@Service
@Slf4j
public class DistributedOrderService {@Autowiredprivate LocalOrderService localOrderService;@Autowiredprivate RemoteInventoryService remoteInventoryService;@Autowiredprivate RemotePaymentService remotePaymentService;@Autowiredprivate RemoteNotificationService remoteNotificationService;/*** 创建分布式订单* 使用Seata分布式事务协调*/@GlobalTransactional(rollbackFor = Exception.class) // Seata分布式事务注解public OrderDTO createDistributedOrder(OrderCreateDTO dto) {log.info("开始创建分布式订单: {}", dto);// 1. 创建本地订单OrderDTO order = localOrderService.createOrder(dto);try {// 2. 远程调用库存服务扣减库存remoteInventoryService.reduceStock(dto.getProductId(), dto.getQuantity());// 3. 远程调用支付服务处理支付PaymentResult result = remotePaymentService.processPayment(dto.getUserId(), order.getOrderId(), dto.getAmount());if (result.isSuccess()) {// 4. 更新订单状态为已支付localOrderService.updateOrderStatus(order.getOrderId(), OrderStatus.PAID);// 5. 远程调用通知服务发送消息(非关键步骤)try {remoteNotificationService.sendPaymentSuccessNotification(dto.getUserId(), order.getOrderId());} catch (Exception e) {log.error("发送通知失败, 已记录但不影响主流程: {}", e.getMessage());}} else {throw new PaymentFailedException("支付失败: " + result.getErrorMessage());}} catch (Exception e) {log.error("分布式订单处理失败: {}", e.getMessage());// 抛出异常,触发全局回滚throw e;}log.info("分布式订单创建完成: {}", order.getOrderId());return order;}
}/*** 本地订单服务* @author ken*/
@Service
@Slf4j
public class LocalOrderService {@Autowiredprivate OrderMapper orderMapper;/*** 创建本地订单*/@Transactional(propagation = Propagation.REQUIRED)public OrderDTO createOrder(OrderCreateDTO dto) {log.info("创建本地订单: {}", dto);// 订单创建逻辑...}/*** 更新订单状态*/@Transactional(propagation = Propagation.REQUIRED)public void updateOrderStatus(Long orderId, OrderStatus status) {log.info("更新订单状态, 订单ID: {}, 状态: {}", orderId, status);// 状态更新逻辑...}
}/*** 远程库存服务客户端* @author ken*/
@FeignClient(name = "inventory-service")
public interface RemoteInventoryService {/*** 远程扣减库存*/@PostMapping("/api/inventory/reduce")@Transactional(propagation = Propagation.MANDATORY) // 必须在分布式事务中运行void reduceStock(@RequestParam("productId") Long productId, @RequestParam("quantity") Integer quantity);
}
分布式场景下的事务传播策略:
- 全局事务由
@GlobalTransactional
注解标记(以 Seata 为例) - 本地服务方法使用
REQUIRED
传播行为,参与全局事务 - 远程服务方法使用
MANDATORY
传播行为,确保必须在全局事务中运行 - 非关键操作(如通知)可以使用
REQUIRES_NEW
或捕获异常,避免影响主流程
五、事务传播机制的常见问题与解决方案
5.1 自调用导致事务失效
问题描述:当一个事务方法调用同一个类中的另一个事务方法时,事务传播机制失效。
@Service
public class OrderService {@Transactional(propagation = Propagation.REQUIRED)public void createOrder(Order order) {// 业务逻辑...// 调用同一个类中的另一个事务方法this.updateOrderStatus(order.getId(), OrderStatus.PENDING);}@Transactional(propagation = Propagation.REQUIRES_NEW)public void updateOrderStatus(Long orderId, OrderStatus status) {// 更新订单状态...}
}
问题原因:Spring 事务通过 AOP 实现,自调用时不会经过 AOP 代理,因此事务注解失效。
解决方案:
- 将方法拆分到不同的服务类中
- 自注入当前服务的代理对象
@Service
public class OrderService {// 注入自己的代理对象@Autowiredprivate OrderService orderService;@Transactional(propagation = Propagation.REQUIRED)public void createOrder(Order order) {// 业务逻辑...// 通过代理对象调用方法orderService.updateOrderStatus(order.getId(), OrderStatus.PENDING);}@Transactional(propagation = Propagation.REQUIRES_NEW)public void updateOrderStatus(Long orderId, OrderStatus status) {// 更新订单状态...}
}
5.2 异常被捕获导致事务不回滚
问题描述:当事务方法中的异常被捕获后,事务不会回滚。
@Service
public class OrderService {@Transactional(propagation = Propagation.REQUIRED)public void createOrder(Order order) {try {orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());} catch (Exception e) {log.error("创建订单失败", e);// 异常被捕获,没有重新抛出}}
}
问题原因:Spring 事务通过检测未被捕获的异常来触发回滚,如果异常被捕获,事务管理器会认为方法执行成功。
解决方案:捕获异常后重新抛出,或手动触发回滚。
@Service
public class OrderService {@Autowiredprivate TransactionStatus transactionStatus;@Transactional(propagation = Propagation.REQUIRED)public void createOrder(Order order) {try {orderMapper.insert(order);inventoryService.reduceStock(order.getProductId(), order.getQuantity());} catch (Exception e) {log.error("创建订单失败", e);// 方案1: 重新抛出异常throw new BusinessException("创建订单失败", e);// 方案2: 手动回滚// TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}
}
5.3 错误的异常类型导致事务不回滚
问题描述:抛出检查型异常时,事务不会回滚。
@Service
public class OrderService {@Transactional(propagation = Propagation.REQUIRED)public void createOrder(Order order) throws IOException {orderMapper.insert(order);// 抛出检查型异常throw new IOException("文件操作失败");}
}
问题原因:Spring 默认只对未检查异常(继承自RuntimeException
)和Error
进行回滚,对检查型异常不回滚。
解决方案:指定rollbackFor
属性,明确需要回滚的异常类型。
@Service
public class OrderService {// 指定需要回滚的异常类型@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public void createOrder(Order order) throws IOException {orderMapper.insert(order);throw new IOException("文件操作失败");}
}
5.4 事务传播与隔离级别的组合问题
问题描述:不同事务传播行为与隔离级别组合时可能产生意想不到的结果。
解决方案:了解不同传播行为对隔离级别的影响:
REQUIRED
和MANDATORY
:使用外部事务的隔离级别REQUIRES_NEW
和NESTED
:使用自己定义的隔离级别SUPPORTS
:如果加入外部事务,使用外部事务的隔离级别NOT_SUPPORTED
和NEVER
:不使用事务,隔离级别无效
建议:核心事务使用明确的隔离级别,非核心事务可以使用默认设置。
六、事务传播机制的最佳实践
6.1 传播行为的选择原则
优先使用默认的 REQUIRED:大多数业务场景都适合使用
REQUIRED
,它能保证相关操作在同一个事务中。独立操作使用 REQUIRES_NEW:日志记录、审计跟踪等需要独立存在的操作,应使用
REQUIRES_NEW
。关键操作使用 MANDATORY:对于必须在事务中执行的关键操作,使用
MANDATORY
确保不会在无事务环境中执行。可选操作使用 SUPPORTS:对于查询类操作或非核心业务,使用
SUPPORTS
可以提高灵活性。部分回滚需求使用 NESTED:当需要部分回滚能力时,使用
NESTED
,但要注意数据库是否支持保存点。避免使用 NOT_SUPPORTED 和 NEVER:这两种传播行为会脱离事务管理,除非有特殊理由,否则应谨慎使用。
6.2 事务边界设计原则
事务边界应尽可能小:只将必要的操作包含在事务中,减少锁的持有时间,提高并发性能。
避免长事务:长时间运行的事务会导致数据库连接耗尽和锁竞争加剧,应拆分为多个短事务。
事务方法应专注于业务逻辑:避免在事务方法中执行耗时操作(如远程调用、IO 操作),可以将这些操作移到事务外。
明确事务的回滚策略:总是指定
rollbackFor
属性,明确哪些异常需要触发回滚。
6.3 性能优化建议
- 合理设置事务超时时间:根据业务复杂度设置合适的超时时间,避免事务长时间占用资源。
@Transactional(propagation = Propagation.REQUIRED, timeout = 30) // 超时时间30秒
public void processOrder(Order order) {// 业务逻辑...
}
- 查询操作使用只读事务:对于纯查询操作,设置
readOnly = true
可以提高性能,特别是对于 Hibernate 等 ORM 框架。
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public List<Order> queryOrders(Long userId) {// 查询逻辑...
}
避免在事务中进行批量操作:大批量操作会导致事务日志过大和锁竞争,应拆分或使用专门的批量处理机制。
合理使用缓存:将频繁访问的数据放入缓存,减少事务中的数据库操作。
七、总结:掌握事务传播,构建可靠系统
Spring 事务传播机制是保证数据一致性的关键技术,它通过定义不同的传播行为,解决了嵌套方法调用中的事务管理问题。