理解一致性模型 (Consistency Model)

172 阅读4分钟

什么是一致性?

从客户端的视角来看,一个系统满足一致性意味着该系统始终呈现可预期的行为。以下是典型的一致性场景:

  • ​Read Your Writes(读己所写)​
    假设系统初始状态为 x=0,若客户端A将 x 修改为1后立即读取 x,则必须读到1或更新的值,而非旧值。实际场景中可能出现不一致:例如客户端A向主库写入后从从库读取,若主从复制存在延迟,可能无法读到最新写入。

  • ​Monotonic Reads(单调读)​
    对同一数据的多次读取不允许出现版本回退。例如同一个客户端先读到版本2再读一次却读取到版本1即违反单调读。此现象可能发生在主从库混合读取的场景。单调读不保证总能读到最新数据,但确保客户端视角不会出现"时间倒流"。

  • ​Monotonic Writes(单调写)​
    同一进程的写入必须按提交顺序被其他进程感知。例如进程先写x=1再写y=2,则所有进程看到y=2时必然能看到x=1。

  • ​PRAM(Pipeline Random Access Memory)​
    等价于 Read Your Writes + Monotonic Writes + Monotonic Reads 的组合。PRAM保证:

    • 单进程写入按顺序被其他进程感知(Monotonic Writes)
    • 单进程能读到自身先前写入(Read Your Writes)
    • 单进程的多次读取不会回退(Monotonic Reads)
  • ​Write Follow Reads(写跟随读)​
    若进程通过读操作r1获取到写操作w1的结果,随后执行写操作w2,则所有感知到w2的进程必须能感知w1。这种情况下,w2是因果依赖于w1的。例如:初始x=0 → 进程A写x=1 → 进程B读x=1后写y=2 → 进程C读y=2后读x。则进程B对y的写入依赖于进程A对x的写入。在 PRAM 下,进程C可能读取到x为0。但是在 Write Follow Reads 下,进程C必然会读取到x为1。

  • ​Causal Consistency(因果一致性)​
    等价于 PRAM + Write Follow Reads。什么是因果?如果在写入之前你读取了某些数据,那么写入就与读取到的那些数据有因果关系,这正是 Write Follow Reads 的含义。如果初始 x=0,进程A写入x=1,进程B写入x=2,那么这两个写入是并发的,没有因果关系。因果一致性不保证这两个写入的顺序。

  • ​Sequential Consistency(顺序一致性)​
    在因果一致性中,无因果关系的写入属于并发操作;而顺序一致性要求所有写入(包括无因果关系的)必须存在全局顺序,尽管该顺序可能与实际发生顺序不同。实际发生的顺序是指按照现实中的时间来排序。

  • ​Linearizability(线性一致性)​
    所有操作都有确定的全局顺序,且该顺序与实际发生时间一致。

实例分析

与 Java 内存模型类比

Java内存模型的行为类似于因果一致性 - Monotonic writes。若无同步操作,线程A的写入在线程B看来可能是乱序的(违反Monotonic writes)。但通过正确使用同步原语(如volatile或synchronized)建立happens-before关系,即可满足因果一致性——Java中的同步机制本质上实现了"Write Follow Reads"。

ZooKeeper 的一致性

ZooKeeper满足顺序一致性但不满足线性一致性:

  • ​满足顺序一致性​​:所有读写操作可按zxid排序,其结果等价于单线程按zxid顺序执行的结果。
  • ​不满足线性一致性​​:读操作在副本执行时可能读到滞后于主节点的zxid状态。例如客户端B写入后,客户端A可能读到写入前的值(写操作因由主节点处理,仍满足线性一致性)。

数据库 Serializable 隔离级别一定是线性一致的吗?

不一定。以PostgreSQL为例,其Serializable隔离级别通过Serializable Snapshot Isolation(SSI)实现。在这种机制下存在一个典型场景:

假设事务A是一个长时间运行的只读事务,事务B是一个读写事务。客户端先启动事务A,随后另一个客户端启动并提交事务B。此时事务A仍可基于快照继续执行并成功提交。这就产生了矛盾:

  • 从逻辑顺序看,事务A的操作发生在事务B之前
  • 但从实际时间看,事务A的完成时间晚于事务B

这种逻辑顺序与实际时间的不一致,明确违反了线性一致性的要求。


参考资料