单领导者复制:数据库里的“只有一个话事人”

0 阅读5分钟

在分布式数据库的复制世界里,单领导者复制(Single-Leader Replication)是最经典、最常见的一种模式。它就像团队里只有一个决策者,所有重要决定都得经过他。

什么是单领导者复制?

简单来说,单领导者复制就是:

  • 领导者(Leader,也叫 primary/master/source):唯一的写入口,所有写请求都必须发给它。
  • 追随者(Follower,也叫 read replica/secondary/hot standby):只读节点,从领导者那里同步数据,供客户端查询。

下图展示了单领导者复制的基本流程: 客户端将写请求发给领导者,领导者将数据变更同步给追随者;读请求可以发给任意副本。

sequenceDiagram
    participant Client as 客户端
    participant Leader as 领导者
    participant Follower1 as 追随者1
    participant Follower2 as 追随者2

    Client->>Leader: 写请求(例如更新数据)
    Leader->>Leader: 本地写入
    Leader->>Follower1: 复制日志(数据变更)
    Leader->>Follower2: 复制日志(数据变更)
    Note over Leader,Follower2: 追随者异步/同步确认

    Client->>Leader: 读请求
    Leader-->>Client: 返回数据
    Client->>Follower1: 读请求
    Follower1-->>Client: 返回数据(可能稍旧)

同步还是异步?这是个时间问题

复制可以是同步的,也可以是异步的,区别在于领导者如何等待追随者的确认。

同步复制(Synchronous replication):领导者发出变更后,必须等待所有追随者都回复“已收到”,才能告诉客户端“写成功了”。这就像项目经理发完邮件,必须等每个人都回复“收到”才能下班。安全是安全,但如果某个同事上厕所去了(节点故障),整个项目就得等他。

异步复制(Asynchronous replication):领导者发完邮件就直接回家睡觉了,不管对方收没收到。速度快,但风险也大——如果项目经理第二天失忆(节点永久故障),那些没收到邮件的同事就永远不知道真相了。

sequenceDiagram
    participant Client as 客户端
    participant Leader as 领导者
    participant FollowerSync as 同步追随者
    participant FollowerAsync as 异步追随者

    Note over Client,Leader: 同步复制示例(至少一个同步追随者)
    Client->>Leader: 写请求
    Leader->>Leader: 本地写入
    Leader->>FollowerSync: 复制日志
    FollowerSync-->>Leader: 确认收到
    Leader->>FollowerAsync: 复制日志(异步)
    Note right of FollowerAsync: 不等待确认
    Leader-->>Client: 返回成功(仅等待同步追随者)

    Note over Client,FollowerAsync: 最终一致性问题(从异步追随者读取)
    Client->>FollowerAsync: 立即读请求
    FollowerAsync-->>Client: 返回旧值(尚未同步)
    Note over Client,FollowerAsync: 片刻后
    Client->>FollowerAsync: 再次读请求
    FollowerAsync-->>Client: 返回新值(已同步)

数据库界有个折衷方案叫半同步(semi-synchronous):找一个最靠谱的追随者同步确认,其他人异步。这样至少保证数据在两个人手里,不会因为一个人的失忆而全剧终。

新同事入职指南

当系统需要增加新副本(replica)时,比如扩招员工或替换故障节点,怎么让新人快速跟上进度?

总不能直接复制项目经理的硬盘吧?因为他一直在改文件,复制到一半可能数据就乱套了。

标准流程如下:

flowchart LR
    A[开始设置<br>新追随者] --> B[获取<br>领导者快照]
    B --> C[将快照复制到<br>新追随者节点]
    C --> D[新追随者<br>连接到领导者]
    D --> E[请求自快照后<br>所有变更]
    E --> F[处理变更积压<br>追赶进度]
    F --> G{是否赶上}
    G -- 是 --> H[新追随者开始<br>正常接收实时变更]
    G -- 否 --> F

节点挂了怎么办?

分布式系统里,节点宕机是家常便饭。我们需要从容应对。

追随者故障:追赶恢复(Catch-up recovery)

追随者挂了重启后,只需要问项目经理:“我掉线那会儿错过了什么?”然后把错过的变更补上就行。就像看剧暂停去吃饭,回来从暂停处继续——前提是项目经理还留着那段时间的剧情日志(replication log)。如果日志被删了,那新人就得从头再来(从备份恢复)。

领导者故障:故障转移(Failover)

这才是真正的重头戏。当项目经理突然失联(网络问题、宕机、被外星人绑架),系统需要:

  1. 检测到领导者挂了——通常用超时机制,比如30秒没心跳就当死亡。
  2. 选新领导——找那个最了解业务的人(拥有最新数据的追随者)。
  3. 重新配置系统——客户端以后找新老板,旧老板回来也只能当小弟。

这个过程有多刺激?想象一下公司突然宣布原项目经理被解雇,新经理上任,所有人要重新站队……而且这一切可能在30秒内自动完成!

脑裂(Split brain) 是最可怕的——两个节点都以为自己才是真领导。这就像两个人都声称自己是项目负责人,结果团队混乱,数据被搞成一团浆糊。为了避免这种情况,有时得用STONITH(Shoot The Other Node In The Head)——物理上关掉那个误以为自己是领导的节点。这名字很暴力,但确实管用。

复制日志的实现

领导者是怎么把变更告诉追随者的?主要有三种方式。

基于语句的复制(Statement-based replication):直接把SQL语句发给追随者执行。听起来简单,但问题来了——如果语句用了NOW()RAND(),每个副本会得到不同结果!就像告诉所有人“现在时间”,大家低头看表,时间能一样吗?

预写式日志传输(Write-ahead log shipping):把数据库底层的修改日志(WAL)发过去。很底层很可靠,但问题是版本不兼容——你不能让用新版软件的领导者给旧版追随者发日志,因为磁盘格式可能变了。

逻辑日志复制(Logical log replication):介于两者之间,记录行级别的变更——“插入了一行,值分别是……”。这种方式更灵活,支持不同版本共存,还能被外部系统解析做数据分析。

写在最后

单领导者复制就像一个有明确权力结构的团队:一个决策者说了算,其他人跟着学。优点是简单清晰、一致性有保障;缺点是决策者成了单点瓶颈,一旦挂了就得经历一波“权力交接”的惊险时刻。

但在分布式系统这个“职场”里,没有完美的制度,只有合适的权衡。单领导者复制已经被无数数据库证明是行之有效的方案,包括 PostgreSQL、MySQL、MongoDB、Kafka 等。