11 · 单主复制:写成功后为什么读不到
目标:用复制位置解释读己之写、单调读和故障切换时的数据损失。
“已保存”到底保存到了哪里
单主复制把写入送到 leader,再把变更传播给 follower。复制可以增加读取能力、容错和地域覆盖,但副本是否拥有某次写入,取决于确认条件。异步复制允许主节点先返回,后传播;同步确认要等待指定副本,因此副本不可达可能拖慢或阻止写入。
复制日志可以记录语句、物理存储变化或逻辑行变化。语句重放要小心随机数和环境差异,物理日志与存储实现耦合,逻辑日志更接近记录变化。新副本通常从一致快照开始,再接上对应位置之后的日志;随意先拷贝文件再读“当前日志”可能留下缺口。
报名已经成功,但列表从落后副本读取,就可能看不到自己的报名。读己之写要求用户至少读到自己的写入;单调读要求看过较新状态后不退回旧状态;一致前缀要求不要先看到依赖结果,再看到前因。它们约束不同,不应合并成一句模糊的“强一致”。
带上写入位置再读
保存 replication.py。这里用一个全局有序日志模拟单个复制组,先预测副本和主节点返回的数量。
log = [(1, "r1"), (2, "r2")]
leader = {"position": 2, "rows": ["r1", "r2"]}
follower = {"position": 1, "rows": ["r1"]}
def read_at_least(replica, minimum_position):
if replica["position"] < minimum_position:
return None # 不把旧结果当成满足承诺的答案
return list(replica["rows"])
token = 2
assert read_at_least(follower, token) is None
chosen = read_at_least(follower, token) or read_at_least(leader, token)
assert chosen == ["r1", "r2"]
lost_if_promoted = set(leader["rows"]) - set(follower["rows"])
print("fallback_rows:", len(chosen))
print("lost_on_stale_promotion:", sorted(lost_if_promoted))
for position, row in log:
if position > follower["position"]:
follower["rows"].append(row)
follower["position"] = position
assert read_at_least(follower, token) == ["r1", "r2"]
uv run --python 3.12 --no-project replication.py
预期输出:
fallback_rows: 2
lost_on_stale_promotion: ['r2']
写入令牌表示最低可接受复制位置。副本未追上时,可以等待、回主节点或显式失败,不能返回旧数据却声称满足读己之写。示例中 or 只是因为合法结果必定非空而简写的回退,通用实现应判断 is None,避免把空列表误判为读取失败。
引导修改:① 去掉主节点回退;② 在副本追上前检查结果为 None;③ 追上后再读得到两条。这把更低主库负载换成了等待,展示策略代价。
故障切换是另一项协议
若主节点永久损坏,直接提升位置为 1 的副本,就无法保住只存在主节点上的 r2。多个副本并不能替代“什么算提交”的定义。旧主恢复后还要被阻止继续接受写入,否则会产生双主。第 19 课讨论共识如何约束选主和已提交日志。
复制也不是备份:误删除通常会复制到所有副本。恢复到错误发生前,需要独立保留的历史和实际恢复验证。跨地域同步还会把地域间往返时间放进响应路径,必须联系第 2 课的目标重新评估。
独立练习:用户在两个设备之间切换,一个读到了位置 8,另一个连接位置 6 的副本。粘住某个副本能否保证跨设备单调读?写出一个明确方案。
参考方案
每台设备各自粘住不同副本不能保证跨设备单调读。可在共享会话上下文保存已观察到的最高位置 8;每次读取要求至少达到该位置,否则等待或回源。多分片系统通常需要分片位置集合或更强的统一协议,不能直接套一个整数。若产品只承诺单设备会话,应明确边界。
若令牌通过但数据仍旧,检查令牌是否属于同一复制组、数据是否真的应用而不只是接收。若切主后请求悬挂,检查是否还将旧位置令牌解释在新历史里。在 design.md 明确报名结果查询与普通浏览分别允许读哪个副本。