跨地域高可用时,为什么常用“用一致性换可用性”?把最终一致、读修复、反熵同步讲明白

6 阅读11分钟

很多人刚学分布式系统时,会下意识觉得:既然是同一份数据,那所有机器当然应该立刻一模一样。

这句话在单机房里还算顺耳,到了跨地域场景就开始“卡壳”了:北京机房刚写完,新加坡机房还没收到;这时如果你非要所有地方同时一致,写请求就得等最远的副本确认。网络一抖、某个地域一慢,整个系统都跟着降速,严重时还会直接影响可用性。

所以很多系统会做一个非常经典的取舍:不追求“立刻一致”,改成“最终一致”。说白了,就是先保证服务别趴、用户能读能写,再用一套补偿机制把各副本慢慢拉齐。

这不是“不要一致性”,而是把“现在就一致”换成“稍后收敛到一致”。这套思路最常见的手段,就是你提到的这五个:最终一致、异步复制、本地读、读修复、反熵同步

flowchart LR
    A[用户写入最近地域] --> B[本地副本先提交]
    B --> C[先返回成功]
    B -.异步复制.-> D[其他地域副本]
    E[用户就近读取] --> F[读取本地副本]
    F --> G{是不是旧值}
    G -- 否 --> H[直接返回]
    G -- 是 --> I[触发读修复]
    I --> J[返回较新版本并修补落后副本]
    K[后台定时任务] --> L[反熵同步]
    L --> D

看完这张图,你可以先记一句话:这套模型的核心不是“复制”,而是“先营业,后对账”。

先把结论说透:什么叫“用一致性换可用性”

如果用一句最朴素的话来解释,就是:

  • 原来要求:每次写入后,所有副本立刻看见同一个结果。
  • 现在改成:写入先在局部成功,其他副本稍后追上。

最终一致的意思是:只要新写入不再持续发生,系统里的多个副本最终会收敛到同一个值。注意,“最终”不等于“下一次读就一定最新”,它保证的是会收敛,不保证立刻收敛

生活类比一下:连锁便利店总部改了矿泉水价格,上海门店已经换好价签,杭州门店可能还在路上。只要更新流程继续跑下去,所有门店最后都会统一,但在这几分钟里,门店之间可能暂时不一样。

一个迷你案例:用户把头像从 v1 改成 v2。华东机房先写成功,立刻返回“修改完成”;华南机房晚 500 毫秒收到复制。此时同一个用户如果马上从华南读取,就可能先看到旧头像 v1。这就是短暂不一致

五个手段,各自到底在干什么

1)最终一致:目标不是立刻相同,而是最后收敛

它是这套思路的总目标。

别被“最终”两个字骗了,它不是佛系等待,更不是“看缘分同步”。它的意思是:系统允许短时间内不同副本不一致,但会通过复制、修复、校对这些动作,把差异消灭掉。

生活类比:公司多个 Excel 表先各自改,晚些时候再统一汇总成最新版。

迷你案例:社交产品里的点赞数、评论数,通常不要求全球每个地域同一毫秒看到完全一样的数字,只要很快对齐即可。

2)异步复制:先把写请求放行,再把更新送往其他副本

异步复制是实现最终一致最常见的动作。写请求先在本地域完成,再把变更异步传播到其他地域或副本。

这样做的好处很直接:写延迟更低,跨地域网络抖动时系统也不至于因为“等所有副本”而卡住。

生活类比:总部先在本地系统登记一笔订单,随后再把信息发给各地分公司,而不是所有分公司都确认了才让客户离柜。

迷你案例:北京用户改了昵称,北京机房先确认成功;新加坡副本稍后收到这次变更。用户感知到的是“改名成功很快”,代价是短时间内其他地域可能还是旧昵称。

3)本地读:优先读离你最近的副本,换更低延迟和更高可用性

本地读的重点不是“读主库”,而是“就近读本地副本”。

这招非常适合跨地域系统。用户在新加坡,就优先读新加坡副本;用户在法兰克福,就优先读欧洲副本。这样网络延迟低,哪怕远端地域抖动,本地也还有机会继续服务。

生活类比:你问家门口门店有没有货,当然比先打电话给总部再层层确认要快得多。

迷你案例:全球电商的商品详情页、营销文案页、用户公开主页,通常更看重“读得快、页面能开”,而不是每个字节都强制全局同步到纳秒级。

4)读修复:读的时候顺手把热点数据修一把

读修复发生在读取路径上。系统发现参与本次读取的副本里,有的版本新、有的版本旧,就会把较新的值返回给用户,并顺手把落后的副本补上。

它很像“顾客来买东西时,店员发现价签还没换,就当场改掉”。

迷你案例:某个用户资料页是热点数据,频繁被访问。新加坡副本这次读到旧值,系统和其他副本一比,发现东京副本更新,于是返回新值,并把新加坡副本补齐。这样热点数据会越读越整齐。

这里有个重要边界:**读修复更照顾热点数据,不擅长照顾冷数据。**一个很久没人读的键,根本不会触发读修复。

5)反熵同步:后台做“全局盘账”,把冷数据也拉齐

反熵同步可以理解成后台的系统性校对。它不等用户来读,而是定时比较副本之间的数据差异,发现不一致就补齐。

“熵”可以先粗暴理解成“越放越乱、越放越不一致”,反熵就是反过来把这些偏差消掉。

生活类比:白天营业时店员会顺手纠正价签,那叫读修复;晚上闭店后统一盘点库存、核对账目,这才像反熵同步。

迷你案例:某个下线很久的商品配置几乎没人访问,读修复长期碰不到它。这时就靠后台定期扫描,把这些冷数据副本也慢慢拉回一致。

读修复和反熵同步,别再傻傻分不清

机制触发时机更擅长处理什么代价适合记忆的画面
读修复有人读到这条数据时热点数据、经常访问的数据会增加这次读取路径的成本顾客来了,店员顺手改价签
反熵同步后台定时任务冷数据、长期没人碰的数据吃磁盘、网络、后台资源闭店后统一盘点库存

看完这张表,下一步的动作很明确:热点靠读修复提速收敛,冷点靠反熵同步兜底,两个机制最好一起上。

它最适合什么场景

这一套最适合下面三类场景:

第一类:跨地域高可用

系统分布在多个地域,核心目标是“某个地域慢了、挂了,其他地域还能继续服务”。

这时候如果每次写都要求所有地域同步确认,最远网络链路就会变成整套系统的“拖后腿冠军”。而采用异步复制 + 本地读,就能明显提高跨地域场景下的可用性和响应速度。

第二类:业务能容忍短暂不一致

不是所有数据都要求“此刻必须全世界同一个值”。

比如:

  • 内容详情页
  • 点赞数、浏览数、转发数
  • 用户公开资料展示
  • 推荐流、时间线、非核心配置

这些场景更在乎“页面能打开、响应够快”,短暂看到旧值,通常不会把业务直接打爆。

第三类:读多写少,或者读性能优先

如果读取远多于写入,本地读能明显改善体验。很多系统甚至会把“关键确认链路”做得更强一致,而把“大量普通读取”放到更弱一致的副本上。

这就是常见的混合策略:不是整套系统都牺牲一致性,而是只在能承受的路径上换可用性。

场景更推荐强一致更推荐“一致性换可用性”
支付余额、库存最终扣减、交易记账
商品详情、内容展示、点赞计数
用户刚改完资料,马上回看自己的页面视情况,可加会话保证是,但最好补会话一致性
全球用户就近访问、强调低延迟

看完这张决策表,你下一步应该做的是:先把业务字段分层,别把支付余额和点赞数塞进同一把一致性尺子里量。

一个可复现的小演练:为什么会读到旧值,又是怎么被修回来的

假设你有两个地域:北京负责写入,新加坡负责本地读取。

系统策略如下:

  • 写入:北京本地成功就返回
  • 复制:异步发往新加坡
  • 读取:用户优先读新加坡本地副本
  • 修复:读到冲突时做读修复,后台再做反熵同步
sequenceDiagram
    participant U as 用户
    participant BJ as 北京副本
    participant SG as 新加坡副本
    participant JOB as 后台同步任务

    U->>BJ: 修改头像为 v2
    BJ-->>U: 写成功
    BJ-->>SG: 异步复制 v2(稍后到达)
    U->>SG: 立刻读取头像
    SG-->>U: 可能先返回旧值 v1
    U->>SG: 再次读取 / 触发校验
    SG->>BJ: 比较版本
    BJ-->>SG: v2 更新
    SG-->>U: 返回 v2,并修补本地副本
    JOB->>SG: 定期反熵扫描

这张时序图告诉你的下一步动作是:如果业务怕用户“刚改完又看到旧值”,就别只依赖最终一致,给关键回看链路补会话一致性或强读策略。

你甚至可以把它记成一段简化的运行日志:

T0      北京写入 user#42.avatar = v2,返回 200
T0+80ms 新加坡尚未收到复制
T0+100ms 用户在新加坡读取,看到旧值 v1
T0+180ms 系统比较版本,发现北京是 v2
T0+220ms 返回 v2,并修复新加坡副本
T0+5min 后台反熵任务继续扫描其他冷数据

这就是为什么很多系统会出现一种“看上去有点怪,但其实合理”的现象:**第一次看到旧值,过一会儿再读就好了。**它不是数据库在闹情绪,而是你选择了高可用优先的复制模型。

真正的代价:业务必须接住“陈旧读”和“冲突”

如果说前面讲的是甜头,这一段就是账单。

代价 1:陈旧读

你必须接受:本地读到的可能不是最新值。

如果你的业务是“看看头像、看看文章标题”,问题不大;但如果是“账户余额还能不能提现”“这个座位是不是最后一张”,陈旧读可能直接变成事故。

所以业务上要做的不是抱怨数据库,而是明确哪些页面能容忍旧值,哪些页面必须升级读取策略

代价 2:写冲突

如果多个地域都能写,或者多个客户端在接近同时修改同一条数据,就可能出现冲突版本

这时系统不能只说一句“你们自己看着办”。业务必须提前定义好冲突策略,例如:

  • 最后写入覆盖前一个值
  • 按字段合并
  • 像购物车那样做集合并集
  • 遇到关键字段冲突时转人工或补偿流程

购物车这种业务比较适合“合并”;支付流水这种业务通常不能靠“最后写入覆盖”糊过去。

代价 3:语义会变复杂

很多开发问题并不是“写不进去”,而是“用户看起来像穿越了”:刚看到新值,下一次又看到旧值;或者 A 页面显示已更新,B 页面还没跟上。

所以业务层通常还要补这些东西:

  • 版本号或更新时间
  • 幂等键
  • 会话一致性令牌
  • 前端提示“数据刷新中”或“结果可能延迟同步”

别小看这些提示,它们常常比一百句架构大道理更能减少客服工单。

落地时最实用的 5 个动作

如果你真准备在业务里采用这套模型,建议按下面顺序做:

  1. 先分级数据:把“必须强一致”的数据和“可短暂不一致”的数据拆开。
  2. 再定读取策略:普通页面走本地读,关键确认页允许强读或会话读。
  3. 给对象加版本信息:至少要有版本号、时间戳,必要时保留冲突元数据。
  4. 同时上读修复和反熵同步:别只靠其一,热点和冷点需要双保险。
  5. 预演故障和延迟:主动测试跨地域延迟、单地域故障、冲突写入,看业务是否还能解释得通。

最后用 5 句话记住它

  • 判断你的业务能不能容忍几百毫秒到几秒的旧值。
  • 选择异步复制和本地读,来换跨地域场景下更低延迟和更高可用性。
  • 配套读修复处理热点数据,配套反熵同步兜住冷数据。
  • 设计版本号、冲突合并和关键链路的强读策略,别把复杂度偷偷甩给用户。
  • 验证故障、延迟和并发写场景,确认“高可用”不是靠运气成立。

如果要把这篇文章压缩成一句话,那就是:一致性换可用性,本质上不是放弃正确性,而是接受“短暂不一致”,再用工程手段把系统拉回正确。