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)

这个模型更轻量。它只保证:你写完之后,你自己能立刻读到。至于别人能不能读到,它不管。

你想想看,用户在论坛发了个帖子,刷新页面看不到自己的帖子,是不是会骂「系统吞了我的帖子」?读己之写一致性就是为了解决这种体验问题。

实现方式也很简单:

  1. 给每个客户端绑定一个「写后读」标记。
  2. 客户端写操作完成后,本地记录一个时间戳或版本号。
  3. 后续读请求带上这个标记,路由到已经同步了该写入的副本。
// 伪代码示例:读己之写实现
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 四种模型对比与选型思路

为了让你看得更清楚,我画了一张对比图:

数据一致性模型对比 强一致性 所有读都看到最新写 因果一致性 有因果关系的操作有序 读己之写一致性 自己写的一定能读到 最终一致性 最终会收敛 约束强 约束弱 性能低 性能高 选型建议 • 金融、库存、分布式锁 → 强一致性(别犹豫) • 社交动态、评论、通知 → 最终一致性(够用) • 协作编辑、聊天消息 → 因果一致性(刚刚好) • 用户个人设置、发帖 → 读己之写一致性(性价比之王)

选型时,我习惯问自己三个问题:

  1. 用户能不能接受「看到旧数据」? 能 → 最终一致性;不能 → 继续往下问。
  2. 是不是只有「自己写自己读」的场景? 是 → 读己之写一致性;不是 → 继续往下问。
  3. 操作之间有没有明确的因果关系? 有 → 因果一致性;没有且必须全局一致 → 强一致性。

我曾经踩过的坑:有个项目选了最终一致性,但没考虑「读修复」机制。结果某个副本挂了三天,恢复后数据一直对不上。最终一致性不是「什么都不管」,你得有冲突检测和修复策略。不然「最终」可能变成「永远」。

记住,没有完美的模型。每个模型都是在一致性、可用性、性能之间做取舍。理解业务场景,比理解模型本身更重要。