数据库版“没大没小”:无领导复制大揭秘

0 阅读8分钟

想象一下,你们部门要团建,需要决定吃什么。如果老大说了算——他说吃火锅,那就火锅,大家无条件服从。这就是我们之前聊的单领导者复制 。但如果老大说:“咱们民主投票,每人投一票,最后看哪个选项得票超过半数就算赢。”这听起来公平,但万一有人缺席呢?还能正常决策吗?这就是今天的主角——无领导者复制 (Leaderless Replication)

为什么要把简单问题复杂化?

你可能会想:有个老大多省心啊,为啥要自讨苦吃搞什么无领导?

原因其实很朴素:

  • 老大太累了:所有写请求都得经过老大,尤其在全球部署时,跨洋请求延迟高得感人。
  • 老大挂了咋办:虽然可以选新老大,但选举期间系统可能无法写入。而无领导者模式天生就更皮实,谁挂了都不怕。

这种模式随着 NoSQL 运动火了起来,最知名的代表就是 Cassandra、Riak 等。它们的设计灵感来自亚马逊的 Dynamo 论文,所以也叫 Dynamo 风格 (Dynamo-style)

核心玩法:法定人数

在无领导者世界里,没有“老大”这个角色。客户端(或协调者)会同时向多个副本发起写入。那问题来了:要写几个才算成功?读几个才能保证拿到最新数据?这就引出了一个关键概念——法定人数 (Quorum)

你可以把它想象成一个点菜投票机制。假设我们有 3 个副本 (n = 3),每个副本上都可以接受读写。我们设定:

  • 写操作需要至少 2 个副本确认 (w = 2) 才算成功。
  • 读操作需要至少 2 个副本响应 (r = 2),并从中挑选版本最新的数据。

只要写确认数加读响应数大于副本总数 (w + r > n),理论上你读到的节点中至少有一个包含最新数据。这就是法定人数的核心思想。

sequenceDiagram
    participant ClientA
    participant Replica1
    participant Replica2
    participant Replica3
    participant ClientB

    Note over Replica3: 副本3宕机

    ClientA->>Replica1: 写请求 (X = 10)
    ClientA->>Replica2: 写请求 (X = 10)
    ClientA->>Replica3: 写请求 (X = 10) (失败)

    Replica1-->>ClientA: 确认
    Replica2-->>ClientA: 确认
    Note over ClientA: 收到2个确认,达到 w=2,写入成功

	Note over Replica3: 副本3恢复
	
	Note over Replica1: 副本1宕机
	ClientB->>Replica1: 读请求 (失败)
    ClientB->>Replica2: 读请求
    ClientB->>Replica3: 读请求

    Replica2-->>ClientB: X=10 (v1)
    Replica3-->>ClientB: X=5 (v0)
    Note over ClientB: 收到2个响应,达到 r=2,读取最新值X=10
    
    ClientB->>Replica3: 写请求 (X = 10, version = 1)
    Replica3-->>ClientB: 确认
    Note over ClientB: 读修复

节点掉队了?它自己会“补课”

在单领导者模式里,如果追随者挂了,它回来后会去找老大要日志来追赶。如果老大挂了,就要搞故障转移。在无领导者模式里,事情更简单——节点掉队了也没关系,系统有几种办法让它“补课”:

  1. 读修复 (Read Repair):当客户端读取时,如果发现某个节点返回的数据版本较旧,它会悄悄把最新值写回那个旧节点。就像每次讨论后,总有人给迟到的同学补上刚才的八卦。这种方式对经常被读取的数据很有效。
  2. 提示移交 (Hinted Handoff):如果节点A宕机了,处理写请求的节点B会临时替A存一份数据,并在旁边贴个便签:“这是替A收的,等他回来记得给他。”当A恢复后,B会把暂存的数据移交过去。这保证了即使不常读的数据也能最终一致。
  3. 反熵 (Anti-Entropy):这是一个后台“扫地僧”进程,定期比较不同节点上的数据,默默地把不一致的地方同步过来。它不关心读写请求,只专注于让所有数据最终握手言和。

单领导者 vs 无领导者:性能擂台赛

现在我们来聊聊大家最关心的性能问题。单领导者和无领导者到底谁更快、谁更靠谱?这就像比较一个独裁政府和民主议会——各有各的套路,也各有各的坑。下面这张表可以帮你快速理清思路:

维度单领导者复制 (Single-Leader)无领导者复制 (Leaderless)
写路径所有写请求必须经过领导者,领导者再同步给追随者客户端直接写多个副本,没有固定领导者
读路径可分发到追随者,实现读扩展,但追随者可能读到旧数据客户端并行读多个副本,取最新版本,可能触发读修复
故障容忍领导者故障需故障转移,期间可能无法写入;追随者故障影响较小无故障转移,只要剩余节点满足 w 和 r 即可继续,容忍度更高
性能瓶颈领导者易成为单点瓶颈,尤其写密集场景;读扩展可缓解读压力无单点瓶颈,但需要等待足够多节点响应,可能受最慢节点影响
延迟特性领导者延迟直接影响所有写;读可从就近追随者获取,但可能旧可使用请求对冲,取最快响应,对慢节点容忍度高;但大法定人数可能增加延迟
数据一致性可通过同步复制实现强一致性;异步复制可能丢数据最终一致性强,通过版本向量、读修复等收敛,但仍可能读到旧值
典型场景需要强一致性、写少读多的场景(如传统关系型数据库)高可用、可容忍最终一致、网络不稳定的场景(如物联网、社交点赞)

单领导者就像一个事事亲为的强人领导。他负责所有写操作,然后通过复制日志把更新告诉下属(追随者)。读操作可以交给下属分担,这叫做“读扩展”。但问题来了:如果领导累了(过载),整个系统就慢了;如果领导突然倒下,大家只能停工,等新的领导选出来才能继续干活。

无领导者则更像一个互助委员会。没有固定的领导,客户端直接找多个委员商量。好处是没有单点瓶颈,写请求可以同时发给多个节点,谁快就用谁的结果。这种技巧叫请求对冲 (Request Hedging),能显著降低尾部延迟 (Tail Latency)。一个节点卡了,其他节点照样能服务。

但无领导者也有它的烦恼:为了凑够法定人数,你必须等足够多的节点响应。如果法定人数规模很大(比如 w=5, r=5),你就要等最慢的那几个节点,延迟反而可能变高。节点宕机后,提示移交和反熵过程会给幸存节点增加额外负载,可能引发连锁反应。网络分区时,如果客户端连不上足够多的节点,就没法形成法定人数,请求就失败了(有些系统支持“松散法定人数”来绕过,但一致性会降低)。

并发写冲突:两个厨师同时炒菜

如果两个客户端同时修改同一条数据,会发生什么?在无领导者系统中,不同节点接收写入的顺序可能不同,这就会导致冲突。

leaderless并发写冲突.png

在这个餐厅菜单的例子中,客户端A将某个菜的价格设为20元,客户端B同时设为30元。Replica1先收到A的更新,Replica2先收到B的更新。当它们相互同步时,Replica3可能先收到30再收到20(或者反过来),最终各节点的数据永久不一致,导致顾客看到不同的价格。

这种状态显然不能接受,所以数据库必须有一种机制来检测并解决这种冲突。

解决冲突:版本向量登场

为了理清谁先谁后,数据库引入了一个聪明的工具——版本向量 (Version Vector)。你可以把它想象成一个多栏的计数器。

每个副本都有自己的版本号。当某个副本处理写入时,它会将自己的计数器加1,同时记录它所见过的其他副本的最新版本号。所有副本的版本号组合起来,就是版本向量。

当客户端读取时,它不仅拿到数据,还拿到数据的版本向量。当客户端写回数据时,需要带上这个版本向量。服务器通过比较版本向量,就能判断因果关系:

  • 如果新写入的版本向量“大于等于”旧数据的版本向量,说明新写入是基于旧数据的最新状态,可以直接覆盖。
  • 如果两个版本向量“各执一词”(技术上叫并发 (Concurrent)),说明存在冲突,需要合并。合并的方式可以是应用自定义逻辑,或者使用更高级的数据结构,比如无冲突复制数据类型 (CRDTs)

总结一下

无领导者复制就像一场自由的圆桌会议,没有主持人,大家地位平等。它的优点是高可用、低延迟、能容忍网络分区和节点故障。缺点是引入了数据一致性的复杂性,需要借助版本向量、读修复、CRDTs等机制来保证最终收敛。

单领导者复制则像一个高效但脆弱的独裁政府,一致性极强,但领导者一倒就全盘停摆。

选择哪种复制模式,最终取决于你的业务是想要“绝对一致但偶尔掉线”的强控制模式,还是想要“始终在线但数据可能短暂混乱”的自治模式。这就是架构师们需要权衡的艺术了。