先明确一件事:网上聊 SAGA 和 Raft 的时候,最常见的就是把两个词揉在一起对比,仿佛“分布式一致性”就是它们俩的全部。但说实话,这俩压根不是一个层面的东西。SAGA 是长事务拆分的补偿模式,处理的是业务状态最终一致;Paxos/Raft 是共识算法,解决的是分布式系统里各节点对某个值“达成一致”的问题。一个是业务设计模式,一个是底层一致性协议,放在一起对比是因为它们经常出现在同一个系统里,却总被混为一谈。
这篇文章我从实际落地角度出发,把 SAGA 和 Paxos/Raft 各自的核心原理、典型使用场景、踩坑经验、以及它们如何组合在一套分布式系统里各司其职,一次性讲清楚。
1. 概念纠偏:为什么 SAGA 和共识算法总被放在一起比较
1.1 两类算法的本质区别
先说结论:SAGA 和 Paxos/Raft 不属于同一个抽象层级,硬放在一起比“谁更重要”没有意义。
- Paxos/Raft 处理的是“多个节点对一个值达成一致”。典型场景:领导选举、分布式锁、复制状态机、配置同步。它们保证的是系统内部状态的一致性。
- SAGA 处理的是“一次跨多个服务的业务操作如何保证最终一致性”。典型场景:下单扣库存、支付账务、跨服务订单状态流转。它保证的是业务层面的最终一致。
用一个生活化类比来说:Paxos/Raft 解决的是“一群裁判如何对比赛结果达成共识”,SAGA 解决的是“一个大型比赛的多环节流程该如何设计,才能保证任何一个环节出错时整体成绩仍然有效”。
Paxos/Raft 关心的是数据在集群中能不能被正确复制、多数派是否同意同一个值;SAGA 关心的是业务从开始到结束经过多个微服务,中途某个服务失败后,系统能不能通过逆向补偿把状态恢复到一个合理的结果。
1.2 为什么会产生混淆
混淆的来源主要有三个:
- 微服务架构普及后,分布式事务这个词被滥用,很多文章把 SAGA、TCC、2PC、Raft 一股脑归到“分布式一致性”大帽子下。
- 一些中间件同时涉及两类机制。比如 etcd 使用 Raft 做共识,但它也提供事务 API;Seata 的 AT/TCC/SAGA 模式下,后端也可以依赖 Raft 类共识组件做高可用。
- 面试题高频出现“SAGA 和 Raft 有什么区别”,导致大量开发者被训练成“把两个词对比”的思维模式。
正确的理解方式:共识算法是基础设施层的能力,SAGA 是业务编排层的模式。两者可以共存也可能互相配合,但不存在替代关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SAGA 与 Paxos/Raft 的核心机制深度拆解
2.1 Paxos 到底解决了什么问题
Paxos 由 Leslie Lamport 在 1990 年提出,论文因“古希腊议会”的寓言形式当年被评为过于晦涩,直到 1998 年才正式发表。它的核心目标:在一个可能发生节点宕机、网络分区、消息延迟的异步系统中,保证多个节点在某个提案上达成一致。
Paxos 的基本角色:
- Proposer:提出提案,值由客户端请求带入。
- Acceptor:接收提案,投票决定是否接受某个值。
- Learner:学习最终选定的值,不参与投票。
两阶段流程(Basic Paxos):
- Prepare 阶段:Proposer 选择一个提案编号 n,向多数 Acceptor 发送 Prepare 请求(编号 n)。Acceptor 收到后,如果 n 大于它已经见过的最小编号,则承诺不再接受编号小于 n 的提案,并返回当前已接受的最大编号提案(如果有)。
- Accept 阶段:Proposer 收集到多数 Acceptor 的 Promise 响应后,从中选出编号最大的已接受值作为本次提案值(如果没有则使用自己的值),向这些 Acceptor 发送 Accept 请求(编号 n,值 v)。Acceptor 收到 Accept,只要编号不小于自己承诺过的最小编号,就接受该提案。
Paxos 保证了安全性:在任意时刻,最多只有一个值被多数派接受。
理解 Paxos 一个关键点:批准某个值需要多数派,但值确定的过程可能不止一轮。如果两个 Proposer 同时发起,会出现活锁——A 发出 Prepare(1),B 发出 Prepare(2),A 收到拒绝后重新 Prepare(3),B 收到拒绝后 Prepare(4),如此反复。
2.2 Raft 对 Paxos 的改进与工程化影响
Raft 本质上是 Multi-Paxos 的一个简化变体,目标是“可理解性优先”。Diego Ongaro 在 2014 年的博士论文中明确提出:Raft 的目的不是发明新算法,而是把 Paxos 的工程难点拆分成容易理解和实现的子问题。
Raft 的核心机制:
- 领导选举(Leader Election):集群中有一个 Leader,任期 term 单调递增。Follower 如果在选举超时时间内没有收到 Leader 的心跳,就转换为 Candidate,自增任期并请求投票,获得多数票后成为 Leader。
- 日志复制(Log Replication):Leader 将客户端请求包装成 Log Entry,复制到所有 Follower。Follower 写入本地日志后回复成功,Leader 收到多数派确认后提交该日志,并向客户端返回结果。
- 安全性(Safety):日志只能从 Leader 流向 Follower;Leader 只能提交自己任期内的日志;选举时 Candidate 必须包含所有已提交日志,通过比较日志的索引和任期来保证。
Raft 与 Paxos 的关键差异:
| 维度 | Paxos | Raft |
|---|---|---|
| 算法复杂度 | 高,难以直接实现 | 中,论文核心章节可直接对应代码结构 |
| 领导人角色 | 可选,Multi-Paxos 有 Leader 但无明确选举流程 | 必须有明确选举流程,Leader 是核心 |
| 日志顺序 | 可乱序提交,需要额外处理 | 严格顺序,日志索引 + 任期唯一确定 |
| 成员变更 | 额外的 Joint Consensus 或手动停机 | 有专门的日志条目方式处理 |
| 工程实现 | 几乎没有教科书式直接落地案例 | etcd、Consul、TiKV 等大量开源实现 |
实际项目中,我很少见到有人从零实现 Paxos,大多数团队在没有特殊需求时都直接用 Raft 或基于 Raft 的现成组件。
2.3 SAGA 模式的核心流程与补偿逻辑
SAGA 是一个“长事务拆分”模式,1987 年由 Hector Garcia-Molina 和 Kenneth Salem 提出。核心思想:将一个分布式事务拆分为一串局部事务 T1, T2, ..., Tn,每个局部事务对应一个补偿动作 C1, C2, ..., Cn。
执行流程:T1 → T2 → ... → Tn,全部成功则事务完成。如果执行到 Ti 发生失败,则逆序执行 C(i-1), ..., C1,把已经完成的局部事务全部回滚。
SAGA 的关键点:
- 正向事务:每个局部事务是真正的业务操作,已经提交到数据库,不是预占。
- 补偿事务:逻辑上撤销正向事务效果的操作,必须幂等。
- 最终一致性:SAGA 不保证中间状态对外可见的一致,只保证最终状态要么全成功,要么回滚到初始状态。
两种编排方式:
- 编排(Choreography):每个服务自己监听事件,执行完发布下一个事件。优点是去中心化,缺点是流程隐式,排障困难,事件风暴后链路难以追踪。
- 编排(Orchestration):中心化一个 Saga 编排器,负责协调每一步执行和回滚。优点是流程显式、可监可控,缺点是编排器本身是单点。
我见过很多团队对“编排”的翻译产生过混乱,实际上:
- Choreography = 舞蹈编排,每个舞者自己动,靠约定。
- Orchestration = 管弦乐指挥,一个指挥家发指令。
这两者翻译成中文都可能叫“编排”,但实现差异非常大。写文档或设计评审时建议直接保留英文。
3. 实际应用与选型实操
3.1 基于关键维度设计选型决策
先看几个典型的选型场景。
场景一:你有一个 MySQL 集群,需要保证多副本数据一致。这个时候选 Raft,理由很直接:共识算法是复制状态机的基础,你需要所有副本按相同顺序执行写操作。直接用 etcd/Consul,或者基于 Raft 的分布式数据库引擎。
场景二:你的微服务中有订单、库存、账户三个服务,需要实现下单减库存扣余额。请用 SAGA,且优先考虑 Orchestration 模式。理由:业务链路长,需要人工可追溯的调用链,最好有一个可视化编排器来支持回滚。
场景三:多个服务共享一个配置中心或元数据中心,需要实时同步配置变更。用 Raft 解决共识,实现自动选主、日志复制、故障转移。
3.2 细节考量建议与存储层配置思路
在真正动手实现 Raft 时,不要去重写算法。以应用最广泛的 etcd 为例,搭建一个基础的三节点 Raft 集群:
bash复制# 节点1
etcd --name infra0 --initial-advertise-peer-urls http://192.168.1.10:2380 \
--listen-peer-urls http://192.168.1.10:2380 \
--listen-client-urls http://192.168.1.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.1.10:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra0=http://192.168.1.10:2380,infra1=http://192.168.1.11:2380,infra2=http://192.168.1.12:2380 \
--initial-cluster-state new
# 节点2
etcd --name infra1 --initial-advertise-peer-urls http://192.168.1.11:2380 \
--listen-peer-urls http://192.168.1.11:2380 \
--listen-client-urls http://192.168.1.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.1.11:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra0=http://192.168.1.10:2380,infra1=http://192.168.1.11:2380,infra2=http://192.168.1.12:2380 \
--initial-cluster-state new
# 节点3
etcd --name infra2 --initial-advertise-peer-urls http://192.168.1.12:2380 \
--listen-peer-urls http://192.168.1.12:2380 \
--listen-client-urls http://192.168.1.12:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.1.12:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra0=http://192.168.1.10:2380,infra1=http://192.168.1.11:2380,infra2=http://192.168.1.12:2380 \
--initial-cluster-state new
创建好后检查集群健康状态:
bash复制etcdctl endpoint health --cluster
输出结果:
code复制http://192.168.1.10:2379 is healthy: successfully committed proposal: name = f9f...
http://192.168.1.11:2379 is healthy: successfully committed proposal: name = f9f...
http://192.168.1.12:2379 is healthy: successfully committed proposal: name = f9f...
如果某个节点 down 掉,Leader 会在选举超时后触发新一轮投票,在剩余节点中选出新 Leader。这个过程通常 500ms 级别,取决于配置和节点间网络延迟。
3.3 选型决策要点总结
大多数情况下不建议从零实现共识算法。已经生产验证的组件很多:
- 分布式协调服务:etcd(Raft)、Consul(Raft)、ZooKeeper(ZAB,与 Multi-Paxos 类似)
- 分布式消息:NATS JetStream(Raft 复制)
- 分布式数据库:TiKV(Raft)、CockroachDB(Raft)
- 分布式存储:SeaweedFS、MinIO 等对象存储也普遍用 Raft 做元数据复制
SAGA 的开源实现要做甄别,因为“SAGA 编排器”本质上是一个工作流引擎或者状态机。常见方案有:
- Seata 的 SAGA 模式(Java 生态,对国内场景友好)
- Temporal / Cadence(支持长时间运行的业务工作流)
- 自研:基于数据库状态表 + 定时任务实现
我见过很多团队在选 SAGA 框架时简单粗暴地认为“Seata 就是标准答案”,这是不对的。Seata 的 AT 模式需要侵入性地改数据源,SAGA 模式需要手写 JSON 描述编排文件。如果你的业务是长时间运行(比如涉及人工审批、物流通知),Temporal 或自研状态机会更合适。
4. 实现 SAGA 的工程细节:避免“回滚不成”的典型坑
4.1 补偿事务为什么比正向事务更难写
SAGA 最大的工程难点在补偿事务。很多人会以为产生一个反向接口就行:扣库存失败了就加库存,扣余额失败就加回来。但真实业务没这么简单。
举个例子:你在一个电商平台下单,正向事务序列是:创建订单 → 扣库存 → 扣余额 → 通知物流。第三步扣余额完成了,第四步通知物流失败。SAGA 回滚时:
- 通知物流:正向还没成功,不需要补偿。
- 扣余额:生成一笔负向账务记录,金额回补。
- 扣库存:库存回补。
- 创建订单:修改订单状态为“已取消”。
看起来很简单,但有几个隐藏问题:
- 扣余额和回补余额之间,如果余额已经被其他事务消费了怎么办?比如用户余额有 100 元,扣了 20 元买 A 商品,同时另一个事务又扣了 20 元买 B 商品。A 回滚时简单地把余额加回 20,用户余额变 120,但实际业务上同时刻可能有多笔并行扣款。
解决方案:金额回补必须使用“流水对冲”,而不是简单累加。产生一条 -20 的账务流水,同时余额以“当前值 + 对冲金额”的方式更新,而不是“当前值 + 20”。
- 回滚期间,用户可能把商品用掉了。比如订单创建后扣了库存,用户在物流送达前申请了退货。此时如果库存已经出库,回滚时“加库存”的逻辑错了——应该加的是“可售库存”,而不是“物理库存”。
这类问题说明一个关键原则:补偿事务要和正向事务反着做,但不是“反过来做一遍”,而是“基于业务语义的撤销”。这也是为什么 SAGA 事务设计前必须梳理所有参与方的“补偿语义”。
4.2 幂等性:补偿失败最常见的原因
正向接口和补偿接口都必须幂等。因为在网络不确定的分布式环境中,补偿动作可能被执行多次。
假设订单服务在执行补偿时调用账户服务“回补余额”,由于网络超时,账户服务已经更新成功但响应丢失,订单服务会重试。如果没有幂等,余额会被回补两次。
常见做幂等的方式:
- 每个正向事务和补偿事务都带唯一 ID(全局唯一,可以用雪花算法)。
- 接收方在数据库中维护一个“处理记录表”,记录已经处理过的 ID 和状态。
- 收到请求后先查处理记录,如果已经处理过,直接返回成功。
sql复制-- 幂等表
CREATE TABLE IF NOT EXISTS txn_idempotent (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
txn_id VARCHAR(64) NOT NULL,
service_name VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
create_time DATETIME NOT NULL,
UNIQUE KEY uk_txn_id (txn_id, service_name)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
事务流程:
- 收到补偿请求,解析出 txn_id 和 service_name。
- 尝试插入 txn_idempotent 记录,如果 Duplicate Key 则直接返回成功。
- 执行实际补偿逻辑。
- 更新该记录 status 为 DONE。
这套逻辑能解决 99% 的重复补偿问题。剩下 1% 是插入成功但执行补偿时服务宕机,重启后无法判断上次任务是否完成。这时候需要引入“补偿任务状态机”,在数据库中记录任务 COMPENSATING / COMPENSATED / FAILED 三种状态,配合定时任务扫描 COMPENSATING 超过 N 分钟的记录并重试。
4.3 悬挂问题与空回滚
SAGA 里还有一个很容易被忽视的问题:空回滚。
场景:编排器向库存服务发送“扣库存”请求 T2,但由于网络原因请求丢了。编排器超时后认为 T2 失败,进入回滚流程,向库存服务发送 C1(补偿 T1)。但就在这期间,库存服务实际没有执行 T2。此时执行 C1(回滚 T1)是有意义的,但如果该补偿的事务 ID 对应 T2,库存服务需要识别出 T2 根本不存在,也就是说这次“对 T2 的补偿”是空操作,依然需要记录成功。
如果库存服务在收到 T2 超时后,T2 对应的请求在网络中延迟到达并执行了,此刻补偿 C1 已经处理完毕,T2 却执行了,就会出现正向事务已经被补偿过,但业务方在补偿后又执行了正向的情况,也就是本地事务悬挂。
解决思路:借助事务处理记录表和一个“标记状态位”。正向请求先写状态位为 INIT,补偿请求检查到状态位是 INIT 且超过阈值时标记为 SKIPPED。正向请求到达时如果状态位被标记为 SKIPPED,就拒绝执行,保证不悬挂。
这些细节我在几套生产系统里都踩过,Seata 的 AT 模式解决了一部分,但 SAGA 模式里这些边界条件都需要开发者自己处理。这也是为什么我建议业务复杂、要求高的场景优先考虑 Temporal / Cadence,它们把“回滚语义、状态持久化、重试、幂等”都已经内置了。
5. 共识算法与 SAGA 的联合实践:一个电商下单系统示例
5.1 系统架构如何融合三类协议
实际分布式系统中,Raft 与 SAGA 通常各管一段。以一个典型的电商下单链路为例:
- 前端请求到达订单服务。
- 订单服务通过 etcd(Raft)完成分布式锁和元数据同步,获取订单号。
- 订单服务调用库存服务扣减库存,调用账户服务扣减余额。
- 若任一步失败,订单服务作为 Saga 编排器协调反向调用库存回补和账户回补。
这个架构中:
- Raft 解决的是“订单服务实例之间的协调”——比如多个订单服务实例同时生成订单号,需要保证唯一且顺序合理;服务注册发现、配置推送也依赖 Raft。
- SAGA 解决的是“订单、库存、账户三个业务服务之间的数据一致性”。
两者不冲突,也没有嵌套关系。
5.2 状态机与事务日志设计
SAGA 编排器的核心是状态机。我一般这样设计状态:
text复制订单状态流转:
CREATED → INVENTORY_DEDUCTING → ACCOUNT_DEBITING → LOGISTICS_NOTIFYING → DONE
异常流转:
INVENTORY_DEDUCTING → FAILED → COMPENSATING_INVENTORY → COMPENSATING_ACCOUNT → COMPENSATION_DONE
ACCOUNT_DEBITING → FAILED → COMPENSATING_ACCOUNT → COMPENSATION_DONE
LOGISTICS_NOTIFYING → FAILED → COMPENSATING_ACCOUNT → COMPENSATING_INVENTORY → COMPENSATION_DONE
这个状态机的每一步产出事件;事件写入本地事务表并发到消息队列;编排器消费事件后驱动下一步。这个设计可以直接落库:
sql复制CREATE TABLE saga_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
current_state VARCHAR(32) NOT NULL,
data JSON NOT NULL,
status VARCHAR(16) NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
CREATE TABLE saga_event (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
event_type VARCHAR(32) NOT NULL,
payload JSON NOT NULL,
create_time DATETIME NOT NULL
);
使用数据库表来持久化 SAGA 实例和事件,可以避免内存状态机在进程崩溃后全部丢失。配合定时任务扫描超时实例,可以弥补消息队列丢失事件导致状态机停滞的问题。
5.3 结合 Raft 实现分布式锁
订单号分配可以用数据库自增,但在分库分表场景下通常会引入一个中心化发号器。很多团队用 Redis 做分布式锁,但 Redis 锁在网络分区时会有脑裂,导致两个服务同时拿到锁。此时用 etcd(基于 Raft 的共识)更稳妥。
etcd 分布式锁的实现思路:
- 客户端创建 key(如 /lock/order-sn),并设置 lease(租约)。
- 使用
Txn事务判断 key 是否不存在,如果不存在则创建,获得锁。 - 如果创建失败,等待并重试。
- 业务完成后释放锁(删除 key),lease 到期也会自动释放,避免死锁。
bash复制# 加锁
etcdctl lock order-sn-lock
Raft 在这里的价值是:虽然集群发生了分区,但只有多数派能选主,少数派即使收到加锁请求也会被拒绝。这把“分布式锁的不可靠性”降到了最低。
5.4 一个常见误区:把 SAGA 事务直接放入 Raft 日志
有些设计文档会出现把 SAGA 的每一步抽象成“日志条目”,希望用 Raft 复制来保证每一步都执行。这在理论上通,但工程上很少这样做,原因有两个:
- Raft 日志复制只能保证同一副本节点上日志顺序一致,不能保证业务服务的补偿逻辑执行成功。日志复制成功不代表业务成功。
- Raft 日志增长会导致磁盘消耗,还会拖慢提交速度。SAGA 事务通常有几十个步骤,日志太大,不适合高频长事务。
正确的方式:Raft 保证基础组件的共识,SAGA 保证业务链路的补偿。两者各自独立,互不干扰。
6. 常见问题与排查技巧实录
6.1 Raft 常见问题速查
| 问题 | 原因 | 排查与解决思路 |
|---|---|---|
| 选举频繁切换 Leader | 网络分区、心跳周期过短、节点负载过高导致延迟抖动 | 检查节点间 RTT;调大 Heartbeat Timeout;检查磁盘 IO(etcd fsync 延迟) |
| Follower 日志与 Leader 不一致 | 节点宕机后重启,日志滞后 | Raft 会自动从 Leader 复制缺失日志,无需人工干预;确认没有启用只读模式 |
| 集群无法选主 | 节点数偶数导致平票;多数派不可达 | 生产环境至少 3 节点;检查成员变更是否正确 |
| 写性能不高 | Leader 是所有写的瓶颈 | 考虑把读请求分发到 Follower(ReadIndex / Lease Read),但要接受一定延迟 |
etcd 一个值得关注的性能参数是 --heartbeat-interval 和 --election-timeout。默认心跳 100ms、选举超时 1000ms。太低会导致频繁选举,太高会导致 Leader 故障时服务中断时间过长。通常建议 heartbeat 设置在 500ms 以上、election-timeout 是 heartbeat 的 5~10 倍。实际生产环境可以根据节点间延迟微调。
6.2 SAGA 常见问题速查
| 问题 | 原因 | 排查与解决思路 |
|---|---|---|
| 补偿多次执行导致数据错乱 | 补偿接口没有做到幂等 | 在接收方维护幂等表;重试前检查补偿任务的执行状态 |
| 回滚期间超时后无继续补偿 | 补偿操作没有可靠的重试机制 | 引入迟滞重试(定时任务扫描);补偿任务状态持久化 |
| 编排器单点故障 | Orchestrator 进程崩溃后无法继续驱动 | 编排器选择无状态化 + 消息队列驱动,或者利用 Raft 选主 |
| 正向步骤成功但响应丢失导致误回滚 | 超时判断与真实执行状态不一致 | 引入“查询接口”确认执行结果;或者用“事务日志”辅助判断 |
6.3 排障心法
处理 SAGA 回滚问题最有效的诊断方法是“打全链路日志 + 追踪 ID”。在每个正向步骤、每个补偿步骤中,都必须打印:
- 事务 ID / Saga ID
- 当前状态
- 输入参数摘要
- 调用结果
- 异常堆栈
这样即使在多服务链路中,也能通过同一个 Saga ID 把整个流程串起来。
曾经遇到过一次非常隐蔽的问题:订单回滚时,账户服务补偿成功,库存服务补偿在并发场景下出现死锁,锁等待超时导致回滚失败。排查时先用 SHOW ENGINE INNODB STATUS 看到了锁等待,后来通过慢查询日志定位到是由于库存表在并发更新时事务隔离级别设置不当导致行锁竞争激烈。解决办法是修改库存扣减语句增加条件过滤,缩小锁范围,并将事务隔离级别从 REPEATABLE READ 降为 READ COMMITTED(在可接受范围内)。
这类问题在常规文档里很难找到答案,只能靠经验和日志积累。
7. 实践经验与扩展建议
目前主流共识算法实现已经非常成熟,除非你是做学术研究或者有极特殊的一致性需求,否则请不要自己造轮子。Raft 已经是事实上的工程标准,etcd、Consul、TiKV 都提供了生产级实现。Paxos 在理论层面依然是必修课,因为论文中关于“安全性”“活性”的分析框架,同样适用于理解 Raft 和一切一致性协议。
SAGA 则需要投入更多时间去设计。很多人第一次接触 SAGA 时会以为“反着调用一遍”就是回滚,但真实业务中的账务对冲、跨账期回退、幂等控制、空回滚这些细节,远不是一个反向接口能覆盖的。建议在没有掌握足够业务细节之前,先把“补偿事务”的语义梳理清楚,再动代码。
如果让我给一个可落地的组合方式,大致是:
- 基础服务的高可用和数据一致性:直接用 etcd / Consul / 分布数据库内置的 Raft 复制,不自己写。
- 业务跨服务事务:优先用 Temporal / Cadence,或者成熟微服务框架的 SAGA 模块。如果团队没有长期维护工作流引擎的能力,自研一个基于状态机 + 数据库事务表 + MQ 的最小实现也是可行路线,但边界条件一定要处理到位。
- 不推荐直接对比 SAGA 和 Paxos/Raft 选型,因为它们的职责范围完全不同。真要画一张系统架构图,Raft 在基础设施层,SAGA 在业务层,两层之间是消息传递和数据访问的接口。
最后分享一个我踩过多次坑后的习惯:不管用什么方案,都应该在系统设计文档里单独画一张“补偿时序图”,明确列出每一步正向和补偿的行为、超时时间、重试次数、失败后的去向。这张图能暴露大多数 SAGA 设计的漏洞,比任何代码 review 都有效。团队里只要有一个人能看懂这张图,分布式事务的排障效率就能高一个量级。
