1. 为什么有人会把 SAGA 和 Paxos/Raft 放在一起聊
先讲个我最近遇到的事儿。
前阵子有个同事跑过来问我,说他在准备系统设计面试,看到别人整理的知识点里把 SAGA、Paxos、Raft 归到了同一个分类下,标题还写着“一致性算法”。他当时就懵了:SAGA 不是处理分布式事务的吗?Paxos 和 Raft 不是解决分布式共识的吗?这俩怎么混到一起去了?
其实有这个困惑很正常。这三样东西在分布式系统里都跟“一致”两个字有关——SAGA 保证的是业务数据最终一致,Paxos/Raft 保证的是多副本状态一致。但它们解决的问题层级完全不同,一个是偏业务层的方案,一个是偏基础设施层的协议。把这俩放在一起聊,本质上是想搞清楚:在一个复杂的分布式系统中,数据一致性问题到底分几个层面?每个层面该用什么工具?
这篇文章我就把我对这两个方向的完整理解拆开讲清楚。先从概念定位说起,再分别深入 SAGA 和 Paxos/Raft 的核心原理,最后聊一下它们在实际系统里怎么协同工作。不管你是刚接触分布式系统的新手,还是已经在业务里写过不少分布式代码的老手,这篇文章应该都能帮你把这两块知识的边界画得更清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先分清两个层面:共识算法和分布式事务各自管什么
2.1 一个经典的歪楼现场
我见过不少团队在讨论架构时,把“我们需要共识算法”和“我们需要分布式事务”这两句话混着说。比如有人提出订单服务要跨库操作,立刻就有同事说“那就上 Raft 呗”。这就是典型的两个层面没分清。
Raft 解决的是“多个节点对同一个值达成一致”,比如三个副本节点,要确认一条日志谁先谁后、提交到哪个位置。它工作在系统内部,是给基础设施用的。而 SAGA 解决的是“一个业务流程分散在多个服务里,怎么保证要么都成功要么都回滚”,它工作在业务逻辑层,是给应用开发用的。
这俩不但不是一个东西,连常见的组合场景都不一样。Raft 通常用在 etcd、Consul、TiKV 这类组件里做元数据同步和数据复制;SAGA 则用在订单、支付、库存这类跨服务的业务链路里。让 Raft 去处理业务流程补偿,或者让 SAGA 去同步多个副本的状态,那都是拿错了工具。
2.2 不同的一致性需求对应不同的一致性方案
要理解为什么需要两套方案,关键是要理解分布式系统里“一致”这件事有不同层级。
- 第一层:状态机一致性。一个系统有多个副本,客户端随便连哪个副本,读到的状态必须一样。这个层面追求的是强一致,解决方案就是 Paxos/Raft 这类的共识协议,把写操作复制到大多数节点上,保证对外只有一个统一的状态执行顺序。
- 第二层:事务一致性。一个业务流程跨越多个独立的数据库或服务,需要像一个本地事务那样,要么全部成功,要么全部回滚。这个层面由于数据分布在多个自治的存储节点上,没有办法用全局锁来实现严格的原子性,所以更多时候用 SAGA、TCC、本地消息表这类柔性事务方案来保证最终一致。
- 第三层:微服务之间最终一致。这是最上层,通常应用系统之间通过消息、事件、异步补偿来收敛状态。这个层面不追求严格意义上的事务,而是接受“短时间不一致、最终一致”的现实。
所以你看,Paxos/Raft 处于第一层,SAGA 处于第二层底层。它们不在一条赛道上,但很多网上文章喜欢把它们并列,是因为它们都属于“在分布式环境下解决一致性”这套方法论的一部分。
2.3 为什么不能用一个方案解决所有层面
有人可能会问:既然 Raft 能保证强一致,那我用 Raft 把多个数据库的状态同步起来,不就能实现跨库事务了吗?理论上可以,比如 Google Spanner 就是用类 Paxos 协议加原子钟来做全球跨数据中心强一致事务的。但问题是,这种方案工程成本极高,不是所有业务都需要。
大多数互联网业务场景并不需要跨库强一致。用户下单之后,订单列表和库存数量短暂不一致,完全可以通过补偿和重试来解决。真要为了一个库存扣减引入全局强一致基础设施,你得到的不是可靠,而是高延迟、高复杂度、高运维成本。SAGA 这类柔性方案的价值就在于:用一个相对简单的状态机加补偿逻辑,换取业务上的最终正确。
所以正确的思维方式是:先判断业务需要哪个层级的一致性,再选择对应的方案。底层系统用共识算法,业务链路用事务方案,二者不是替代关系。
3. SAGA 模式深入拆解:核心原理与你落地时会踩的坑
3.1 SAGA 到底在解决什么问题
SAGA 这个概念最早是 1987 年由 Hector Garcia-Molina 和 Kenneth Salem 在一篇数据库论文里提出的。它解决的核心问题是:当一个长事务拆分成多个本地事务时,如何保证整个流程在部分失败的情况下能够回滚到合理状态。
传统数据库本地事务通过 undo log 实现回滚,修改的数据可以直接撤销。但跨服务的分布式场景里,各个服务的数据是独立的,没有办法共享一个 undo log。SAGA 的做法是不回滚数据,而是通过执行补偿操作来抵消已经完成的步骤产生的副作用。
举个例子,一个完整的下单流程可能包含:创建订单、扣减库存、扣减用户余额。如果扣减余额失败了,SAGA 不要求前面的操作撤销到原始状态,而是执行一个“创建订单”的逆操作——把订单状态改为已取消,同时把扣掉的库存加回来。本质上是“新增一个操作”来抵消“旧操作”的影响,而不是“还原旧操作”本身。
3.2 SAGA 的两种协调模式:编排式与协同式
落地 SAGA 时,你需要先决定由谁来做流程调度的总指挥。这是两个派系的重要分水岭。
协同式(Choreography)是比较早期的实现风格。整个流程没有中心调度器,每个服务执行完自己的本地事务后,发布领域事件,下一个服务监听对应事件后继续执行。比如订单服务发“订单已创建”事件,库存服务收到后扣减库存并发“库存已扣减”事件,支付服务再监听。
编排式(Orchestration)则是引入一个中央协调者,由它来告诉各个服务“你现在该做什么”“做完了下一步做什么”。协调者里有一个明确的状态机,流程卡在哪一步、下一步要触发哪个操作、失败后走哪条补偿路径,全部由它管理。
我自己的实践经验是:协同式在流程固定的简单场景里代码更少、服务间耦合度更低;但一旦流程分支多、补偿操作复杂,协同式的事件链路会变得非常难跟踪,出了问题你不知道是谁没发事件,还是谁收到事件没处理成功。编排式虽然引入了一个中心节点,但这个节点的职责很纯粹,就是调度和执行状态机,配合可视化工具后,整个流程的清晰度比协同式高很多。
目前在工业界,编排式占了绝对主流,比如 Seata 的 SAGA 模式、AWS 的 Step Functions 都是这种思路。
3.3 手写一个最小的 SAGA 协调器
纸上谈兵没有感觉,这里我写一个伪代码级别的 SAGA 协调器核心逻辑,帮助你把编排式的状态流转理解透。
java复制// SAGA 状态机定义
class SagaDefinition {
// 每个步骤包含:执行操作 + 补偿操作
List<SagaStep> steps;
// 正向执行
Object execute(SagaContext ctx) {
List<SagaStep> executedSteps = new ArrayList<>();
for (SagaStep step : steps) {
try {
step.action(ctx);
executedSteps.add(step);
} catch (Exception e) {
// 执行失败,逆序执行已成功步骤的补偿操作
Collections.reverse(executedSteps);
for (SagaStep doneStep : executedSteps) {
try {
doneStep.compensation(ctx);
} catch (Exception ce) {
// 补偿操作也需要重试、记录、告警
log.error("补偿失败: {}", doneStep.getName(), ce);
}
}
throw e;
}
}
return ctx.getResult();
}
}
这段代码看起来简单,但真实落地时情况要复杂得多,主要难在两个地方:
第一,补偿操作的顺序必须严格逆序。比如正常顺序是“创建订单 → 扣库存 → 扣余额”,失败发生在扣余额步骤,那么补偿顺序必须是“加回余额(如果部分成功)→ 加回库存 → 取消订单”,不能反过来。
第二,补偿操作本身也可能失败。真实系统里网络随时会断,你调补偿接口超时了怎么办?不重试就结束,那数据就永远不一致了。所以补偿操作需要配套重试机制、幂等控制、以及人工介入的兜底方案。绝大多数 SAGA 事故都发生在补偿失败之后没人管的状态,而不是补偿逻辑写错了。
3.4 SAGA 落地时最容易忽略的几个细节
第一个坑是幂等。无论是正向操作还是补偿操作,网络超时重试是必然的。同一个请求发两次,第一次成功了,第二次又执行一遍,数据就错了。比如扣库存接口被重试了两次,库存就多扣了。解决办法是每个操作都带上全局唯一的请求 ID,服务端通过这个 ID 判断是否已经处理过,处理过就直接返回上次的结果。这个能力必须在设计阶段就考虑进去,不能上线后再补。
第二个坑是悬挂和空补偿。所谓悬挂,是指补偿操作比正向操作先到达,或者正向操作还没完成就收到了补偿请求。这种情况在分布式环境中因为消息乱序是可能的。处理方式是给每个事务操作维护一个生命周期状态,操作只有在满足前置状态时才能执行,否则直接拒绝。状态机不仅是用来记录流程走到哪一步的,也是用来拦截非法状态转换的。
第三个坑是事务边界的粒度。SAGA 的每个子事务都应该是一个可以独立提交的本地事务,不要把多个不可分割的操作塞进一个步骤里。比如“创建订单并发送短信通知”最好拆成两步,否则订单创建成功但短信发送失败,补偿时会把订单取消,但用户还是收到了短信,看起来就很奇怪。
4. Paxos 和 Raft 共识算法:从原理到工程实现的对比
4.1 共识算法到底要解决什么问题
如果说 SAGA 是业务层在面对多服务时的妥协方案,那共识算法就是系统层在面对多副本时的底线保障。
设想你有一个服务要部署三个副本,客户端写了一个值,这个值到底以哪个节点为准?如果 A 节点收到了写请求,B 节点也收到了,两个节点各自把这个值写进了自己的存储,其他节点读的时候该听谁的?共识算法给这个问题的答案是:所有节点通过投票机制,选出一个大家都能接受的写入顺序,只有被大多数节点确认的值才算是最终提交的值。
这就引出了共识算法的几个核心特征:每个节点可以提案,但最终只有一个提案被选中;被选中的提案必须被大多数节点知晓;如果某个节点在提案过程中挂掉,剩余节点必须能继续工作。这三个特征直接决定了共识算法的基本框架:提案、投票、多数派确认。
4.2 Paxos 的历史地位和它的复杂度问题
Paxos 是 Leslie Lamport 在 1990 年提出的共识协议,后来在 1998 年正式发表。它是第一个被严格证明正确性的共识算法,所以它的历史地位无可替代。几乎所有后来的共识算法,包括 Raft,都能看到 Paxos 的思想影子。
但为什么工程界使用 Paxos 的并不多?原因很简单:太难实现了。Paxos 本身被拆分成几个子问题,传统 Paxos 也叫 Basic Paxos,它只解决“对一个值达成一致”这个单轮问题。而在实际系统中,日志是一条接着一条的,你需要对每一个日志条目都跑一轮 Paxos,这个效率低得没法用。于是有了 Multi-Paxos,它通过选出一个领导者来跳过大部分准备阶段,但 Lamport 的论文里对 Multi-Paxos 的描述相对简略,很多工程细节没有给出明确的实现指导。
这导致一个非常尴尬的局面:论文里有理论正确性的证明,但照着论文写不出一个可用的工业级共识协议实现。Google 的 Chubby 使用了类 Paxos 算法,但 Google 也没有公开具体的实现细节。所以工程界一直在寻找一个更易懂、更易实现的共识协议,这就是 Raft 出现的背景。
4.3 Raft 如何把共识算法拉下神坛
Raft 的目标是提供一个比 Paxos 更容易理解和实现的共识算法。它把共识问题分解成了三个相对独立的子问题:领导者选举、日志复制、安全性保障。这样的拆解让算法变得可以被清晰描述和实现,而不是像 Paxos 那样在多个问题之间来回穿插。
领导者选举是 Raft 里比较直观的部分。每个节点有三个角色:领导者、跟随者、候选者。正常情况下只有一个领导者,客户端的请求都发给领导者,领导者负责把日志复制给所有跟随者。如果跟随者在一段时间内没有收到领导者的心跳,它会认为领导者挂了,于是给自己增加任期号并发起选举。得票超过半数的节点成为新领导者。
日志复制是第二个核心环节。领导者收到客户端的写请求后,把日志条目追加到自己的本地日志,然后并行发送 AppendEntries RPC 给所有跟随者。当这个日志条目被多数节点写入成功后,领导者把它应用到状态机,并向客户端返回成功结果。这个过程看起来简单,但细节里最有价值的设计是“日志匹配特性”:领导者在发送日志时,会带上前一条日志的索引和任期号,跟随者发现自己的日志和领导者的不匹配就拒绝接收。这个机制保证了所有节点日志的一致性。
安全性保障是 Raft 最容易忽略但最核心的部分。比如选举限制:只有日志足够新的节点才能当选领导者;提交规则:领导者不能根据旧任期号的日志副本数量来决定提交,必须通过当前任期的新日志间接提交。这些规则保证了 Raft 满足状态机安全性——所有节点以相同顺序应用相同的日志条目。
4.4 Paxos 和 Raft 在实际工程中的选型思考
在工业界,Raft 的普及程度明显高于 Paxos。etcd、Consul、TiKV、MongoDB 副本集、Redis 集群之下的底层协调,都有 Raft 的影子。这些系统选择 Raft,主要看重的是实现的确定性和可维护性。团队里任何一个有两年经验的工程师,读几篇 Raft 的教程就能大致讲清楚整个流程,这在 Paxos 上基本是不可能的。
但 Paxos 并没有消失。一些基础组件追求极致的性能和群组扩展能力,还是会倾向 Multi-Paxos 的变种。比如微信的 PhxPaxos、腾讯的 PaxosStore,都是在 Paxos 基础上做了大量工程优化。这些系统敢用 Paxos,是因为他们有顶尖的分布式系统团队,有能力和精力去啃这个硬骨头。
所以选型建议其实很直白:如果你的团队不是专门做存储基础设施的,优先选 Raft;只有当你的场景明确需要 Paxos 特有的一些优化点,并且团队有人能彻底吃透 Paxos 论文并做出改动时,才考虑 Paxos。
5. 一个系统里 SAGA 和共识算法如何协作共存
5.1 从实际架构看它们各自的位置
很多人以为 SAGA 和共识算法是两个互相独立的世界,一个跑在应用层,一个跑在基础设施层,互不相关。但在真实的系统架构里,它们往往是协同工作的。
我们看一个典型的微服务系统。订单服务、库存服务、支付服务各自有独立的数据库。这些数据库如果做了主从复制或者多副本部署,那副本之间的数据同步就需要 Raft 或 Paxos 来保证。也就是说,每个服务内部的数据一致性问题,由共识算法来解决。
但是订单服务、库存服务、支付服务之间的跨服务业务一致性,共识算法管不到,这里需要 SAGA。举个例子:用户下单之后,订单服务本地事务成功,通过消息发送“订单已创建”事件;库存服务收到事件后本地扣减库存;支付服务触发扣款。整条链路如果在中途失败,比如库存扣减后发现余额不足,那就需要触发库存的补偿操作,把库存加回来,同时把订单状态改为失败。这个过程完全由 SAGA 协调器管理。
所以一个完整的分布式系统里,这两套方案各管一段:底层数据副本的一致性交给 Raft,上层业务流程的一致性交给 SAGA。它们面对的对象不同,目标不同,但是共同支撑起了整个系统的正确性。
5.2 一个故障场景里的协同兜底
再设想一个实际故障场景,帮你感受它们是怎么合作的。
你的订单服务数据库用了 Raft 做多副本,主节点在华东机房,两个跟随者分别在华北和华南。某个瞬间华东机房网络故障,主节点失联了。此时 Raft 的选举机制会自动触发,华北和华南的跟随者通过多数派选举出一个新的主节点继续服务外部请求。你不需要人为干预,订单服务在短暂的不可用后自动恢复。
但是用户那笔正在进行的下单请求可能已经走到了“创建订单完成、等待扣库存”这一步。这时候 SAGA 协调器发现库存服务一直没返回成功,超时了。协调器决定走补偿路径:调用订单服务的取消订单接口。这个接口会把订单状态变成已取消,同时发布订单已取消事件,库存服务收到事件后执行库存恢复。整个过程里,订单数据库内部的副本状态是 Raft 保证的,而订单和库存之间的业务状态收敛是 SAGA 保证的。
如果这个场景里只有共识算法没有 SAGA,订单数据库的副本能保持一致,但库存扣减了、订单却显示未付款,业务逻辑就是错乱的。反过来只有 SAGA 没有共识算法,业务状态能通过补偿逐步收敛,但数据库副本之间可能已经出现脑裂,系统整体早就不可用了。这两层少了一个都不行。
5.3 设计一个业务系统时的一致性选型清单
我在做架构设计时,会按下面的顺序来梳理一致性需求,你也可以直接拿这套思路当清单用。
第一步,盘点系统里哪些数据需要多副本部署。如果确认需要,就为这些组件选择一套成熟的共识方案:像 etcd、Consul 这类现成组件直接依赖即可,不需要自己实现 Raft;只有当你是在开发一个新的存储系统时,才需要考虑把 Raft 引入到自己的代码里。
第二步,盘点业务链路里哪些流程是跨服务的。对每一条跨服务链路,按业务要求的数据不一致容忍度来决定方案:如果可以接受最终一致,SAGA 是最合适的候选;如果需要实时强一致,那就要考虑是否有必要引入全局强一致基础设施,或者干脆把服务合并。
第三步,确定事务方案的具体模式。如果你的链路有明确的主流程、分支多、补偿逻辑复杂,选编排式 SAGA;如果链路简单、服务少,可以考虑协同式,但一定要准备一套事件追踪的排查手段。
第四步,为每一个正向步骤和补偿步骤定义好幂等键、状态机和重试策略。这些工作不能等系统上线后再补,设计阶段就要把三个要素明确下来:请求唯一标识怎么生成、状态机允许哪些状态流转、补偿失败后重试多少次然后人工介入。
6. 常见误区和排查经验速查
6.1 关于 SAGA 的常见误区
市面上关于 SAGA 的说法很多,但有几个高频误区我每次都想纠正一下。
误区一:SAGA 可以替代本地事务。这是错的。SAGA 里的每个子步骤自己仍然需要本地事务来保证该步骤内部的原子性。比如扣减库存这个动作,本身要在库存服务里开启一个数据库事务来执行 update 语句,只是这个事务不需要跨库而已。
误区二:SAGA 的补偿和回滚是一回事。严格说,SAGA 的补偿是执行一个业务语义上相反的操作,不要求数据恢复到执行前的物理状态。比如你扣减了库存 5 件,补偿操作是增加库存 5 件。如果这 5 件库存已经被其他并发事务修改,加回来后的数字也可能和最初不同,但从业务角度看,它恢复了可用库存的语义。
误区三:SAGA 失败后系统会自动恢复。真实情况是,SAGA 只保证在你定义了完整补偿路径的情况下,可以把已经执行的步骤补偿掉。但如果某个补偿步骤依赖的外部服务也挂了,整个流程就会卡住。你需要一个定时扫描未完成事务并把它们推进到终态的兜底组件,这个组件在技术方案里常常被忽略。
6.2 关于 Raft 的常见误区
Raft 也有几个值得拿出来说的误区。
误区一:多数派意味着任何时候都能继续工作。多数派确实保证了只要过半节点存活,系统就能继续选主和服务。但要注意,这里的多数派是相对当前集群总节点数来说的,并不是相对存活节点数。一个 5 节点集群挂 2 个节点还能工作,但挂 3 个节点就只剩下 2 个,不构成多数派,整个集群不可用。这是为了数据一致性付出的代价,不是 bug。
误区二:领导者切换不会丢失已提交的数据。Raft 通过选举限制确实保证了已提交日志在新领导者上任后依然存在。但如果你使用的是被阉割过的 Raft 实现,或者不小心修改了选举条件,这个保证就不成立了。我见过一个团队为了“提升选举成功率”,把选举限制里的日志新旧判断去掉了,结果发生领导者切换后,一个旧的日志条目被覆盖,数据丢失。这种改动是绝对不应该做的。
误区三:Raft 日志复制不可能产生不一致。实际情况是,Raft 允许跟随者的日志和领导者不一致,它通过强制同步机制让跟随者逐步向领导者对齐。网络分区、节点重启这些都可能导致日志出现分歧,但 Raft 的正确性在于最终会收敛,而不是过程中永远一致。
6.3 一个排查 SAGA 问题的实践思路
如果你在线上遇到 SAGA 流程卡住,比如订单一直处于“已创建但未完成”的中间态,我建议按照这个顺序排查。
先看事务状态表。如果你的 SAGA 协调器维护了一张事务实例表,先定位这个实例当前停在了哪个步骤、状态值是什么。然后看是正向操作没返回,还是补偿操作没返回。通常这种卡住的问题,要么是调用的下游服务超时,要么是回调接口因为没有幂等而拒绝执行。
再看日志。SAGA 协调器一般会在每次状态转换时打印日志,包括步骤名、入参、出参、执行耗时和错误信息。你找到对应事务 ID,把整个流程日志从后往前翻,定位最后一次成功执行的操作和紧接着失败的调用。
最后看下游服务的状态。有可能是下游服务收到了请求,执行了操作,但返回响应时网络断了,协调器认为执行失败走了补偿。如果补偿操作也有类似问题,就可能出现正向操作和补偿操作交替进行的“抖动”状态。这时候就需要人工介入,用管理端工具手动推进或终止这个事务实例。
我自己的经验是,绝大部分 SAGA 问题都能通过“幂等做没做好”和“状态机允不允许这个转换”这两个问题来找到根因。先把这两个基础问题排查好,再考虑是不是协调器本身有 bug。
7. 一个实用的小结思路
聊了这么多,最后分享一个我在实际工作中反复用到的思路。
我每次看一个分布式系统设计文档,都会先问自己两个问题:第一,这个系统里哪些组件需要多副本强一致?第二,哪些业务流程可以容忍最终一致?第一个问题的答案指向共识算法,第二个问题的答案指向 SAGA 这类事务方案。
把这两个问题想清楚,你就不会被各种概念绕晕,也知道该在哪个层面引入什么技术。SAGA 和 Paxos/Raft 不是竞品,不是二选一的关系,它们是分布式系统不同层面的一致性解决方案,各自守着一道门。
如果你现在正准备设计一个跨服务业务流程,我建议你先把 SAGA 的状态机画出来,再为每个步骤定义好幂等策略和补偿路径,最后再去考虑底层存储用 Raft 还是别的方案。按这个顺序来,整个系统的正确性会清晰很多。
