很多人刚学分布式系统时,会下意识觉得:既然是同一份数据,那所有机器当然应该立刻一模一样。
这句话在单机房里还算顺耳,到了跨地域场景就开始“卡壳”了:北京机房刚写完,新加坡机房还没收到;这时如果你非要所有地方同时一致,写请求就得等最远的副本确认。网络一抖、某个地域一慢,整个系统都跟着降速,严重时还会直接影响可用性。
所以很多系统会做一个非常经典的取舍:不追求“立刻一致”,改成“最终一致”。说白了,就是先保证服务别趴、用户能读能写,再用一套补偿机制把各副本慢慢拉齐。
这不是“不要一致性”,而是把“现在就一致”换成“稍后收敛到一致”。这套思路最常见的手段,就是你提到的这五个:最终一致、异步复制、本地读、读修复、反熵同步。
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 个动作
如果你真准备在业务里采用这套模型,建议按下面顺序做:
- 先分级数据:把“必须强一致”的数据和“可短暂不一致”的数据拆开。
- 再定读取策略:普通页面走本地读,关键确认页允许强读或会话读。
- 给对象加版本信息:至少要有版本号、时间戳,必要时保留冲突元数据。
- 同时上读修复和反熵同步:别只靠其一,热点和冷点需要双保险。
- 预演故障和延迟:主动测试跨地域延迟、单地域故障、冲突写入,看业务是否还能解释得通。
最后用 5 句话记住它
- 判断你的业务能不能容忍几百毫秒到几秒的旧值。
- 选择异步复制和本地读,来换跨地域场景下更低延迟和更高可用性。
- 配套读修复处理热点数据,配套反熵同步兜住冷数据。
- 设计版本号、冲突合并和关键链路的强读策略,别把复杂度偷偷甩给用户。
- 验证故障、延迟和并发写场景,确认“高可用”不是靠运气成立。
如果要把这篇文章压缩成一句话,那就是:一致性换可用性,本质上不是放弃正确性,而是接受“短暂不一致”,再用工程手段把系统拉回正确。