分布式事务实战:Saga、TCC与最终一致性方案深度解析
在微服务架构盛行的今天,分布式事务是每个后端工程师绕不开的难题。单体应用中的本地事务(ACID)在拆分为多个服务后,如何保证数据一致性?本文将结合电商下单的真实场景,深入浅出地讲解Saga、TCC和最终一致性三种模式,并给出可运行的代码示例和选型建议。
为什么需要分布式事务?
假设你正在开发一个电商平台,用户下单后需要:
- 创建订单(订单服务)
- 扣减库存(库存服务)
- 扣减余额(账户服务)
这三个操作分别由不同的微服务提供,如果其中一个失败,如何保证整体数据的一致性?传统的本地事务无法跨服务,因此需要分布式事务方案。
分布式事务的三大模式
一、Saga模式:基于补偿的事务编排
Saga模式将一个长事务拆分为多个本地事务,每个本地事务都有对应的补偿操作。如果后续步骤失败,则执行补偿操作回滚。
Saga的两种实现方式
- Choreography(编排):服务间通过事件驱动,每个服务监听事件并执行相应操作或补偿。
- Orchestration(编排):由一个中心协调器(Saga Execution Coordinator)负责指挥各个服务执行操作和补偿。
实战:使用Orchestration实现下单Saga
我们使用Spring Boot + Java来演示。首先定义订单服务、库存服务、账户服务的接口。
// 订单服务
public interface OrderService {
Order createOrder(Long userId, Long productId, int amount);
void compensateOrder(Long orderId);
}
// 库存服务
public interface StockService {
void deductStock(Long productId, int quantity);
void compensateStock(Long productId, int quantity);
}
// 账户服务
public interface AccountService {
void deductBalance(Long userId, BigDecimal amount);
void compensateBalance(Long userId, BigDecimal amount);
}
接下来实现Saga协调器:
@Component
public class OrderSagaCoordinator {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@Autowired
private AccountService accountService;
public void createOrderSaga(OrderRequest request) {
// 步骤1:创建订单
Order order = orderService.createOrder(request.getUserId(), request.getProductId(), request.getAmount());
try {
// 步骤2:扣减库存
stockService.deductStock(request.getProductId(), request.getAmount());
try {
// 步骤3:扣减余额
accountService.deductBalance(request.getUserId(), request.getPrice());
} catch (Exception e) {
// 步骤3失败,补偿库存
stockService.compensateStock(request.getProductId(), request.getAmount());
throw e;
}
} catch (Exception e) {
// 步骤2失败或步骤3补偿后,补偿订单
orderService.compensateOrder(order.getId());
throw e;
}
}
}
注意:Saga模式没有隔离性,可能会有脏读。例如:在扣库存后、扣余额前,其他事务可能看到库存已扣但余额未扣。解决方式通常使用语义锁或业务上的幂等设计。
二、TCC模式:Try-Confirm-Cancel
TCC将每个服务操作拆分为两个阶段:Try阶段检查并预留资源,Confirm阶段执行真正提交,Cancel阶段释放预留资源。
TCC的三种角色
- 主业务服务:发起整个分布式事务。
- 从业务服务:提供Try/Confirm/Cancel接口。
- 事务管理器:协调各从业务服务的执行。
实战:使用TCC-Transaction框架
这里使用开源框架tcc-transaction,但为了演示原理,我们手写一个简单的TCC实现。
// 库存服务TCC接口
public interface StockServiceTcc {
void tryDeductStock(Long productId, int quantity);
void confirmDeductStock(Long productId, int quantity);
void cancelDeductStock(Long productId, int quantity);
}
// 实现类
@Service
public class StockServiceTccImpl implements StockServiceTcc {
@Override
public void tryDeductStock(Long productId, int quantity) {
// 检查库存是否充足,并扣减可用库存(冻结库存)
// update stock set frozen_quantity = frozen_quantity + ? where product_id = ? and available_quantity >= ?
// 如果影响行数为0,则抛出异常
}
@Override
public void confirmDeductStock(Long productId, int quantity) {
// 真正扣减库存:将冻结库存转为已扣减
// update stock set frozen_quantity = frozen_quantity - ?, available_quantity = available_quantity - ? where product_id = ?
}
@Override
public void cancelDeductStock(Long productId, int quantity) {
// 释放冻结库存
// update stock set frozen_quantity = frozen_quantity - ? where product_id = ?
}
}
事务管理器协调代码:
public class TccTransactionManager {
public void executeTcc(TransactionContext context) {
// 1. 执行所有Try
for (TccAction action : context.getActions()) {
action.tryMethod();
}
// 2. 根据Try结果决定Confirm或Cancel
boolean success = true;
try {
// 模拟业务逻辑,如果全部成功则确认,否则取消
if (success) {
for (TccAction action : context.getActions()) {
action.confirmMethod();
}
} else {
for (TccAction action : context.getActions()) {
action.cancelMethod();
}
}
} catch (Exception e) {
// 如果Confirm或Cancel失败,需要重试或人工介入
}
}
}
注意:TCC要求每个服务实现三个接口,开发成本较高。且Confirm和Cancel需要保证幂等性,因为可能被重复调用。
三、最终一致性:基于消息队列
最终一致性方案通常使用消息队列(如RocketMQ、Kafka)实现。核心思想是将本地事务与消息发送绑定,通过消息异步通知其他服务,保证最终一致。
经典实现:本地消息表 + 消息队列
- 在业务数据库中创建消息表。
- 在本地事务中,同时写入业务数据和消息记录。
- 通过定时任务或消息队列发送消息。
实战:使用RocketMQ事务消息
RocketMQ原生支持事务消息,我们使用它来演示。
// 订单服务中发送事务消息
public class OrderServiceWithMq {
@Autowired
private TransactionMQProducer producer;
public void createOrder(OrderRequest request) {
// 1. 创建订单(本地事务)
orderDao.create(request);
// 2. 发送事务消息
Message msg = new Message("ORDER_TOPIC", "TAG", request.getOrderId().getBytes());
TransactionSendResult result = producer.sendMessageInTransaction(msg, request);
// 根据result状态决定后续操作
}
// 本地事务执行器
@Transactional
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
OrderRequest request = (OrderRequest) arg;
try {
// 执行本地事务(创建订单)
orderDao.create(request);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 消息回查
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 根据订单号查询订单是否存在,决定commit或rollback
return LocalTransactionState.COMMIT_MESSAGE;
}
}
库存服务监听消息并扣减库存:
@Component
public class StockConsumer {
@KafkaListener(topics = "ORDER_TOPIC")
public void onMessage(String orderId) {
// 幂等处理:检查是否已消费
if (!isProcessed(orderId)) {
// 扣减库存
stockDao.deduct(orderId);
// 标记已处理
markProcessed(orderId);
}
}
}
注意:最终一致性方案无法保证实时一致性,只适用于对实时性要求不高的场景。且消费方需要做幂等处理,防止重复消费。
三大模式对比与选型
| 模式 | 一致性 | 隔离性 | 开发成本 | 适用场景 | |——|——–|——–|———-|———-| | Saga | 最终一致 | 无 | 中 | 长事务、业务流程复杂 | | TCC | 强一致 | 有 | 高 | 对一致性要求高、短事务 | | 最终一致性 | 最终一致 | 无 | 低 | 异步场景、高并发 |
选型建议:
- 如果业务对一致性要求高(如支付),且事务较短,优先选TCC。
- 如果业务流程长,且每一步可以异步,选Saga。
- 如果允许数据延迟一致,选最终一致性方案,成本最低。
踩坑经验
- 空回滚与悬挂:在Saga中,补偿操作可能在原操作未执行时就触发(例如网络超时后重试),需要设计空补偿逻辑。
- 幂等性:所有补偿和确认操作都必须是幂等的,因为可能被多次调用。
- 事务消息的可靠性:使用消息队列时,要确保生产者本地事务和消息发送的一致性,RocketMQ事务消息解决了这个问题。
总结
分布式事务没有银弹,选择哪种方案取决于业务场景和一致性要求。Saga适合长流程,TCC适合强一致短事务,最终一致性适合高并发异步场景。实践中,应结合幂等设计、异常处理机制,并做好监控和告警。
延伸阅读:
- 《分布式系统设计》
- 阿里巴巴Seata框架
- 微服务架构下的数据一致性实践