2. 数据一致性模型:强一致性、最终一致性、因果一致性、读己之写一致性
聊副本,绕不开一致性模型。说白了,就是「你写完了,别人多久能看到」的问题。
我在项目中见过太多人一上来就选「强一致性」,结果系统卡成狗。也见过有人拍脑袋用「最终一致性」,结果用户投诉数据丢了。嗯,选模型之前,得先搞清楚它们到底承诺了什么。
2.1 强一致性(Linearizability / Strict Consistency)
强一致性是最直观的模型。你写一个值,后续任何读操作,只要时间上发生在写之后,就一定读到新值。看起来就像只有一份数据。
核心承诺:所有节点看到的写入顺序完全一致,且读操作永远返回最近一次写入的值。
我早期做一个金融交易系统,账务模块用的就是强一致性。为什么?因为用户转账 100 块,如果 A 节点看到余额少了 100,B 节点还显示原余额,那就要出大问题。
但代价也很明显:
- 延迟高:每次写入必须等所有副本确认,网络抖动一次,写入就卡住。
- 可用性低:副本节点挂了,写入可能失败,甚至整个集群不可用。
- 吞吐量受限:CAP 定理里,选了 C 和 P,就得牺牲 A。
你想想看,如果每个请求都要等全球所有数据中心同步完才返回,用户体验能好吗?
避坑指南:我曾经接手过一个项目,团队为了「正确」给所有接口都上了强一致性。结果线上压测时,写入 QPS 刚过 200 就超时。后来发现,大部分业务场景根本不需要这么严格。别把强一致性当默认选项。
2.2 最终一致性(Eventual Consistency)
最终一致性是另一个极端。它只保证:如果没有新的写入,所有副本最终会收敛到同一个值。至于「最终」是 1 毫秒还是 1 小时,模型本身不承诺。
DNS 系统就是个典型例子。你更新了一条域名解析记录,全球 DNS 缓存可能要几分钟甚至几小时才全部刷新。但最终,大家都会看到新记录。
我做过一个社交动态流系统,用户发一条动态,不需要立刻让所有人看到。延迟几秒甚至几十秒,用户根本感知不到。这种场景用最终一致性,性价比极高。
| 特性 | 强一致性 | 最终一致性 |
|---|---|---|
| 写入延迟 | 高(需全副本确认) | 低(主节点写入即可) |
| 读旧数据概率 | 几乎为零 | 存在时间窗口 |
| 适用场景 | 金融、库存、锁服务 | 社交动态、CDN、日志 |
| 实现复杂度 | 中等(Paxos/Raft) | 低(异步复制) |
个人经验:选最终一致性时,一定要想清楚「不一致窗口」的上限。我习惯在 SLA 里明确写出来,比如「99.9% 的读操作在 5 秒内返回最新数据」。这样团队和业务方都有个预期。
2.3 因果一致性(Causal Consistency)
因果一致性比最终一致性强,但比强一致性弱。它只保证有因果关系的操作,顺序一致。没有因果关系的操作,可以乱序。
举个例子:
- A 发了一条动态:「我感冒了」。
- B 评论:「多喝热水」。
这两条消息有因果关系——必须先看到动态,才能看到评论。因果一致性保证所有用户看到的顺序都是「动态 → 评论」。
但如果两条动态没有关系,比如 A 发了「今天天气不错」,C 发了「午饭吃啥」,不同用户看到这两条的顺序可以不一样。
我在做协作编辑系统时用过因果一致性。两个人同时编辑文档的不同段落,他们的操作没有因果关系,可以并发处理。但如果一个人先插入文字,另一个人修改了那段文字,那顺序就必须保持。
关键理解:因果一致性不是「最终一致性的升级版」,而是「在性能和正确性之间找平衡」。它保留了人类直觉里的「先后关系」,又允许无关操作并行。
2.4 读己之写一致性(Read-Your-Writes Consistency)
这个模型更轻量。它只保证:你写完之后,你自己能立刻读到。至于别人能不能读到,它不管。
你想想看,用户在论坛发了个帖子,刷新页面看不到自己的帖子,是不是会骂「系统吞了我的帖子」?读己之写一致性就是为了解决这种体验问题。
实现方式也很简单:
- 给每个客户端绑定一个「写后读」标记。
- 客户端写操作完成后,本地记录一个时间戳或版本号。
- 后续读请求带上这个标记,路由到已经同步了该写入的副本。
// 伪代码示例:读己之写实现
class ReadYourWritesStore {
private Map<String, Long> clientWriteTimestamps = new ConcurrentHashMap<>();
public void write(String key, String value, String clientId) {
long timestamp = System.currentTimeMillis();
// 写入所有副本,但至少保证主副本写入成功
primaryReplica.write(key, value, timestamp);
// 记录客户端的最新写入时间
clientWriteTimestamps.put(clientId, timestamp);
}
public String read(String key, String clientId) {
Long lastWrite = clientWriteTimestamps.get(clientId);
if (lastWrite == null) {
// 没有写过,随便读一个副本
return anyReplica.read(key);
}
// 必须读时间戳 >= lastWrite 的副本
return replicaWithTimestampAtLeast(lastWrite).read(key);
}
}
我建议:读己之写一致性是性价比最高的模型之一。实现成本低,但能解决 80% 的用户体验问题。如果你的系统不需要全局强一致,优先考虑这个。
2.5 四种模型对比与选型思路
为了让你看得更清楚,我画了一张对比图:
选型时,我习惯问自己三个问题:
- 用户能不能接受「看到旧数据」? 能 → 最终一致性;不能 → 继续往下问。
- 是不是只有「自己写自己读」的场景? 是 → 读己之写一致性;不是 → 继续往下问。
- 操作之间有没有明确的因果关系? 有 → 因果一致性;没有且必须全局一致 → 强一致性。
我曾经踩过的坑:有个项目选了最终一致性,但没考虑「读修复」机制。结果某个副本挂了三天,恢复后数据一直对不上。最终一致性不是「什么都不管」,你得有冲突检测和修复策略。不然「最终」可能变成「永远」。
记住,没有完美的模型。每个模型都是在一致性、可用性、性能之间做取舍。理解业务场景,比理解模型本身更重要。