分布式事务的最终一致性方案旨在确保在分布式系统中,所有参与方最终将达到一致的状态,尽管在过程中可能存在短暂的不一致。这种模型在保证系统可用性和分区容错性的同时,牺牲了强一致性的实时性要求。以下是几种主流的最终一致性方案的汇总与详解。
主流最终一致性方案对比
| 方案 | 一致性模型 | 性能 | 实现复杂度 | 典型适用场景 | 核心思想 |
|---|---|---|---|---|---|
| TCC | 最终一致 | 高 | 高 | 金融、电商核心业务(如扣减库存) | 业务侵入补偿型事务,分为Try、Confirm、Cancel三个阶段。 |
| Saga | 最终一致 | 高 | 高 | 长流程、跨服务业务编排 | 将长事务拆分为一系列本地事务,通过正向执行和反向补偿来保证最终一致。 |
| 本地消息表 | 最终一致 | 中 | 中 | 异步通知、对账场景 | 利用数据库本地事务,将消息生产和业务操作绑定,通过异步轮询重试保证下游消费。 |
| 可靠消息(基于MQ) | 最终一致 | 高 | 低 | 高并发、最终一致场景(如状态同步) | 借助消息队列的持久化、重试和确认机制,确保消息至少被成功消费一次。 |
方案详解与示例
1. TCC (Try-Confirm-Cancel)
TCC是一种业务侵入性较强的补偿型事务,要求每个服务提供三个接口:Try(预留资源)、Confirm(确认提交)、Cancel(取消释放)。
核心原理:在Try阶段锁定或预留必要资源,所有参与者Try成功后,在Confirm阶段提交;若任一Try失败,则在Cancel阶段执行补偿操作,释放预留资源。
适用场景:适用于对一致性要求较高且能容忍短暂资源锁定的核心业务,如电商下单时的库存扣减和优惠券核销。
代码示例(库存服务伪代码):
// 库存服务接口定义 public interface InventoryService { // Try: 预扣库存 boolean tryDeduct(Long productId, Integer quantity); // Confirm: 确认扣减 boolean confirmDeduct(Long productId, Integer quantity); // Cancel: 取消预扣,回退库存 boolean cancelDeduct(Long productId, Integer quantity); } // 实现示例 @Service public class InventoryServiceImpl implements InventoryService { @Override @Transactional public boolean tryDeduct(Long productId, Integer quantity) { // 检查并预扣库存(如将可用库存转入冻结库存) int rows = inventoryMapper.freezeStock(productId, quantity); return rows > 0; } @Override @Transactional public boolean confirmDeduct(Long productId, Integer quantity) { // 确认扣减,清除冻结记录 return inventoryMapper.confirmFreeze(productId, quantity) > 0; } @Override @Transactional public boolean cancelDeduct(Long productId, Integer quantity) { // 取消预扣,将冻结库存加回可用库存 return inventoryMapper.unfreezeStock(productId, quantity) > 0; } }2. Saga
Saga模式将一个分布式事务拆分为一系列连续的本地事务。每个本地事务都有对应的补偿事务。执行流程中,如果某个本地事务失败,则会按相反顺序触发前面所有已成功事务的补偿操作。
适用场景:适用于业务流程长、步骤多、跨多个服务的场景,如机票酒店预订流程。
实现模式:
- 编排式:由一个中心协调器来触发并监听各个服务。
- 协同式:每个服务完成后触发下一个服务,并知晓自己的补偿操作。
3. 本地消息表
该方案利用数据库的本地事务原子性,将消息生产和业务操作放在同一个事务中。
核心流程:
- 业务服务在完成本地数据库操作的同时,向同一数据库的“消息表”插入一条待发送的消息记录。
- 有一个独立的“消息发送者”定时轮询消息表,将未发送的消息投递到消息队列。
- 下游消费者从MQ消费消息,处理业务,处理成功后调用回调接口或发送确认消息。
- 消息发送者收到确认后,将消息状态更新为“已发送”或删除。
代码示例(订单创建与消息存储):
-- 在同一事务中执行订单创建和消息记录插入 START TRANSACTION; -- 1. 创建订单 INSERT INTO `order` (order_id, user_id, amount, status) VALUES ('O001', 1001, 299.00, 'CREATED'); -- 2. 在本地消息表中记录待发送的“扣减库存”事件 INSERT INTO `outbox_message` (message_id, `topic`, payload, status, created_time) VALUES (UUID(), 'inventory.deduct', '{"orderId":"O001","productId":"P1001","quantity":1}', 'PENDING', NOW()); COMMIT;4. 可靠消息(基于消息队列)
此方案高度依赖消息队列(如RocketMQ、Kafka)的事务消息或类似机制来保证可靠性。
核心流程(以RocketMQ事务消息为例):
- 发送半消息:生产者向MQ发送一个“半消息”(对消费者不可见)。
- 执行本地事务:生产者执行本地业务逻辑(如创建订单)。
- 提交或回滚:根据本地事务执行结果,向MQ发送Commit或Rollback指令。
- Commit:半消息变为正式消息,可供消费者消费。
- Rollback:消息被丢弃。
- 消费与重试:消费者消费消息并处理业务。如果消费失败,MQ会按重试策略重新投递,直到成功或进入死信队列。
配置示例(RocketMQ生产者):
// 创建事务监听器,用于执行本地事务和检查本地事务状态 TransactionListener transactionListener = new TransactionListenerImpl(); TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("localhost:9876"); producer.setTransactionListener(transactionListener); producer.start(); // 发送事务消息 Message msg = new Message("OrderTopic", "TagA", "OrderID001", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.sendMessageInTransaction(msg, null); // 第二个参数可用于传递业务参数方案选型建议
- 高并发与业务复杂:优先考虑TCC,因其性能好且业务控制粒度细。
- 简单业务,允许延迟:使用本地消息表,实现相对简单,对中间件依赖小。
- 长流程、跨系统编排:Saga模式更为合适,适合流程化的业务场景。
- 已深度依赖消息队列:直接采用可靠消息方案,充分利用MQ的成熟能力实现最终一致。
在实际架构中,常组合使用多种方案。例如,一个电商下单系统可能:创建订单使用本地事务;扣减库存采用TCC或预扣库存;优惠券核销采用预冻结+最终扣减;而支付成功后的状态同步则通过消息队列保证最终一致性。系统必须设计完善的补偿机制,如订单超时未支付时触发库存回退和优惠券释放,并配合重试、告警等异常处理策略。
参考来源
- 分布式事务一致性方案介绍_分布式事务处理多喝保证库存、订单、优惠券核销原子性-CSDN博客
- 分布式事务里的最终一致性
- 分布式事务处理与一致性保障方案详解