共计 2377 个字符,预计需要花费 6 分钟才能阅读完成。
在分布式系统开发中,事务一致性是个老生常谈但又极其重要的话题。特别是当 a 函数调用 b 函数完成插表操作后,如果 a 函数后续执行出现异常,如何确保 ab 操作的原子性回滚,是很多开发者都会遇到的痛点问题。今天我们就来详细聊聊这个问题的解决方案。

1. 背景与痛点
在传统单体应用中,我们通常使用本地事务来保证数据一致性。但在分布式场景下,特别是服务拆分后,函数间的调用会导致事务边界变得模糊。最常见的问题就是:
- b 函数执行成功并提交了事务
- 但 a 函数后续处理失败
- 由于 b 函数的事务已经提交,无法自动回滚
- 最终导致数据不一致
这种情况在支付系统、订单系统等对数据一致性要求高的场景中尤其致命。
2. 技术方案对比
解决这类问题,通常有几种方案:
- 本地事务:适合单体应用,但对分布式场景支持有限
- 分布式事务:如 XA 协议、Seata 等,性能开销大,复杂度高
- Spring 嵌套事务 :使用
PROPAGATION_NESTED传播特性,折中方案
对于大部分中小型项目,Spring 的嵌套事务是个不错的选择。它的特点是:
- 主事务和嵌套事务共享物理连接
- 嵌套事务可以独立回滚
- 主事务回滚会连带回滚所有嵌套事务
- 性能优于分布式事务
3. 代码实现详解
下面我们来看具体实现。首先需要在 application.properties 中配置事务管理器:
# 事务超时时间(秒)
spring.transaction.default-timeout=30
# 开启事务日志
logging.level.org.springframework.transaction=DEBUG
然后是核心业务代码:
@Service
@RequiredArgsConstructor
public class OrderService {
private final InventoryService inventoryService;
private final OrderRepository orderRepository;
// 主事务方法
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {// 1. 创建订单(主事务操作)
Order order = convertToOrder(orderDTO);
orderRepository.save(order);
try {// 2. 调用库存服务(嵌套事务)
inventoryService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
} catch (BusinessException e) {
// 记录异常日志但不抛出,避免触发主事务回滚
log.error("库存扣减异常", e);
// 这里可以根据业务需求决定是否继续处理
}
// 3. 其他业务操作
if (someCondition) {
// 模拟业务异常
throw new RuntimeException("订单创建失败");
}
}
}
@Service
@RequiredArgsConstructor
public class InventoryService {
private final InventoryRepository inventoryRepository;
// 嵌套事务方法
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void deductStock(Long productId, Integer quantity) {Inventory inventory = inventoryRepository.findById(productId)
.orElseThrow(() -> new BusinessException("商品不存在"));
if (inventory.getStock() < quantity) {throw new BusinessException("库存不足");
}
inventory.setStock(inventory.getStock() - quantity);
inventoryRepository.save(inventory);
}
}
4. 避坑指南
在实际使用中,有几个常见错误需要特别注意:
- 误用 PROPAGATION_REQUIRES_NEW:
- 每次都会新建独立事务
- 导致数据库连接数暴增
-
可能引发死锁问题
-
未处理检查型异常:
- 默认只回滚 RuntimeException
- 需要显式配置
rollbackFor -
或者手动
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() -
同类方法调用失效:
- Spring 事务基于 AOP 代理
- 直接调用 this.method()会绕过代理
- 解决方案:注入自身 bean 或使用 AopContext
5. 性能考量
嵌套事务虽然比分布式事务轻量,但仍需注意:
- 每个嵌套事务都会创建保存点(savepoint)
- 数据库需要支持保存点功能(大多数主流数据库都支持)
- 建议设置合理的事务超时时间
- 避免在循环中创建嵌套事务
6. 延伸思考
对于更复杂的微服务场景,可以考虑:
- Saga 模式:
- 将长事务拆分为多个本地事务
- 每个步骤提供补偿操作
-
最终一致性而非强一致性
-
事件溯源:
- 记录所有状态变更事件
- 可通过重放事件恢复状态
动手实验
建议读者可以在本地尝试以下实验:
- 使用 H2 内存数据库搭建测试环境
- 实现上述代码示例
- 模拟以下场景:
- 正常流程
- 库存不足异常
- 订单创建后异常
- 观察不同情况下数据状态变化
通过这次实践,相信大家对 Spring 事务处理会有更深入的理解。事务处理看似简单,实则处处是坑,需要我们在实际开发中不断积累经验。
正文完
