一致性模型对比

某团队在设计分布式数据服务时,遇到了一个难题:

产品经理说:"用户下单后,立刻刷新页面,必须看到订单。"

开发团队有两种实现方案:

方案 A:同步等待订单写入所有节点后再返回 —— 强一致性,但延迟高 方案 B:异步写入,立即返回,后台同步 —— 延迟低,但用户刷新可能看不到订单

最终团队选择了方案 C:根据操作类型选择一致性级别。下单操作强一致(用户必须看到自己的订单),查询操作最终一致(可以接受稍旧的数据)。

这个决策背后,是对一致性模型的深刻理解。

【架构权衡】 一致性不是一个二元选择(强一致 vs 最终一致),而是一个连续的光谱。不同的业务场景需要不同的一致性级别。理解每种一致性模型的代价和收益,才能做出正确的架构决策。


一、问题背景

1.1 一致性模型的光谱

一致性模型从强到弱的连续体:

┌────────────────────────────────────────────────────────────────┐
│  强一致                                                              │
│  ├─ 线性一致性(Linearizability)                                   │
│  └─ 顺序一致性(Sequential)                                        │
├────────────────────────────────────────────────────────────────┤
│  弱一致                                                              │
│  ├─ 因果一致性(Causal)                                             │
│  ├─ 读己之所写(Read Your Writes)                                  │
│  ├─ 单调读(Monotonic Read)                                        │
│  ├─ 前缀一致(Prefix)                                               │
│  └─ 最终一致性(Eventual)                                           │
├────────────────────────────────────────────────────────────────┤
│  弱一致                                                              │
│  └─ 弱一致性(Weak)                                                 │
└────────────────────────────────────────────────────────────────┘

1.2 为什么需要多种一致性模型?

不同的业务场景对一致性的要求不同:

场景要求的一致性原因
银行转账线性一致性资金必须精确
社交 Feed最终一致用户可以接受看到稍旧的 Feed
库存扣减顺序一致性不能超卖
消息通知因果一致回复必须在原消息之后
页面浏览单调读不希望看到"倒退"

二、方案对比

2.1 线性一致性(Linearizability)

定义:所有操作看起来是原子性的,且按全局时钟排序。

线性一致性的时序图:

客户端A ──写:x=1──┬───────────────────────────────
客户端B ──写:x=2──┤
客户端C ──读:x=?──┴───必须读到 2(因为 A 的写先完成)

关键:读操作必须返回"最后一次写入"的值
时间约束:操作在返回值之前必须完成

特点

  • 最强的一致性保证
  • 实现代价最高(需要全局时钟或共识协议)
  • 延迟较高

代表系统:ZooKeeper(读操作)、etcd、分布式锁

2.2 顺序一致性(Sequential)

定义:所有进程看到的操作顺序一致,但不需要按全局时钟排序。

顺序一致性的时序图:

场景:进程 P1 写入 x=1,进程 P2 写入 x=2

合法顺序:
├─ x=1 → x=2(P1 先于 P2)
└─ x=2 → x=1(P2 先于 P1)

非法顺序:
└─ 不可能:部分 P1 看到 x=2,部分 P1 看到 x=1

特点

  • 比线性一致性弱(不要求全局时钟)
  • 比因果一致性强
  • 实现相对简单

代表系统:ZooKeeper(写操作)、Flink checkpoint

2.3 因果一致性(Causal)

定义:满足因果关系的操作必须按因果顺序执行,不满足因果关系的操作可以任意顺序执行。

因果一致性的时序图:

事件:
├─ 事件1:A 发帖
├─ 事件2:B 回复 A 的帖子(A 必须在 B 之前)
├─ 事件3:C 浏览帖子(可以在任意顺序)
└─ 事件4:D 回复 A 的帖子(必须在事件1之后,但可以和事件2任意)

合法顺序:
├─ 1 → 2 → 3 → 4
├─ 1 → 3 → 2 → 4
└─ 1 → 4 → 3 → 2

非法顺序:
└─ 2 → 1(Causally impossible)

特点

  • 比顺序一致性弱
  • 比最终一致性强
  • 需要追踪因果关系(向量时钟)

代表系统:Facebook TAO、Tarantool

2.4 读己之所写一致性(Read Your Writes)

定义:一个进程的写操作总是在后续的读操作中可见。

读己之所写的时序图:

客户端A:
├─ 写 x=1 ───────────────────────────
└───读 x──────────────────────────────必须看到 1

关键:用户必须看到自己的写入

实现方式

// 方式1:Session 跟踪
public class ReadYourWritesService {
    private Map<String, Object> sessionWrites = new ConcurrentHashMap<>();

    public void write(String key, Object value) {
        distributedStorage.write(key, value);
        sessionWrites.put(key, value); // 本地记录
    }

    public Object read(String key) {
        // 先检查本地写记录
        if (sessionWrites.containsKey(key)) {
            return sessionWrites.get(key);
        }
        return distributedStorage.read(key);
    }
}

// 方式2:客户端路由到同一节点
// 所有操作路由到同一节点,利用节点本地缓存保证

2.5 单调读一致性(Monotonic Read)

定义:如果一个进程多次读取某个数据项,后续读取不能返回比之前更旧的值。

单调读的时序图:

客户端C:
├─ 第一次读:x=1
├─ 第二次读:x=1 或 x=2(不能是 x=0)
└─ 第三次读:x=2 或更高(不能倒退到 x=1)

关键:不能"时光倒流"

实现方式

// 方式1:单调路由
public class MonotonicReadService {
    private volatile int minVersion = 0;

    public Object read(String key) {
        // 读取时记录版本号
        Object result = distributedStorage.read(key);
        int version = getVersion(key);
        if (version > minVersion) {
            minVersion = version;
        }
        return result;
    }

    // 永远不读取低于 minVersion 的版本
}

// 方式2:使用向量时钟
// 读取时检查向量时钟,只返回足够新的数据

2.6 最终一致性(Eventual Consistency)

定义:如果没有新的写入,所有副本最终会达到一致。

最终一致性的时序图:

时间 →
T0: [A节点:x=1] [B节点:x=1] [C节点:x=?]    ← C 可能还是旧值
T1: [A节点:x=1] [B节点:x=1] [C节点:x=1]    ← 全部一致
T2: [A节点:x=2] [B节点:x=1] [C节点:x=1]    ← 新写入开始同步
T3: [A节点:x=2] [B节点:x=2] [C节点:x=2]    ← 再次全部一致

关键:"最终"是多长?取决于同步机制

"最终"的时间窗口

系统不一致窗口
DynamoDB< 1 秒
Cassandra< 10 秒
DNS分钟级
CDN 缓存小时级

【架构权衡】 最终一致性是 CAP 定理中 AP 系统的默认选择。关键问题是:你能接受多大的不一致窗口?不一致期间的业务影响是什么?

三、一致性模型对比总览

一致性模型保证强度实现代价延迟适用场景
线性一致性最强极高分布式锁、抢单
顺序一致性分布式队列
因果一致性中强社交互动、评论
读己之所写用户数据
单调读历史查询
前缀一致中弱日志分析
最终一致性社交 Feed、推荐
弱一致性最弱最低最低不关键数据

四、生产避坑

4.1 一致性级别选择错误

// ❌ 错误:所有操作都用最强一致性
@Service
public class BadInventoryService {
    public void deduct(String itemId, int count) {
        // 强一致写入 —— 延迟高,高并发下成为瓶颈
        // 读也强一致 —— 不必要
        Object result = storage.syncWriteAndRead(itemId, count);
    }
}

// ✅ 正确:根据操作类型选择一致性
@Service
public class GoodInventoryService {
    public void deduct(String itemId, int count) {
        // 扣减操作:顺序一致(防止超卖)
        storage.sequencedWrite(itemId, count);
    }

    public int getStock(String itemId) {
        // 查询操作:最终一致(稍旧可接受)
        return storage.eventualRead(itemId);
    }
}

4.2 混用一致性导致的问题

陷阱:读写操作一致性级别不匹配

写操作:强一致
读操作:最终一致

问题:
1. 用户 A 写入 x=1
2. 用户 A 立刻读取 x=0(读到了旧值)
3. 用户 A 以为写入失败,重试
4. 结果:写入两次

解决:
├─ 读己之所写:确保用户看到自己的写入
└─ 路由到同一节点:利用节点本地缓存

五、工程代价评估

维度强一致性弱一致性
实现复杂度
延迟
吞吐
可用性分区期间低始终高
运维成本
业务风险需要评估

六、落地 Checklist

  • 业务分析:识别各操作需要的一致性级别
  • 技术选型:选择支持该一致性级别的系统
  • 降级设计:设计从高一致性到低一致性的降级路径
  • 监控部署:监控不一致窗口长度
  • 测试验证:验证降级后的一致性行为