Saga 模式
Saga模式适用于长事务场景,通过将大事务拆分为多个本地小事务,并定义补偿操作来回滚,实现最终一致性。与TCC不同,Saga没有预留资源阶段,而是直接提交每个本地事务,因此隔离性较弱(可能脏读),但业务侵入性较低。
核心思想
-
将一个大事务拆分为多个连续的本地事务(Saga子事务)。
-
每个子事务都有对应的补偿事务,用于失败时回滚。
-
执行方式:顺序执行所有子事务,若某个子事务失败,则按相反顺序执行补偿事务。
两种协调模式
-
命令式:无中心协调器,每个服务产生事件并监听其他事件。
-
编排式:有中心协调器负责集中决策。
核心原理
1. 事务拆分与补偿链
- 正向事务:将分布式事务拆分为连续的子事务(T₁ → T₂ → T₃)
- 补偿事务:为每个子事务定义逆向操作(C₁ ← C₂ ← C₃)
电商下单
电商下单:
T₁: 创建订单 → C₁: 取消订单
T₂: 扣减库存 → C₂: 恢复库存
T₃: 扣款 → C₃: 退款
T₄: 发送订单成功通知 → C₄: 发送失败通知(可选)
2. 执行规则
- 成功场景:顺序执行 T₁ → T₂ → T₃ → T₄
- 失败场景:
- 若 T₃ 失败,则执行 C₃ → C₂ → C₁(反向补偿)
- 补偿必须幂等且可重试
协调模式对比
1. 命令式
- 特点:
- 无中心协调器
- 服务间通过事件驱动(如 Kafka/RabbitMQ)
- 适用场景:简单流程(3-5 个服务)
2. 编排式
- 特点:
- 中心协调器控制流程
- 状态持久化,易于监控
- 适用场景:复杂流程(>5 个服务)
关键问题
问题 | 方案 |
---|---|
隔离性缺失 | 版本控制(如库存版本号)、业务规则约束(如订单时效性) |
补偿不确定性 | 设计等幂补偿操作、记录操作日志 |
流程中断 | 超时重试机制 + 人工干预后台 |
调试困难 | 分布式链路追踪 + Saga 日志快照 |
代码(编排式 - Spring Boot)
// 协调器服务
@Service
public class OrderSagaOrchestrator {
@Autowired
private OrderService orderService;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(OrderRequest request) {
// 1. 创建订单
String orderId = orderService.createOrder(request);
try {
// 2. 扣减库存
inventoryService.deductStock(orderId, request.getItems());
// 3. 扣款
paymentService.charge(orderId, request.getAmount());
} catch (Exception ex) {
// 补偿流程(反向执行)
paymentService.refund(orderId); // 可能为空操作(若未扣款)
inventoryService.restoreStock(orderId);
orderService.cancelOrder(orderId);
throw ex;
}
// 4. 发送通知(可异步)
notificationService.sendOrderSuccess(orderId);
}
}
// 库存服务(幂等补偿)
@Service
public class InventoryService {
@Transactional
public void deductStock(String orderId, List<Item> items) {
// 幂等检查:若已扣减则跳过
if (deductLogDao.exists(orderId)) return;
items.forEach(item -> {
stockDao.reduce(item.sku(), item.quantity());
deductLogDao.save(orderId, item.sku(), item.quantity()); // 记录日志
});
}
public void restoreStock(String orderId) {
// 根据日志恢复库存
List<DeductLog> logs = deductLogDao.findByOrderId(orderId);
logs.forEach(log -> {
stockDao.increase(log.getSku(), log.getQuantity());
deductLogDao.delete(log); // 防止重复补偿
});
}
}
适用场景
✅ 优势
- 无阻塞:本地事务提交即释放资源
- 低侵入:无需改造现有业务接口
- 适应长事务:支持小时/天级事务(如物流系统)
⚠️ 劣势
- 弱隔离性:可能脏读(如看到中间状态订单)
- 补偿复杂性:需设计全链路回滚
- 调试困难:分布式链路跟踪成本高
🚀 适用场景
- 电商订单履约(创建订单 → 扣库存 → 支付 → 发货)
- 旅行预订(订机票 → 订酒店 → 租车)
- 微服务架构下的跨系统业务流程
- 对强一致性要求不高的场景
实践
-
模式选择:
- 简单流程 → 事件编排式(Kafka + 服务监听)
- 复杂流程 → 中心协调式(Camunda/Zeebe + 状态机)
-
状态机实现:
// 使用状态机定义Saga流程
StateMachine<OrderState, OrderEvent> stateMachine = config.buildStateMachine();
stateMachine.sendEvent(OrderEvent.CREATE_ORDER);
stateMachine.sendEvent(OrderEvent.DEDUCT_STOCK);
if (stateMachine.hasState(OrderState.FAILED)) {
stateMachine.sendEvent(OrderEvent.COMPENSATE); // 触发补偿
}
-
可视化监控:
- 持久化 Saga 日志到数据库:
CREATE TABLE saga_log ( saga_id VARCHAR(36) PRIMARY KEY, current_step VARCHAR(50), status ENUM('PENDING','SUCCESS','FAILED','COMPENSATING'), created_time TIMESTAMP );
- 结合 Grafana 展示事务状态大盘
- 持久化 Saga 日志到数据库:
-
容错设计:
- 补偿操作必须实现 等幂性
- 部署后台任务修复中断的 Saga:
@Scheduled(fixedDelay = 300000) // 每5分钟扫描 public void repairSagas() { List<Saga> stuckSagas = sagaDao.findByStatusAndTimeout("PENDING", now()-1h); stuckSagas.forEach(saga -> sagaService.retryOrCompensate(saga)); }
Saga 是处理长周期分布式事务的经典模式,适用于业务链路过长且无法同步阻塞的场景。关键成功要素在于补偿操作的完备性设计和全链路可观测性。在金融等高一致性领域,建议结合业务规则增强隔离性(如预冻结资金)。
本地消息表
本地消息表是一种基于最终一致性的分布式事务方案,通过业务数据库+消息日志的组合,实现跨服务事务的可靠传递。
核心思想
先执行本地事务,再异步通知下游。
核心原理
1. 事务发起阶段
// 订单服务创建订单
@Transactional
public void createOrder(Order order) {
// 1. 业务操作:创建订单记录
orderDao.insert(order);
// 2. 事务操作:写入本地消息表(同库同事务)
Message msg = new Message();
msg.setBizId(order.getId());
msg.setContent(JSON.toJSON(order));
msg.setStatus("UNSENT"); // 初始状态
messageDao.insert(msg);
// 事务提交:订单数据和消息记录原子性持久化
}
- 关键点:业务操作与消息写入在同一个数据库事务中
- 消息表结构:
CREATE TABLE local_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_id VARCHAR(64) NOT NULL, -- 业务ID(如订单ID) content TEXT NOT NULL, -- 消息内容(JSON) status ENUM('UNSENT','SENT','CONSUMED') NOT NULL, retry_count INT DEFAULT 0, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
2. 消息投递阶段
- 可靠性保障:
- 定时任务扫描未发送(
UNSENT
)或发送失败的消息 - 消息发送成功后更新状态为
SENT
- 失败重试(指数退避策略),超过阈值报警
- 定时任务扫描未发送(
3. 消息消费阶段
// 下游服务:库存消费扣减消息
@KafkaListener(topics = "order_created")
public void consumeOrderMessage(Message message) {
// 1. 幂等检查(防止重复消费)
if (deductLogService.exists(message.getBizId())) {
return;
}
// 2. 执行业务操作
inventoryService.deductStock(message.getContent());
// 3. 记录消费日志(业务事务)
deductLogService.logSuccess(message.getBizId());
// 4. 可选:向上游发送ACK
}
- 关键设计:
- 消费幂等性:通过
biz_id
去重 - 业务与日志原子性:扣减库存和记录日志在同一事务
- ACK机制:消息队列自动ACK或手动ACK
- 消费幂等性:通过
消息可靠投递方案
方案1:本地消息表
- 优点:实现简单,无中间件依赖
- 缺点:消息表与业务库耦合
方案2:独立消息服务
- 优势:业务与消息解耦
- 代价:增加一次网络调用
方案3:RocketMQ事务消息
// RocketMQ 事务消息
public void createOrderWithMQ(Order order) {
// 1. 发送半消息(对消费者不可见)
TransactionSendResult result = producer.sendMessageInTransaction(
new Message("order_topic", JSON.toJSONBytes(order)),
null
);
// 2. 执行本地事务
if (result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
orderDao.insert(order);
}
}
// 事务监听器(实现回查)
class OrderTransactionListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
return LocalTransactionState.COMMIT_MESSAGE; // 根据业务结果返回
}
}
- 流程:
- 发送半消息 → 2. 执行本地事务 → 3. 提交/回滚消息
- 优势:无需本地消息表,由MQ保证最终一致性
方案4:MySQL Binlog监听(Canal + Kafka)
- 适用场景:数据库变更通知(无需改业务代码)
- 局限:只能捕获DB变更,无法携带业务上下文
消息丢失与重复的策略
风险 | 产生原因 | 方案 |
---|---|---|
消息丢失 | 投递前服务宕机 | 事务保证消息持久化 |
重复消费 | 消费后ACK失败 | 消费端幂等设计 + 去重表 |
消息无序 | 多分区并行消费 | 相同biz_id路由到固定分区 |
消息积压 | 消费速度<生产速度 | 动态扩容消费者 + 降级策略 |
完整代码(Spring Boot + RabbitMQ)
1. 消息实体与存储
@Entity
@Table(name = "local_message")
public class LocalMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bizId; // 业务ID(订单ID等)
private String content; // JSON消息体
private String status; // UNSENT/SENT/CONSUMED
private int retryCount;
private LocalDateTime createdTime;
}
2. 消息发送定时任务
@Scheduled(fixedRate = 10000) // 每10秒执行
public void sendPendingMessages() {
List<LocalMessage> messages = messageRepo.findByStatus("UNSENT");
for (LocalMessage msg : messages) {
try {
// 发送到RabbitMQ
rabbitTemplate.convertAndSend("order.exchange", "order.key", msg.getContent());
msg.setStatus("SENT");
messageRepo.save(msg);
} catch (Exception e) {
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() > 5) {
alertService.notifyAdmin("消息发送失败", msg.getId());
}
}
}
}
3. 下游幂等消费
@RabbitListener(queues = "order.queue")
@Transactional
public void handleOrderMessage(String jsonMessage, Message amqpMessage) {
OrderMessage message = JSON.parseObject(jsonMessage, OrderMessage.class);
// 幂等检查
if (deductLogRepository.existsByBizId(message.getOrderId())) {
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
return;
}
// 业务操作(扣减库存)
inventoryService.deduct(message.getSku(), message.getQuantity());
// 记录消费日志
DeductLog log = new DeductLog(message.getOrderId(), "SUCCESS");
deductLogRepository.save(log);
// 手动ACK
channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
}
4. 对账补偿任务
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点
public void reconcileMessages() {
// 检查已发送未消费的消息
List<LocalMessage> sentMessages = messageRepo.findByStatus("SENT");
sentMessages.forEach(msg -> {
if (!deductLogRepository.existsByBizId(msg.getBizId())) {
// 重新投递
rabbitTemplate.convertAndSend("order.exchange", "order.key", msg.getContent());
}
});
}
适用场景
✅ 优势
- 业务侵入低:只需添加消息表写入
- 高可靠性:本地事务保证消息必达
- 架构简单:无需额外中间件(如Seata)
⚠️ 劣势
- 时效性差:异步投递有延迟(秒级~分钟级)
- 耦合业务库:消息表与业务库同实例
- 维护成本:需开发消息管理后台
🚀 适用场景
- 订单支付后通知发货
- 用户注册送积分
- 数据变更同步(主库→搜索索引)
- 所有接受最终一致性的业务场景
实践
-
消息表分库分表
-- 按业务ID哈希分表 CREATE TABLE local_message_00 ( ... ) ENGINE=InnoDB; CREATE TABLE local_message_01 ( ... ) ENGINE=InnoDB;
-
消息压缩与清理
// 只保留7天消息 @Scheduled(daily) public void cleanOldMessages() { messageRepo.deleteByStatusAndCreatedTimeBefore( "CONSUMED", LocalDateTime.now().minusDays(7) ); }
-
监控指标
- 消息积压量(
UNSENT
状态数) - 平均投递延迟(写入到发送的时间差)
- 消费失败率(重试次数>3的消息比例)
- 消息积压量(
-
灰度方案
// 在消息头添加灰度标记 MessageProperties props = new MessageProperties(); props.setHeader("gray", "v2"); rabbitTemplate.send(new Message(json.getBytes(), props));
技术选型建议:
- 简单场景 → 本地消息表
- 高并发场景 → RocketMQ事务消息
- 零侵入改造 → Binlog监听方案
本地消息表是分布式事务的实用主义解决方案,平衡了实现复杂度与可靠性。在80%的业务场景中,它比2PC/TCC更具性价比,尤其适合中长尾业务系统。