为什么要复制
复制(replication)是在通过网络连接的多台机器上保留相同数据的副本,目的有三个:
- 降低延迟:让数据在地理上更靠近用户
- 提高可用性:部分节点故障时系统仍能工作
- 读扩展:增加能处理读查询的机器数量
复制的难点不在于”复制”本身,而在于处理数据的变更。本章讨论三类复制算法:单主复制、多主复制、无主复制,几乎所有分布式数据库都采用其中一种。
单主复制
最经典的复制方式,也称主备复制(primary-backup):
- 一个副本被指定为领导者(leader / primary),所有写请求必须发给它
- 领导者把数据变更以复制日志 / 变更流的形式发给追随者(follower / read replica),追随者按相同顺序应用写入
- 读请求可以发往领导者或任意追随者(追随者对客户端只读)
PostgreSQL、MySQL、Oracle Data Guard、MongoDB、Kafka,以及 Raft 系系统(CockroachDB、TiDB、etcd)都采用此模型。
同步与异步
- 同步复制:领导者等追随者确认后才向客户端报告成功。优点是副本保证最新;缺点是同步追随者不响应时写入会阻塞
- 异步复制:领导者不等确认,写入快但追随者可能落后,故障切换时可能丢数据
- 半同步:实践中常见的折中——一个追随者同步,其余异步。保证至少两个节点持有最新数据
故障处理
- 追随者失效:追赶恢复。追随者记录已处理的日志位置,恢复后向领导者请求断档期间的变更
- 领导者失效:故障切换(failover)。风险包括:未复制的写入被丢弃、脑裂(两个节点都自认领导者,需 fencing/STONITH 防护)、超时设太短会触发不必要的切换
复制日志的三种实现
| 方式 | 原理 | 问题 |
|---|---|---|
| 基于语句 | 转发 INSERT/UPDATE/DELETE 语句 | NOW()、RAND() 等非确定性函数在各副本结果不同 |
| WAL 传输 | 直接发送预写日志(磁盘块级) | 与存储引擎耦合,主从版本必须一致,无法滚动升级 |
| 逻辑(行)日志 | 以行粒度记录增删改(如 MySQL binlog) | 与存储引擎解耦,可跨版本,可做 CDC 供外部系统消费 |
复制延迟引发的一致性问题
异步复制的追随者可能返回陈旧数据。停止写入后副本最终会赶上,这叫最终一致性——但”最终”没有上限。针对三类典型异常,有三种一致性保证:
写后读一致性(read-your-writes)
保证:用户重新加载页面时,总能看到自己刚提交的更新(不保证看到其他人的)。
实现方法:
- 用户可能修改的内容走领导者读,其余走追随者(如个人资料只本人可编辑)
- 记录最近写入时间,写后一段时间内都读领导者;或监控复制延迟,不向落后过多的追随者发查询
- 客户端记住最近写入的时间戳/LSN,要求所读副本至少已应用到该位置
- 多设备场景下还需跨设备的写后读一致性,元数据需集中保存
单调读(monotonic reads)
保证:用户一旦读到较新的数据,之后不会再读到更旧的——不会”时光倒流”。
异常场景:用户刷新页面,两次查询被路由到复制延迟不同的副本,先看到评论后又消失了。
实现:让每个用户固定读同一个副本(如按用户 ID 哈希选副本),副本失效时再切换。
一致前缀读(consistent prefix reads)
保证:一系列按顺序发生的写入,被读取时也按相同顺序出现——回答不能出现在问题之前。
这在分片数据库中尤其成问题:不同分片独立运行、无全局写入顺序,写入可能以错误顺序抵达副本。解决办法是让有因果关系的写入落在同一分片,或用算法显式跟踪因果依赖。
多主复制
单主的痛点是所有写入必须经过一个领导者,连不上就写不了。多主复制(主动/主动)允许多个节点接受写入,每个领导者同时是其他领导者的追随者。有意义的场景是异步多主——同步多主本质上等价于单主。
适用场景
- 跨地域部署:每个地区一个领导者,本地写入本地处理再异步同步。对地区停机和地区间网络问题更耐受,写入延迟更低;代价是一致性弱(无法保证余额不为负、用户名唯一等全局约束)
- 离线/本地优先应用:每台设备是一个”领导者”,日历、协作文档(Google Docs、Figma)的同步引擎(sync engine)本质就是复制延迟极大的多主复制
复制拓扑
- 全对全:容错最好,但不同链路速度不同,写入可能乱序抵达(更新先于插入到达),需版本向量排序
- 环形 / 星形:写入需经中间节点转发(用节点标识防循环);单点失效会中断消息流
写入冲突的处理
多主最大的难题:两个领导者并发写同一记录,复制时才发现冲突。四种策略:
- 冲突避免:确保同一记录的写入都经过同一领导者(如按用户归属地区路由)
- 最后写入者胜(LWW):按时间戳取最新值,简单但会丢写入(Cassandra 默认)
- 手动解决:保存所有并发值(兄弟值),交应用或用户合并(CouchDB)
- 自动解决:CRDT 等算法自动合并并发写入,保证所有节点收敛到相同状态
无主复制
彻底放弃领导者:客户端(或协调者)直接把写入并行发给多个副本,读也并行读多个副本。Dynamo 风格数据库:Riak、Cassandra、ScyllaDB(注意 DynamoDB 实为 Multi-Paxos 单主,架构完全不同)。
追赶错过的写入
- 读修复:读时发现某副本陈旧,把新值写回
- 提示移交:副本不可用时由其他副本代存写入(提示),恢复后移交
- 反熵:后台进程定期比对副本差异并补齐,不保证顺序
读写仲裁
n 个副本中,写需 w 个确认、读需查 r 个节点。只要 w + r > n,读到的 r 个节点中必有一个持最新值。常见配置 n=3/5、w=r=(n+1)/2,可容忍 (n-1)/2 个节点失效。
但仲裁不是绝对保证——并发读写、部分写入成功、时钟偏移、再平衡期间都可能读到陈旧值。w、r 只是调节陈旧概率的旋钮。
性能特点
- 优势:无需故障切换,对慢节点/灰色失效(gray failure)韧性强;请求对冲(取最快响应)可降低尾延迟
- 劣势:提示移交增加恢复负载;法定人数越大,遇到慢副本概率越高;无主结构没有固定写入顺序,复制延迟难以监控
三种方式对比
| 单主 | 多主 | 无主 | |
|---|---|---|---|
| 写入路径 | 唯一领导者 | 任一领导者 | 并行写 w 个副本 |
| 一致性 | 最强(可串行化) | 弱,需冲突解决 | 弱,需冲突解决 |
| 故障切换 | 需要,有脑裂风险 | 不需要 | 不需要 |
| 适用场景 | 通用,最易理解 | 跨地域、离线同步 | 高容错、多地区 |
| 代表系统 | PostgreSQL、MySQL、MongoDB | YugabyteDB、pglogical | Cassandra、Riak、ScyllaDB |