用户更换头像,页面提示保存成功;他立刻拿起手机刷新,却仍看到旧头像。几秒后再刷一次,新头像才出现。
这类现象经常被当作缓存故障或复制延迟随手处理。它也可能是一项明确的系统选择:写请求先在一个节点完成,其他副本、缓存和读模型稍后更新。系统接受一段短暂的不一致,换取更低的写入延迟、更高的吞吐量,或者在部分节点失联时继续服务。
这项选择叫最终一致性。它能让系统更灵活,也会把一部分正确性责任交给应用和业务设计。
「最终」承诺结果,不承诺时间
最终一致性的准确含义可以压成一句话:在没有新写入的前提下,所有副本经过传播和冲突处理后会收敛到一致状态。
一次典型写入大致如下:
客户端 节点 A 节点 B 节点 C
| 写入 X | | |
| ----------> | | |
| 成功 | | |
| <---------- | | |
| | --复制 X--> | |
| | --------复制 X----------> |
| | | 收敛到 X |
| | | | 收敛到 X
成功响应可以早于全部副本完成复制。此时从节点 A 读取能看到 X,从 B 或 C 读取仍可能得到旧值。
「最终」没有给出固定时限。它可能是几毫秒、几秒,也可能因为积压、故障或修复任务持续更久。要向用户或下游系统承诺“10 秒内可见”,还需要额外的服务目标、超时、重试与监控;仅靠最终一致性的定义无法得到这个保证。
还要注意前提中的“没有新写入”。同一数据不断被并发修改时,系统必须有明确的版本顺序和冲突解决规则,否则“收敛”本身就没有唯一答案。
CAP 只在网络分区时逼你选择
CAP 经常被简化成“从一致性、可用性和分区容错里任选两个”。这句话容易让人误以为设计数据库时要永久放弃其中一项。
真正关键的时刻是网络分区发生之后。节点之间已经无法可靠通信,系统必须决定如何响应:
- 保持一致:无法确认最新状态的节点拒绝或延迟请求;
- 保持可用:节点继续响应,但返回值可能落后,写入之间也可能产生冲突。
网络正常时,系统可以同时提供一致响应和可用服务。分区持续期间,两项强保证无法同时覆盖每个请求。实际系统还会按数据、区域、操作甚至单次请求选择不同策略,很少需要给整个产品永久贴上 CP 或 AP 标签。
当前产品能力也证明了这一点。DynamoDB 的表和本地二级索引默认使用最终一致读取,也允许请求强一致读取;全局二级索引和流只支持最终一致读取。DynamoDB Global Tables 现在还提供默认的多区域最终一致模式和可选的多区域强一致模式。
Cassandra 同样允许每次操作选择一致性级别。复制因子为 3 时,读写都使用 QUORUM 会让读写副本集合至少重叠一个节点;改用 ONE 或 LOCAL_ONE,通常能降低等待并提高可用性,但读到旧值的机会随之增加。
讨论一致性时,应该问“这次读写需要什么保证、失败时如何表现”,数据库名称只能提供初步线索。
你早已在使用最终一致性
最终一致性并不局限于大型数据库集群。常见系统里到处都有它:
- DNS 记录更新需要经过缓存 TTL 和层级传播;
- CDN 边缘节点会在刷新或失效完成前继续提供旧内容;
- 主库写入后,读副本可能存在复制延迟;
- CQRS 的读模型需要消费事件后才反映写模型变化;
- 搜索索引、推荐结果、点赞数和统计报表通常异步更新;
- 服务之间通过消息队列传播状态,消费者可能延迟、失败或重复处理。
判断一个页面是否受最终一致性影响,可以顺着数据读取路径问:写入成功后,下一次读取是否可能经过另一个副本、缓存、索引或异步投影?只要答案为“会”,应用就要处理短暂旧读。
四类故障最常见
最终一致性相关问题通常集中在四个模式里。
用户读不到刚写的内容
用户保存资料后被路由到读副本,副本尚未追上主库,于是页面像是“撤销”了刚才的修改。这会直接破坏用户对成功提示的信任。
汇总值落后于明细
订单已经创建,订单列表能看到新记录,总数和金额报表仍是旧值。单项数据正确,异步维护的聚合视图暂时滞后。
并发写入互相覆盖
两名用户基于同一个旧版本编辑资料。若系统只采用最后写入获胜,先提交的修改可能无声消失。依赖物理时钟的最后写入规则还会受到时钟偏差影响。
旧数据触发不可逆副作用
消费者根据已经过期的订单状态发送邮件、扣减库存或触发付款。数据稍后可以收敛,已经发送的通知和外部支付却无法自动消失。
这些现象具有可预测性。只要系统允许复制延迟、异步传播或重复消息,就应该在设计阶段为它们准备处理方式。
先定义每类数据需要的保证
一致性选择应该从业务不变量开始。可以先给每类读写回答四个问题:
- 旧数据最多能接受多久?毫秒、秒、分钟,还是完全不能接受?
- 用户是否必须立即读到自己的写入?
- 两次读取能否出现时间倒退,即第二次读到比第一次更旧的版本?
- 并发写入冲突时,可以自动合并、最后写入获胜、拒绝提交,还是必须人工决定?
很多业务不需要全局强一致,却需要比最终一致更具体的会话保证:
- Read-your-writes:同一用户能看到自己已经成功的写入;
- Monotonic reads:同一会话中的后续读取不会倒退;
- Consistent prefix:按因果顺序发生的写入不会乱序展示;
- Causal consistency:有因果关系的操作按该关系被观察到。
例如 Azure Cosmos DB 的会话一致性使用 session token 帮助 Web 应用跨节点读取自己的写入。这个中间层级常常足以修复用户体验,无需让所有用户、所有区域为全局强一致付出协调成本。
写入路径要同时处理重复、丢失与冲突
接受最终一致性后,应用至少需要覆盖下面几项。
让重试保持幂等
超时只表示客户端没有收到结果,无法证明服务端没有完成操作。客户端或消息系统重试时,同一命令可能被处理多次。
给创建、付款、扣库存等命令附带 idempotency key,并在业务状态旁记录已处理的键。同一个键再次到达时返回已有结果,避免重复产生副作用。消费者也应按消息 ID 去重,因为至少一次投递允许同一事件重复出现。
用版本检测并发冲突
为记录增加版本号、ETag 或序列号。更新时携带客户端读到的版本,并让存储层只在版本仍匹配时提交:
UPDATE profile
SET name = ..., version = 18
WHERE id = ... AND version = 17
影响行数为 0 表示数据已经被别人修改。此时可以重新读取并合并,也可以把冲突交给用户。静默覆盖通常是风险最高的默认行为。
原子保存状态和待发事件
业务数据提交成功、消息发布失败,会让下游永远不知道这次变化。先发消息再提交数据库,也可能让下游看到一项最终回滚的操作。
Transactional Outbox 把业务变化和待发事件写入同一个本地事务,由后台任务稍后发布。发布任务仍可能重复发送,因此下游必须保持幂等;它解决的是“状态已提交但事件丢失”,不会自动解决重复和乱序。
为跨服务失败设计补偿
订单、库存、付款分别拥有自己的数据库时,很难把它们放进一个普通 ACID 事务。Saga 把业务流程拆成一组本地事务,并为失败后的步骤定义补偿操作,例如释放库存或退款。
补偿也会失败,需要记录进度、允许安全重试,并为无法自动恢复的情况发出可追踪告警。退款等业务补偿还可能有费用和时限,不能简单理解为恢复数据库旧值。
用户界面要诚实表达中间状态
乐观更新可以让操作看起来即时完成:用户点击保存后,界面先显示新内容,再异步等待服务器确认。它适合失败概率低、容易撤销的操作。
界面仍需区分至少三种状态:
处理中:本地已经显示新值,服务器或下游尚未确认;已完成:关键写入达到承诺的一致性级别;失败或需重试:明确恢复旧值、提供重试,或告诉用户后续处理方式。
如果接口已经返回成功,而读模型仍可能滞后,可以在响应中返回刚写入的资源版本,让客户端暂时使用该版本;也可以把后续读取绑定到同一会话,提供 read-your-writes。仅靠刷新页面碰运气,会把复制细节直接暴露给用户。
关键不变量应留在强一致边界内
最终一致性适合动态流、搜索、推荐、缓存、分析和可重建投影。对以下约束要更谨慎:
- 账户不能凭空多扣或少记一笔账;
- 最后一件库存不能同时承诺给两位客户;
- 唯一用户名不能被并发注册两次;
- 已撤销权限不能继续放行高风险操作;
- 同一幂等键不能产生两笔付款。
常见设计是缩小强一致核心:用事务、条件写入或共识维护订单、余额、库存预留等权威状态,再异步生成列表、搜索索引、通知和统计。这样可以让展示层容忍延迟,同时保护真正不能破坏的业务规则。
没有可观测性,就不知道何时收敛
生产系统需要把“不一致窗口”变成可以测量的量。至少观察:
- 副本延迟与最旧未复制版本的年龄;
- 消息队列积压和最老消息等待时间;
- 读模型或搜索索引落后写模型的版本差;
- 冲突、条件写入失败和重复消息数量;
- 重试次数、死信数量、补偿状态与人工介入次数;
- 用户保存后立即读取却命中旧版本的比例。
还应定期执行 reconciliation(核对与修复),比较权威数据和派生状态,补发遗漏事件或重建投影。传播流程只要可能永久失败,系统就需要一条主动发现差异的路径。
最终一致性提供的是收敛方向。可接受的时间、冲突结果、失败恢复和用户体验仍由系统设计决定。下一次准备把写入异步化时,先写下业务不变量、旧数据时限和用户必须获得的会话保证,再选择数据库参数、缓存策略和消息模式。
Aide Hub 会继续分享 AI 助手、开发工具和软件工程实践。
参考
- Irina Scurtu:Eventual Consistency Explained
- Amazon DynamoDB:Read consistency
- Apache Cassandra:Dynamo architecture and tunable consistency
- MongoDB:Read Concern
- Microsoft Learn:Manage consistency levels in Azure Cosmos DB
- Azure Architecture Center:Minimize coordination
- Azure Architecture Center:Transactional Outbox with Cosmos DB
- Azure Architecture Center:Compensating Transaction pattern