1. 先澄清一个经常被混淆的核心问题
1.1 事务一致性讨论的其实是"多参与者原子性"
我在很多技术社区的讨论帖里看到过一个高频现象:有人问"分布式事务和Raft到底什么关系",底下回答五花八门,甚至有把两阶段提交说成"共识协议的一种实现"的。这种混淆不难理解——两者都在谈"一致性"这三个字,但站在不同的坐标轴上。
事务一致性指的是对一个或多个数据资源执行的一组操作,要么全部生效,要么全部不生效,这个"原子性"看着简单,在单机数据库里靠日志和锁就实现了。但一旦拆到多个微服务、多个数据库、多个队列中间件里,问题就变味了:订单服务扣了库存,但账户服务那边余额扣减失败了,此时库存已经被扣掉了,怎么回滚?谁来决定是提交还是回滚?这正是分布式事务要解决的问题——它管的是多个参与者(Participant)之间的协作结果。
这个问题的关键并不是"大家怎么达成共识",而是"怎么保证所有参与方最终执行的是同一个结果"。这背后有一个天然的"拍板者"角色——事务协调者(Coordinator),由它向各个参与者发出准备、提交或回滚的指令。所以分布式事务更像是一个指令分发与结果收集的过程,参与者没有决策自由,只有执行义务。
1.2 共识协议解决的是"单日志的复制与定序"
Paxos和Raft解决的是另一个物种的问题:多个副本之间,如何就一系列操作日志达成一致,确保每个副本最终处于相同状态。
这里最核心的矛盾是——没有中心协调者,或者说不能依赖单一协调者,因为协调者本身可能宕机。多个节点地位平等(或者说可以通过选举动态产生领导者),任何一个节点宕机后,剩余节点依然能够继续对外服务。Raft的领导者选举、日志复制、提交条件,本质上都在回答一个问题:多个副本之间,哪个节点的日志是可以信的?新选出的领导者的日志是否完整?旧的未提交日志如何处理?
你可以把共识协议理解为"决定日志顺序的机制"。它本身不关心日志里装的是什么——装数据库写操作就是数据库高可用集群,装状态机命令就是配置同步服务,装的是业务流水也是一样。关键是所有节点必须按照完全相同的顺序应用这些日志,最终状态才一致。
1.3 两者的本质区别在一个"谁说了算"
如果用大白话总结上面的区别:分布式事务是"有一个老板在指挥大家干活,老板说了算"——难点在于老板单点故障怎么办、员工中间改主意怎么办;Paxos/Raft是"没有固定老板,大家按规矩投票选话事人"——难点在于网络分区时怎么保证不出现两个话事人。
这个区别直接决定了它们的适用场景完全不同。事务处理强调一组操作跨多个资源时的原子性,共识协议强调一份复制日志在多个副本上的顺序一致性。前者面对的是"多个不同数据源"的问题,后者面对的是"一份数据源的多份拷贝"的问题。
从实际从业者的视角看,理解这个差异的最快方式不是背概念,而是看它们的工作机制。我下面把这两套机制的内部运行逻辑详细拆一遍,你就能彻底看清它们不在一个平面上。
2. 机制拆解:2PC/3PC/TCC 与 Paxos/Raft 的关键差异
2.1 分布式事务的经典实现:协调者驱动的两阶段提交
两阶段提交(2PC)是最经典的分布式事务方案。Phase 1是准备阶段:协调者给每个参与者发"Prepare"请求,参与者各自执行本地事务到"可提交"状态(通常是写入Undo/Redo日志,锁定相关资源),然后返回"我准备好了"或"我搞不定"。Phase 2是提交通知阶段:协调者只要收到全部参与者都准备好的回应,就广播"Commit";只要有一个参与者返回失败,就广播"Abort"。
这个机制看着严谨,实际落地时会遇到两个致命瓶颈。第一个是阻塞问题:如果协调者广播Commit后自身宕机,参与者处于已经提交但无法确认的状态,客户端也不知道最终结果,只能一直等协调者恢复。第二个是单点问题:协调者本身就是单点,它一挂,整个事务悬在半空,全球用户都卡在"处理中"页面上。
三阶段提交(3PC)通过引入"预提交(PreCommit)"阶段、增加超时机制,缓解了阻塞问题,但依然没有彻底解决网络分区下的决策冲突,而且实现复杂度高出不少,业界真正大规模使用的还是2PC的变种居多。
在我做过的模拟项目X里,我们用的是TCC(Try-Confirm-Cancel)——本质上把事务控制权下沉到业务层,Try阶段冻结资源,Confirm阶段真正执行,Cancel阶段做回滚。这套方案不依赖底层数据库的XA能力,但要求每个参与方自己实现Try/Confirm/Cancel三段逻辑,开发量翻倍,对业务侵入不小。TCC换来的好处是可以灵活处理异构资源:Redis缓存、HTTP接口、消息队列都能纳入事务范围。
2.2 共识协议的内部逻辑:法定人数和领导者
Paxos/Raft走的是完全不同的路子。Raft把共识问题简化成三个子问题:领导者选举(Leader Election)、日志复制(Log Replication)、安全性(Safety)。它依赖"多数派(Quorum)"决策:任何一条日志要成为已提交状态,必须被超过半数的节点持久化。
在Raft中,只有领导者可以处理客户端的写请求。领导者把日志条目复制给所有跟随者,当一条日志被复制到多数节点后,这条日志就算提交了,领导者把它应用到状态机上,并通知跟随者继续复制。请求到达一条、复制一条、应用一条,所有节点最终状态一致。
Paxos更抽象一些。Basic Paxos通过网络中的任意节点发起Prepare请求来竞选编号,再通过Accept请求让日志真正选定某个值。要想让一个值被选定,必须获得多数Accept,而任何两个多数派必然有交集,这就保证了"不可能同时选定两个不同的值"。Multi-Paxos则通过选举一个稳定领导者,在大部分时间内只走Accept阶段,性能才接近Raft。
这里面最关键的设计思想是"多数派相交性"。把这个原理翻译成人话就是:如果你和我在两个不同的会议室里分别做决策(网络分区),你那边凑了三个人,我这边也凑了三个人,但5个节点的集群里最多只能允许两个3人组没有重叠——只要每个决策都要求超过一半的人签字,任何两个决策必然至少共享一个签字人,这个共享人就能揭示"另一个决策已经选了谁"。
2.3 一张表说清两者的核心差异
这张对比表是我在给组内新人培训时整理的,几乎可以当速查手册用:
| 对比维度 | 分布式事务(2PC/3PC/TCC) | 共识协议(Paxos/Raft) |
|---|---|---|
| 核心问题 | 跨多资源操作的原子性 | 单份日志在多个副本上的顺序一致 |
| 参与者关系 | 中心化,有协调者 | 去中心化,领导者由选举产生 |
| 决策机制 | 协调者收集结果后仲裁 | 多数派投票 |
| 故障模型 | 协调者故障会阻塞事务 | 领导者故障可重新选举 |
| 典型场景 | 订单 + 库存 + 账户跨库扣减 | Redis哨兵选主、ZooKeeper选主、数据库高可用 |
| 数据源类型 | 异构资源(数据库/缓存/队列) | 同构副本(同一服务的多个实例) |
| 对应理论 | 原子性(ACID中的A) | 顺序一致性 / 线性一致性 |
| 性能代价 | Prepare/Commit两轮RTT,锁持有时间长 | 日志复制期间QoS高,但写入可并行 |
很多人看到"多数据资源"和"多副本"都是"多",就觉得是同一回事。实际上一个"多"是广度问题——数据种类多、接口多;另一个"多"是深度问题——同一份数据复制多少份。方向完全不同。
3. 真实系统里它们如何组合:一个下单场景的链路拆解
3.1 用Raft做高可用,用事务做跨库原子性
实际生产系统里,这两套机制通常不是"二选一",而是各管一段后叠在一起用。一个典型电商微服务架构通常长这样:
- 订单库、库存库、账户库分别部署成主从集群,每个集群内用Raft或类Raft协议做日志复制和主节点自动切换,保证数据库本身高可用;
- 下单时要跨订单库、库存库、账户库三个数据库保证原子扣减,这时候用分布式事务(XA两阶段或TCC)来协调三个数据库的提交与回滚。
这两层各自解决各自的问题:Raft管的是"某个数据库挂了之后,数据不能丢,服务不能断";分布式事务管的是"三个数据库作为一个整体,要么都看到这个扣减,要么都看不到"。如果Raft把主库切换了但分布式事务不知道,事务状态可能丢失;反过来,如果分布式事务已经提交了但Raft复制还没完成,新主库上可能缺少这部分数据,但事务已经告诉用户成功了。
所以真实系统里,两者存在一个衔接问题——这恰恰是我觉得最有工程价值的地方。分布式事务的协调者必须要对底层数据库的主备感知有一种"容忍策略":要么事务日志本身通过共识协议复制,要么协调者记录完整的事务状态并支持幂等补偿。缺乏这层设计,单纯同时用好这两套机制很容易埋雷。
3.2 一个下单事务的阶段拆解
我以一个标准的下单流程为例,看看几步操作分别落在哪套机制的管辖范围内。
第一步,客户端调用订单服务,发起一个分布式事务。订单服务作为事务发起方,向事务协调器注册事务ID。第二步,协调器向库存服务的数据库发起Prepare——库存库先把"扣减库存"这个操作写入自身事务日志,锁定库存行,但不提交。第三步,同样的Prepare发给账户服务数据库,冻结扣款金额。第四步,协调器汇总所有Prepare结果。全部成功,广播Commit;任何一个失败,广播Abort并触发各参与者回滚。
这个链条里,如果某个数据库恰好是Raft高可用集群,那么Prepare操作本身也会触发一次Raft日志复制——也就是说,单节点本地事务同时经历了两个一致性过程:一是与对等副本之间的日志共识,二是与外部协调器之间的事务协商。这两层各自有各自的提交状态:Raft日志复制可能已经完成了,但分布式事务还没最终提交;或者分布式事务已提交,但Raft复制尚未完成,主节点在这瞬间崩溃的话需要从日志恢复。
这种"两层一致性嵌套"的场景是我在之前一个互联网支付系统的现场排查中遇到的最多的问题。当时我们使用的分布式事务框架里,协调器的事务ID和数据库内部的Raft任期、日志索引完全没有关联,最后不得不在协调器里加点特殊机制来解决状态丢失问题。这是我后来在给别人设计系统时一定会先理顺的一层关系。
3.3 数据库XA事务与分库分表中间件的真实关系
再补充一个常见实践:很多人在分库分表后,直接依赖底层数据库的XA协议做全局事务。XA协议本质是2PC的标准接口,由全局事务管理器(Transaction Manager,TM)充当协调者,各个分库的资源管理器(Resource Manager,RM)完成准备和提交。这套方案简单,开发量小,但锁范围大,长事务下数据库性能衰减明显,而且一旦协调器宕机,各分库的数据要么全部提交要么全部回滚,恢复过程复杂。
更务实的做法是放弃跨库的强一致性,改成最终一致性方案。我在某中间件项目里实践过"本地消息表 + 消息队列"的模式:事务发起方先把业务操作和一条消息记录写入本地消息表,这两个动作在同一本地事务里完成;然后异步把消息投递给下游消费方;下游消费成功后回写状态,本地消息表里标记已投递。整个过程没有全局锁,吞吐量远高于2PC,但引入了延迟,也不再是强一致,只能在"最终一致"这个标准下工作。
回到本文主题:分布式事务2PC和Raft这里面的分工要理清——XA是标准的事务协调协议,Raft决定了分库自身主从节点间的复制共识。两者一个管全局结果,一个管局部高可用,互相依赖又互相独立,这才是很多架构师说"分布式事务系统不能只靠数据库XA,还要考虑高可用部署形态"的原因。
4. 选型决策与常见误区:从故障域到一致性级别
4.1 误区一:以为Raft能直接解决分布式事务问题
我在技术咨询中遇到过很多"Raft万能论"的开发者——他们觉得既然Raft能保证副本一致,那把所有服务的数据放到同一个Raft组里不就行了吗?跨服务调用不就是多台机器执行同一条日志吗?如果大家执行同一个日志,那账不就对上了吗?
这个想法的核心漏洞在于:Raft只能保证日志的复制顺序一致,但日志里的"操作"执行时产生的副作用是否可回滚、是否幂等,Raft完全不管。一个事务要向库存服务扣库存,同时向账户服务扣余额,这两个动作是两个独立的业务操作,它们的执行结果受业务逻辑影响。即使你把这两个操作编码进同一条日志,当账户服务的应用层因为余额不足抛异常时,日志已经复制到所有副本了——Raft层面是"一致"的,但业务层面这个操作是失败的,库存还是被减了。
此路不通的根因在于Raft把"所有节点执行相同的命令"当成假设,而分布式事务的前提是"不同节点执行不同的操作,但这些操作必须同生共死"。命令集合里的某一条可能成功,另一条可能失败,这种异构操作的原子性不是日志复制协议能覆盖的。
4.2 误区二:把"最终一致"等同于"不需要共识"
同样常见的一个误区是反向的:有人为了规避分布式事务复杂性,直接放弃强一致,用最终一致方案,然后以为这样就不需要Raft了。这也是不对的。我见过一个订单系统,订单服务把订单数据写到本地库,然后异步通知下游服务更新缓存,下游服务消费消息时如果从库读取——从库的复制延迟导致读写不一致——数据库从库之间的数据一致性依然依赖主从复制协议(现在主流是通过类Raft的日志复制实现)。
最终一致强调的是"允许有中间状态,但最终数据会一致";而这个最终一致本身依然需要某种底层机制来保证"最终"能落地——可能是消息队列的重试,可能是数据库副本的日志复制,也可能是分布式任务调度框架的定期对账。无论哪种,底层都离不开某种一致性协议。只是不再需要全局事务级别的强一致而已。
4.3 如何根据故障域和数据特性做决策
我给正在做技术选型的朋友一条经验性的判断路径,按顺序回答下面四个问题,基本能确定该选哪套机制:
第一,业务是否要求强一致?账户余额、库存扣减、充值提现这类涉及资金和稀缺资源的核心链路,必须强一致,只能考虑2PC/TCC/XA这类方案;而用户昵称、商品详情、消息已读状态这些可以容忍短暂旧值的场景,可以走最终一致。
第二,操作是否涉及异构资源?涉及的资源类型越多(数据库、缓存、搜索引擎、外部HTTP服务),用2PC/XA越不可行,TCC会是主力;只有两个数据库之间的同构事务,XA还顶得住。
第三,对故障恢复时间的要求有多高?如果协调者宕机超过10秒就不可接受,你的分布式事务方案必须支持事务状态持久化并快速重建协调者。Raft在这里的角色是帮你把协调者状态本身做多副本。
第四,系统的规模到什么程度?单个分布式事务涉及的节点越多,锁竞争越严重,事务成功概率反而因为参与者数量指数下降——3个参与者成功率可能99%,8个参与者可能就跌破95%了。所以不是所有场景都适合把所有服务拉进同一个事务,过半调用可以采用补偿型最终一致方案。
这些判断不是理论推演,都来自实际架构审查中反复看到的取舍模式。千万警惕没有依据的一致性强求,也别在核心资金链路上过度妥协到最终一致。
5. 协议的内部实现差异对系统设计的启示
5.1 Raft任期机制与事务协调的冲突处理
Raft的一个独特机制是任期(Term)和领导者心跳。每个任期内最多存在一个领导者,跟随者在选举超时后递增任期并发起新选举。这个机制保证了日志的唯一权威写入方。但放到分布式事务场景中,会出现一个有意思的交互:如果一个数据库主节点在分布式事务Prepare阶段发生了Raft领导者切换,新领导者可能没有收到旧领导者已确认但尚未提交的日志条目,此时协调者那边还在等这个数据库的Prepare成功回复。
这种冲突我实际处理过不止一次。最稳妥的处理方式是让事务协调者记录每个参与者的"事务进度标记",和Raft日志交互时,不仅要求数据返回,还要带上该数据库Raft组的当前任期号与最新日志索引,协调者把这些信息一并存储。之后任何节点向协调者汇报事务状态,如果任期号比协调者记录的小,说明它是旧领导者视角,不能采信。
如果设计之初没做这个标记,排查时会相当痛苦:所有节点看起来数据都没丢,但事务状态就莫名丢失了,只能靠人工核对数据和消息补偿。这个代价是惨痛的——我之前跟进的一个模拟项目X就是这类问题的持续维护成本让整个团队的低效持续了整整一个季度。
5.2 日志复制与事务日志的写入顺序冲突
另一个值得注意的细节是写入顺序。Raft严格要求日志条目按索引顺序提交并按索引顺序应用到状态机,分布式事务则要求Prepare阶段执行的本地事务不能提前对外可见,必须等Commit指令后才能生效。这两者会产生顺序上的矛盾:Prepare阶段已经把写操作写入Raft日志了,日志复制已完成,但本地事务还没提交,这会导致从库读到的数据可能是全局尚未确认的中间状态。
务实的处理方式有两种。一种是在业务层面设计"事务状态字段",行记录里带一个status字段,Prepare阶段写入"PENDING"状态,Commit阶段更新为"COMMITTED",所有读取业务SQL强制过滤status='COMMITTED'。另一种是用独立的事务状态表配合版本号,查询时以状态表为准。这两种思路本质上是"把原子性从数据操作提升到业务状态流转"。
无论选哪种,都要理解一个底层事实:Raft管的是日志条目落盘,事务管的是数据可见性,两者在时间线上天然有缝隙。用"状态字段"或"状态表"来填充这个缝隙,是每个做分布式事务系统的团队迟早要面对的一课。
5.3 共识协议网络分区和事务阻塞的相似惩罚
虽然前面强调了它们的区别,但它们在故障模型上有一个惊人的相似点:网络分区都是最复杂的敌人。
Raft在网络分区时,如果旧领导者无法联系到多数节点,它会持续收到拒绝的写入请求,直到任期超时,新领导者被选出。这个过程通常需要几个RTT,期间服务短暂不可写——这是可以接受的。
但分布式事务遇到网络分区更麻烦:如果某个参与者在Prepare阶段和协调者断连,协调者不可能收到它的回复,事务只能原地等待直到超时。更麻烦的是,如果协调者和所有参与者都没断,但协调者所在网络区域与"决策元信息存储"之间分区,事务也是悬空状态。一旦分区恢复,参与者已经超时回滚,而协调者还想提交——这就会出现"部分已回滚 + 部分未回滚"的不一致状态。
应对思路是要在协调者和参与者之间引入超时协商机制。参与者Prepare后不无限等待,而是在超时后单方面回滚并标记事务状态;协调者在确认所有参与者都明确回复之前,不能盲目依赖"超时默认视为成功"。这套逻辑我通常建议作为上线验收的必测用例来模拟:人为切断一个参与者的网络,观察整个事务的最终一致性和事务状态表的数据。
6. 一些经过验证的工程经验
6.1 优先保住事务状态的可用性
在实战中,我发现很多团队把精力集中在事务算法本身,却忽略了事务状态存储的可用性。无论用2PC还是TCC,事务协调器自己一定要做成高可用集群——这本身就是一个Raft问题。把事务状态存放到Raft组里,让协调器从单点变成高可用。
我做过的最佳实践就是:事务协调器使用内嵌的Raft状态存储(类似etcd模式),协调器自身节点通过Raft选举出主协调者,事务状态日志由Raft复制。这样分布式事务的整体可用性上限,就取决于Raft状态存储的可用性上限——这个问题其实已经被共识协议解决得相当成熟了。与其在事务协议层面反复纠结,不如先把协调器状态放在一个稳固的多副本底座上。
6.2 明确超时和重试的幂等边界
分布式事务和共识协议对"重复消息"的处理逻辑不同。Raft天然对同一日志条目的重复复制幂等(日志索引唯一),但分布式事务的Commit指令可能重复投递,因此每个参与者必须对同一事务ID的Commit/Abort幂等。
我的建议是:对参与者的处理接口设计成天然幂等。事务ID作为唯一键,处理结果记录在参与者本地表,重复消息来了直接查表返回历史结果。千万别只依赖消息队列的去重能力,因为事务协调器和参与者之间可能直接用RPC通信,去重要做在自己手里。
6.3 不要把2PC的锁范围设计为本性过大
最后一条经验是针对强一致场景的:2PC期间,所有参与者的资源处于锁住状态,一旦事务规模变大,锁的持有时间会对线上QPS造成致命压制。优化手段包括把长事务拆分短事务;细化锁粒度,用行锁代替表锁;引入批量提交减少阶段轮次。最理想的目标是让一个事务的整体锁持有时间控制在几十毫秒内,否则体验会直线下降。
7 最后一点实际体会
我自己经历过从"稀里糊涂用事务"到"分层看待一致性"的过程,最大的转变是:不再试图让一套协议解决所有问题。事务和共识各自主宰自己的领域——一个管住"多个资源的同生共死",一个管住"一份日志的众口一词"。真正值得投入的,是想清楚你们系统的瓶颈在哪个层面,再决定去做事务改造还是去学共识机制,别上来就把两者搅在一起。
如果这篇内容能让你在遇到相关术语时至少多停顿三秒,区分一下当前讨论的对象到底是"跨资源的原子操作"还是"多副本的日志排序",这篇文章的价值就已经兑现了。