1 分钟

分布式数据库:在一致性与可用性之间权衡

了解为什么分布式数据库在故障时常常放宽一致性以保持可用性,CAP 和仲裁如何工作,以及何时选择不同策略。

分布式数据库:在一致性与可用性之间权衡

在实践中一致性与可用性意味着什么

当数据库分布在多台机器(副本)上时,你获得了更好的速度和弹性——但同时也会出现这些机器暂时无法完全达成一致或无法可靠通信的情况。

一致性(通俗含义)

一致性的意思是:一次写成功后,所有人的读都返回相同的值。如果你更新了个人资料的邮箱,下一次读取——无论哪个副本响应——都应返回新的邮箱地址。

在实际系统中,优先保证一致性的系统可能会在故障期间延迟或拒绝某些请求,以避免返回冲突答案。

可用性(通俗含义)

可用性的意思是:系统对每个请求都有响应,即使有些服务器宕机或断开。你可能不会得到最新的数据,但会得到一个回答。

在实践中,优先保证可用性的系统可能会在副本尚未达成一致时继续接受写入和提供读取,然后在以后再进行差异修复。

这个权衡对实际应用意味着什么

“权衡”意味着你不能在每种故障场景下同时把两个目标最大化。如果副本无法协调,数据库必须要么:

  • 等待/失败一些请求以保护单一、被一致认可的真相(偏向一致性),要么
  • 在有风险返回过期或冲突数据的情况下继续响应用户(偏向可用性)。

一个简单的例子:购物车与银行转账

  • 购物车: 如果购物车数量在其他设备上短暂地多或少一件,虽然令人烦恼但通常可接受。很多团队更倾向于更高的可用性并后续 reconcile。
  • 银行转账: 如果你转了500美元而余额短时间内显示了两个不同答案,那就是严重问题。此处,偶尔“请稍后再试”的失败以换取更强的一致性通常是值得的。

没有单一的最佳选择

合适的平衡取决于你能容忍哪种错误:短时间的停机,还是短时间的错误/过期数据。大多数实际系统会在两者之间选一个点,并把权衡明确化。

为什么分布改变了规则

当数据在多台机器(节点)上存储和服务时,这就是“分布式”数据库。对应用来说,它仍可能看起来像一个数据库——但在底层,请求可能由不同地点的不同节点处理。

复制:团队添加节点的原因

大多数分布式数据库会复制数据:同一条记录存放在多台节点上。团队这样做是为了:

  • 在机器宕机时保持服务运行,
  • 通过从附近节点服务来降低延迟,
  • 在更多硬件上扩展读取(有时也扩展写入)。

复制很强大,但它会立即提出一个问题:如果两台节点都有相同数据的副本,如何保证它们总是保持一致?

部分故障是常态,而非例外

在单台服务器上,“宕机”通常很明显:机器要么在,要么不在。在分布式系统里,故障往往是部分的:某个节点可能存活但很慢,网络链路可能丢包,某一整排机架可能失去连通而集群的其他部分仍在运行。

这很关键,因为节点无法瞬间知道另一个节点是真正宕机、暂时不可达,还是仅仅延迟。在它们等待得知情况时,仍必须决定如何处理到达的读和写请求。

当通信不可信时,保证会改变

在单机上,有一个事实来源:每次读都能看到最新的成功写。

在多节点上,“最新”依赖于协调。如果一次写在节点 A 成功但节点 B 无法联系,数据库应该:

  • 阻塞该写直到 B 确认(保护一致性),还是
  • 仍然接受该写(保护可用性)?

这种张力——由不完美网络现实化——就是分布改变规则的原因。

网络分区:核心问题

网络分区是指节点间的通信断裂,这些节点本应作为一个数据库一起工作。节点可能仍然运行且健康,但由于交换机故障、链路过载、错误路由、误配置的防火墙或云网络中的“吵闹邻居”,消息无法可靠交换。

为何在大规模下分区不可避免

一旦系统分布在多台机器上(通常跨机架、可用区或区域),你便无法控制它们之间的每一跳。网络会丢包、引入延迟,有时会分成“孤岛”。在小规模时这些事件罕见;在大规模时它们日常化。即使短暂中断也会影响,因为数据库需要持续协调来达成对发生事项的共识。

分区如何制造冲突的“最新”数据

分区期间,双方都在接收请求。如果用户可以在两侧写入,每一侧可能接受对方看不到的更新。

例子:节点 A 将用户地址更新为“新街”。同时节点 B 将其更新为“旧街 2 号”。每一侧都相信自己的写是最新的——因为实时比对的途径被切断。

用户可见的症状

分区不会以整齐的错误信息呈现;它会表现为令人困惑的行为:

  • 超时:数据库在等待另一节点确认读或写时超时。
  • 过期读取:刷新仍看到旧数据,因为你命中了错过更新的副本。
  • 分裂脑行为:不同用户根据他们访问到哪一侧看到不同的“真相”。

这正是当网络无法保证通信时,分布式数据库必需做出选择的压力点:优先一致性还是优先可用性。

用通俗话讲的 CAP 定理

CAP 是描述当数据库分布在多台机器上时会发生什么的简洁方式。

三个术语(通俗)

  • 一致性(C):写入后,任何后续读取都返回该写入的值。
  • 可用性(A):每个请求都能得到非错误的响应,即便部分服务器出现问题。
  • 分区容忍(P):即便网络分裂且服务器无法可靠通信,系统仍能继续运行。

关键结论

没有分区时,许多系统看起来既一致又可用。

发生分区时,必须选择优先级:

  • 选择一致性:拒绝或延迟某些请求直到服务器达成一致;
  • 选择可用性:在分裂的每一侧都接受请求,尽管答案可能暂时不一致。

一个可想象的时间线

  • 10:00 客户端将 balance = 100 写到服务器 A。
  • 10:01 网络分区:服务器 A 无法到达服务器 B。
  • 10:02 客户端从服务器 B 读取。
    • 如果你优先一致性,服务器 B 必须拒绝或等待。
    • 如果你优先可用性,服务器 B 会响应,但可能仍显示 balance = 80

常见误解

CAP 并不是叫你“永远只选两个”。它的意思是:在发生分区时,你无法同时保证 一致性可用性。在没有分区时,很多系统在大多数时间里可以接近两者兼顾——直到网络出现异常为止。

选择一致性:你获得什么、失去什么

选择一致性意味着数据库把“所有人看到同一事实”置于“始终响应”之上。实际上,这通常指向强一致性,常被描述为线性化(linearizable):一旦写被确认,任何后来的读(无论在哪)都返回该值,就像存在一个单一的、最新的副本一样。

分区期间会发生什么

当网络分裂且副本无法可靠通信时,强一致性的系统不能安全地在两侧同时接受独立更新。为了保护正确性,它通常会:

  • 阻塞请求以等待协调,或
  • 拒绝请求(返回错误/超时)如果它无法联系到所需的副本/领导者。

从用户角度看,这可能表现为停机,即便有些机器仍在运行。

你获得的好处

主要好处是推理更简单。应用代码可以像在与一台数据库交互一样工作,而不是面对可能不一致的多个副本。这能减少诸如:

  • 写入成功后读取到旧数据,
  • 根据命中的副本看到同一记录的两个不同值,
  • 由于并发冲突而丢失不变式(例如超卖库存)等“奇怪时刻”。

此外,对于审计、计费以及任何必须一次性正确的场景,模型会更清晰。

你失去的东西

一致性有真实代价:

  • 更高延迟:许多操作必须等待跨机器或跨区域的协调;
  • 故障期间更多错误:分区、慢副本或领导者问题可能转化为超时或“请稍后再试”。

如果你的产品不能容忍部分宕机期间的请求失败,强一致性即便正确,也可能显得代价昂贵。

选择可用性:你获得什么、失去什么

测试读写模式
启动 React 应用和 Go API,测试严格写入与宽松读取。

选择可用性意味着你把一个简单的承诺放在首位:系统会在大多数故障情况下做出响应。在实践中,“高可用”并不等于“永远无错误”——而是指在节点故障、复制压力或网络断裂时,大部分请求仍能得到响应。

网络分区时会发生什么

当网络分裂时,副本无法可靠通信。优先可用性的数据库通常会继续从可达的一侧服务流量:

  • 读取从本地副本返回当前数据;
  • 写入在本地接受,并在连通性恢复后排队/复制。

这让应用能继续运行,但也意味着不同副本可能在短期内接受不同的“真相”。

你获得的好处

你得到更好的正常运行时间:即便某一区域被隔离,用户仍能浏览、往购物车放商品、发表评论或记录事件。

在高压下,你还会得到更平滑的用户体验:你的应用可以用“你的更新已保存”来替代超时并在后台同步。对于很多消费类和分析类工作负载,这样的权衡是值得的。

你失去的东西

代价是数据库可能返回过期读取:用户在一台副本更新资料后,立即在另一台副本读取可能看到旧值。

你也面临写冲突:两个用户(或同一用户在两地)可能在分区两侧更新同一记录。分区恢复后,系统必须调和分叉的历史:视规则而定,可能由某一方“胜出”、按字段合并,或需要应用层逻辑介入。

优先可用的设计就是接受短暂的分歧以保证产品继续响应——然后投资于如何检测并修复这些分歧。

仲裁与投票:折中的方法

仲裁是许多复制数据库用来平衡一致性与可用性的实用“投票”技术。系统不是信任单个副本,而是询问“足够多”的副本达成一致。

(N, R, W) 的思路

你常会看到用三个数字描述仲裁:

  • N:某条数据的副本数
  • W:写入被视为成功需多少副本确认
  • R:读取时咨询多少副本

一个常见经验法则是:如果 R + W > N,那么每次读取都会与某次成功写的副本集合至少有交集,从而降低读到过期数据的概率。

直观示例

如果有 N=3 个副本:

  • 单副本策略(R=1, W=1):快速且高度可用,但很容易读到过期副本。
  • 多数投票(R=2, W=2):写需到达 2 个副本,读也咨询 2 个副本。这增加了你看到最新值的概率,因为读和写集合有重叠。

有些系统会把 W=3(所有副本)作为更强的一致性方案,但只要任一副本慢或宕机,就会导致更多写失败。

仲裁在分区期间的行为

仲裁并不能消除分区问题——它定义了“谁被允许继续前进”。如果网络分裂成 2–1,拥有 2 个副本的一侧仍能满足 R=2W=2,而孤立的单个副本则不能。这可以减少冲突更新,但也意味着部分客户端会看到错误或超时。

代价与取舍

仲裁通常带来更高的延迟(需要联系更多节点)、更高的成本(跨节点流量)和更细化的失败行为(超时看起来像不可用)。好处是一个可调的中间地带:你可以根据重要性把 RW 调整为更偏向读取新鲜或写入成功。

最终一致性与常见异常

最终一致性意味着允许副本临时不同步,只要它们最终收敛到相同的值。

一个具体类比

想象一个连锁咖啡店在更新某个点心的“售罄”标志。一家店标记为售罄,但这个更新需要几分钟才能到达其他门店。在这一窗口期,另一家店可能仍显示“有货”并卖出了最后一份。系统并没有“坏”——更新只是在赶着同步。

常见的异常行为

当数据尚在传播中,客户端可能观察到令人生疑的行为:

  • 过期读取:你从尚未收到最新写的副本读取到旧数据;
  • 读到写入缺失:你写了更新,然后立即从另一个副本(或故障切换后)读取却看不到自己的改动;
  • 更新顺序错乱:两个更新在不同副本上按不同顺序到达,短时间内产生不一致视图。

帮助副本收敛的技术

最终一致性系统通常添加后台机制以缩短不一致窗口:

  • 读修复(read repair):若读操作检测到副本不一致,就在后台更新旧副本;
  • 提示转发(hinted handoff):若某副本宕机,其他节点暂时存储对其的写提示,待其恢复时转发;
  • 反熵(anti-entropy)/定期同步:通过 Merkel 树或校验和找出并修复偏差。

最终一致性适合的场景

当可用性比绝对及时性更重要时,它适用:活动流、查看计数、推荐、缓存的用户资料、日志/遥测等非关键数据,在这里“过一会儿正确”通常是可接受的。

冲突解决:分歧写如何被调和

放心试验变更
尝试类似仲裁机制的行为,若结果不符合预期可回滚。

当数据库在多个副本上接受写入时,可能产生冲突:在临时无法同步期间,不同副本对同一项做了独立更新。

经典例子是用户在一台设备上更新收货地址,同时在另一台设备上改了电话号码。如果两个更新在临时断开时落到不同副本上,系统在副本交换数据后必须决定什么是“真实”记录。

最后写胜(LWW):简单但有风险

许多系统以最后写胜开始:时间戳最新的更新覆盖其他更新。

它吸引人的地方在于实现简单且计算快速。缺点是它会静默丢失数据:如果“最新”获胜,旧但重要的更改可能被覆盖——即便两个更新修改的是不同字段。

它还假定时钟可信。机器或客户端之间的时钟偏移会导致“错误”更新获胜。

保留历史:版本向量与相关思想

更安全的冲突处理通常需要追踪因果历史。

概念上,版本向量(及其简化变体)会为每条记录附带一小段元数据,概述“哪个副本看到了哪些更新”。当副本交换版本时,数据库能检测到某个版本是否包含另一个(表示无冲突),或它们是否发生分歧(需要解决冲突)。

一些系统使用逻辑时间戳(如 Lamport 时钟)或混合逻辑时钟,以减少对墙钟时间的依赖,同时仍提供排序提示。

合并而非覆盖

一旦检测到冲突,你有多种选择:

  • 应用层合并:由应用决定如何合并字段、提示用户或保留两个版本以供审查;
  • CRDT(无冲突复制数据类型):为某些数据结构提供自动且确定性的合并(适合计数器、集合、协作文本等)。它们通常避免“胜者通吃”的行为,同时保持高可用性。

最佳方法取决于你的数据对“正确”的定义——有时丢失一次写是可接受的,有时则是严重的业务错误。

如何为你的用例做选择

选择一致性/可用性的策略不是哲学争论,而是产品决策。先问自己:短暂出错的代价是多少?“请稍后再试”的代价又是多少?

将业务风险映射到一致性需求

某些领域在写时需要权威答案,因为“差不多正确”仍然是错误的:

  • 金钱与计费:重复扣费、透支和退款通常要求强一致性;
  • 身份与权限:登录、重置密码、访问控制和角色变更应避免分裂脑;
  • 库存与容量:若不能接受超卖(票务、限量商品),请选择一致性或设计显式预留机制。

如果临时不一致的影响较小或可逆,你通常可以偏向更可用的设计。

决定能容忍多少过期数据

许多用户体验可以接受略微过期的读取:

  • 动态流和时间线:帖子晚几秒出现通常可接受;
  • 分析与看板:批量或延迟的数字是常见且可以接受的;
  • 缓存与搜索索引:若快速且稳定,用户接受“尚未更新”。

明确“可接受过期”是多少:秒、分钟还是小时?这个时间预算将驱动你的复制和仲裁选择。

选择用户最讨厌的失败模式

当副本无法达成一致时,通常会出现三种 UX 结果之一:

  • 等待/转圈(优先正确性,可能感觉很慢);
  • 错误/重试(诚实但破坏性强);
  • 过期结果(平滑但偶尔令人惊讶)。

按功能而非全局决定最不令人反感的选项。

快速清单

当错误结果带来财务/法律风险、或有安全/不可逆动作时,偏向 C(一致性)

当用户重视响应速度、过期数据可容忍且冲突可安全修复时,偏向 A(可用性)

若不确定,则拆分系统:关键记录保持强一致性,派生视图(Feeds、缓存、分析)则优化为可用性更高。

减轻权衡痛点的设计模式

简单模拟分区
创建最小副本模拟,演示分区、陈旧读取和冲突。

你很少需要在整个系统上只选择一种“一致性设置”。许多现代分布式数据库允许你按操作选择一致性——智能的应用会利用这一点,在不掩盖权衡的情况下保持良好用户体验。

使用按操作的一致性级别

把一致性当作一个旋钮,根据用户行为调整:

  • 关键更新(支付、库存扣减、密码变更):使用更强的一致性(如仲裁/线性化写入);
  • 非关键读取(动态流、仪表盘、“最后在线”):允许更弱的读取(本地/单副本/最终一致)以换取速度和弹性。

这样避免为所有操作支付最强一致性的成本,同时保护真正重要的操作。

在同一流程中混合强与弱

常见模式是写强、读弱

  • 写入用严格级别以确保系统有权威记录;
  • 读取用较松级别,如果发现异常(缺失项、过期计数器),则用更强的读取刷新或提示“正在更新”。

有时也会反过来:快速写入(排队/最终一致)加上强读取以确认结果(“我的订单下好了吗?”)。

为重试设计:幂等性

网络不稳定时,客户端会重试。用幂等键使重试安全,确保“提交订单”被执行两次不会创建两个订单。在遇到相同键时存储并重用第一次的结果。

长流程:Saga 与补偿

对于跨服务的多步操作,使用Saga:每一步都有对应的补偿动作(退款、释放预留、取消发货)。这样即便部分流程临时不同步或失败,系统也可恢复。

用于一致性 vs 可用性的测试与可观测性

如果你看不到一致性/可用性的表现,就无法管理它。生产问题常常看起来像“随机失败”,直到你加上合适的测量与测试。

该测量什么(及其原因)

从一小组与用户影响直接相关的指标开始:

  • 延迟(p50/p95/p99):关注故障切换、领导者变更或仲裁重试时的尖峰;
  • 错误率:把“硬”错误(超时、5xx)与“软”错误(回退提供的部分结果)分开;
  • 过期读取率:返回比你目标更旧的数据的读取占比(例如晚于 2 秒);
  • 冲突率:并发写需要调和的频率(包括 LWW 覆盖)。

如能做到,按 一致性模式(仲裁 vs 本地)和 区域/可用区 打标签,以发现行为分歧的来源。

故意测试分区

不要等到真实故障。在预发布环境做混沌实验,模拟:

  • 副本之间丢包与高延迟,
  • 某一地区不可达,
  • 部分分区(只有某些节点能通信)。

验证的不仅仅是“系统是否继续运行”,还要看哪些保证得以保持:读是否保持新鲜,写是否被阻塞,客户端是否得到明确错误?

提早发现权衡问题的告警

加入以下告警:

  • 复制延迟超过你可容忍的过期窗口,
  • 仲裁失败(达不到足够副本)和重试次数上升,
  • 写冲突或调和积压上升。

最后,把你的保证写清楚:文档说明系统在正常运行与分区期间承诺什么,并教育产品与支持团队用户可能看到的情况以及如何应对。

更快原型化 CAP 选择(无需重建全部系统)

如果你在新产品中探索这些权衡,尽早验证假设很有帮助——尤其是故障模式、重试行为以及在 UI 上“过期”看起来如何。

一个实用方法是先为工作流做一个小型原型(写路径、读路径、重试/幂等性和调和任务),再决定架构。使用像 Koder.ai 这样的工具,团队可以通过聊天驱动的工作流快速搭建 Web 应用和后端,迭代数据模型和 API,并测试不同一致性策略(例如严格写 + 放松读),而无需传统构建流水线的开销。当原型行为符合预期时,可以导出源码并逐步演进为生产系统。

常见问题

为什么分布式数据库会面临一致性与可用性的权衡?

在复制的数据库中,“相同”的数据存在于多台机器上。这提升了可靠性并能降低延迟,但也带来了协调问题:节点可能变慢、无法到达或被网络分割,因此不能总是即时达成一致,导致一致性与可用性之间的权衡。

用通俗的话说,“一致性”是什么意思?

一致性意味着:在一次写操作成功之后,任何后续的读都会返回该值——无论由哪个副本提供服务。实际系统通常通过延迟或拒绝读/写请求,直到足够的副本(或领导者)确认更新来实现这一点。

用通俗的话说,“可用性”是什么意思?

可用性意味着系统在每次请求时都会返回非错误响应,即便部分节点宕机或无法通信。响应可能是过期的、部分的或基于本地状态,但系统在故障期间尽量避免阻塞用户。

什么是网络分区,为什么它如此重要?

网络分区是指原本应协同工作的节点之间出现通信中断。节点本身可能仍然健康,但消息无法可靠地跨越分隔。这迫使数据库在两种取舍中选择:

  • 阻塞或拒绝请求以维持单一真相(保证一致性),或
  • 在各自分区上响应请求,然后在网络恢复后再进行合并(保证可用性)。
在分区或副本不一致期间,用户实际会看到什么?

在分区期间,双方都可能接受它们看不到对方的更新,导致:

  • 超时(等待不可达副本),
  • 过期读取(从落后副本读到旧数据),
  • 分裂脑(split-brain)行为(不同用户看到不同的“事实”)。

这些都是副本临时无法协调时的用户可见表现。

CAP 定理真的意味着只能从三者中选两个吗?

它并不意味着“永远只能选两个”。它的含义是:当发生分区时,你不能同时保证:

  • 一致性(所有读都能看到最新被确认的写),和
  • 可用性(每个请求都有响应)。

在没有分区的情况下,许多系统在大部分时间里看起来既一致又可用——直到网络出现问题为止。

仲裁(N, R, W)如何帮助在一致性和可用性之间取得平衡?

仲裁(quorum)通过让多个副本“投票”来平衡一致性与可用性:

  • N = 副本总数
  • W = 写需要多少副本确认才算成功
  • R = 读需要咨询多少副本

常见指导是 R + W > N,这样每次读至少会与某次成功写的副本集合重叠,从而降低读取过期数据的概率。仲裁并不能消除分区,但它决定了谁可以继续前进(例如拥有多数的一侧)。

什么是最终一致性,我应期待哪些异常现象?

最终一致性允许副本在短时间内不同步,只要它们最终收敛即可。常见异常包括:

  • 过期读取
  • 读到写入缺失(写了之后立即从别的副本读不到自己的更新),
  • 更新顺序错乱

系统通常用 读修复提示转发(hinted handoff) 和定期的 反熵(anti-entropy) 校验来缩短不一致窗口。

分区恢复后,如何解决冲突写入?

当不同副本在断开期间接受了对同一项的不同写时,就会发生冲突。常见的解决策略有:

  • 最后写胜(LWW):简单但可能静默丢失数据,且依赖时钟;
  • 版本向量 / 因果元数据:检测是否真的冲突或只是有序更新;
  • 合并 / CRDT:对某些数据类型提供确定性自动合并,适合计数器、集合或协作文本等。

选择策略应基于对“正确”含义的判断:有时丢弃写是可接受的,有时是不可接受的业务错误。

我如何为我的应用选择合适的一致性/可用性策略?

基于业务风险和用户能容忍的失败模式来决定:

  • 资金、权限、库存等需要一次性正确的场景偏向强一致性;
  • 信息流、分析、缓存、日志等可以容忍短暂不一致的场景偏向可用性/最终一致性。

实用做法包括按操作设定一致性级别、用幂等键保护重试、以及用补偿事务(sagas)处理跨步工作流的回滚。通常把关键数据设计为强一致,派生视图为高可用更为稳妥。

Related posts