1.2 时间、因果关系与一致性模型
物理时间适合回答“现实世界大约几点发生”,不能单独证明两个分布式事件的因果顺序。逻辑时间表达因果或版本,一致性模型则规定操作在客户端眼中允许呈现哪些历史。
墙上时钟与单调时钟
操作系统通常提供两类时间:
- wall clock:对应日历时间,会被 NTP 校正,可能跳变;
- monotonic clock:用于同一时钟域内的持续时间测量,不应倒退;能否跨进程比较、是否计入系统挂起时间,取决于具体 API。
超时和延迟统计使用单调时钟;审计和跨系统对齐使用标明 UTC 的时间戳,并记录时钟不确定性。不同机器的单调时钟读数不能直接相减。物理时间戳可以用于 last-write-wins 冲突规则,但误差可能让真实发生较早的写入胜出,不能把它当作因果证明。
Happened-before 是偏序
Lamport 定义:
- 同一进程中,前一个事件 happened-before 后一个事件;
- 发送消息 happened-before 接收该消息;
- 关系具有传递性。
若 A 和 B 之间都无法推出 happened-before,它们是并发事件。这不表示物理上同一瞬间,而是所建模的进程顺序和消息关系中不存在这条因果链。若追踪漏了消息或系统外通信,仅凭日志缺失不能断定并发。
Lamport Clock 保留因果的必要方向
每个节点维护递增计数器:本地事件(包括发送)先加一,发送时携带该值;接收事件则设为 max(local, received) + 1。
它保证:
A happened-before B ⇒ L(A) < L(B)逆命题不成立。看到 L(A) < L(B) 不能断定 A 导致 B;并发事件也可能得到不同 Lamport 时间戳。因此 Lamport Clock 可建立与因果兼容的排序,但不能直接检测并发。
若需要全序,可再使用节点 ID 打破相同逻辑时间的并列;这个全序是协议约定,不是新的因果事实。
Vector Clock 可以识别并发
在固定参与者集合中,每个节点保存一份向量,每个参与者对应一个分量。本地事件和发送事件递增自己的分量;接收时先逐分量取本地与消息向量的最大值,再递增自己的分量。若 V(A) 的每个分量都不大于 V(B),且至少一个更小,则 A happened-before B;若两向量互不支配,则事件并发。
例如 A 的第一次发送携带 [1, 0]。B 在接收之前独立发生两次事件,向量为 [0, 2]:这两个向量互不支配,说明事件并发。B 随后收到消息,取最大值再增加自己的分量,得到 [1, 3],这时发送事件确实 happened-before 接收事件。对应的 Lamport 值可为 1、2、3;单看前两个标量仍不能识别并发。
代价是元数据随参与者增长。实际系统可能使用版本向量、dotted version vector 或按副本集合压缩。动态、大规模客户端不能直接维护无限向量。裁剪或压缩后的实现还要单独说明保留了哪些因果判断能力。
Hybrid Logical Clock 组合近似时间与因果
HLC 结合物理时间部分与逻辑计数。节点接收消息时会合并本地物理时间、本地 HLC 和消息 HLC;逻辑计数用于区分同一时间部分内的事件,并保留 happened-before 的时间戳递增性质。它不消除物理钟误差,也不自动产生线性一致性。时间戳与 wall clock 的接近程度依赖时钟误差假设,远端时钟跑到未来也会影响接收方。
TrueTime 显式返回物理时间不确定区间。Spanner 把它与事务并发控制、复制协议及 commit wait 结合,在默认的可串行化隔离下实现外部一致性,即事务串行顺序也尊重真实时间先后。单独拿到一个时间区间不会产生这个保证;等待到提交时间戳已确定过去,是协议的一部分。
一致性模型是一组允许历史
Linearizability
每个操作仿佛在调用与返回之间某个瞬间原子生效;一个操作返回后才发起的另一个操作,必须排在它之后。它适合需要单对象实时顺序的场景,但多个线性化读写不会自动组成事务。例如两位客户端都读到“用户名不存在”后分别创建用户,仍可能冲突;唯一性还需要原子条件写或相应事务约束。
Sequential consistency
存在一个包含所有操作的总序,且每个客户端的程序顺序被保留,但不要求尊重不同客户端观察到的真实时间先后。
设 x 初值为 0,客户端 A 的 write(x, 1) 已返回,客户端 B 才调用 read(x) 并读到 0。若各客户端只有这一次操作,顺序一致性可以把 B 的读排在 A 的写之前;线性一致性不允许这样解释。
Causal consistency
所有节点以相同顺序观察具有因果关系的操作;无因果关系的并发操作可以顺序不同。
Eventual consistency
当没有新更新,且更新最终能传播到相关副本时,副本最终收敛。永久分区下不能凭这个名字期待收敛。这个定义本身不说明收敛多久、冲突如何解决、能否读到自己的写。
客户端常还需要会话保证:read-your-writes、monotonic reads、monotonic writes 和 writes-follow-reads。
Quorum 公式不是线性一致性证明
在同一固定集合的 N 个副本中,成功读写分别收到 R、W 个不同副本的应答时,R + W > N 保证读写集合相交,W > N/2 保证两个写集合相交。例如 N=3、R=W=2,任意两个这样的集合至少共用一个副本。若替补节点形成的 sloppy quorum 不再取自这同一集合,就不能直接套公式。集合相交仍没有解决并发写、部分失败的写入、版本选择和读取协议,因此不能单凭它证明线性一致性。
像 Cassandra 的 ALL、QUORUM 是副本确认级别,不能仅凭级别名称宣称普通读写具有线性一致性。需要线性化条件更新时,应使用产品明确提供的共识/LWT 等机制,并核对其具体保证。
根据不变量选择保证
| 需求 | 可能需要 |
|---|---|
| 唯一用户名 | 线性化条件写或单一权威所有者 |
| 用户刚修改后立刻看到 | read-your-writes 会话保证 |
| 协同编辑中的因果操作 | 因果元数据或专门协同算法 |
| 统计与推荐 | 可接受延迟的最终一致投影 |
| 审计时间线 | 物理时间 + 因果/序列元数据 |
先写业务异常是否可接受,再选择存储和协议,不能从产品宣传的“一致性”一词反推需求。
参考资料
Kulkarni 等,Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases
Google Cloud, TrueTime and external consistency
Apache Cassandra, Guarantees
Leslie Lamport, Time, Clocks, and the Ordering of Events in a Distributed System
Viotti, Vukolić, Consistency in Non-Transactional Distributed Storage Systems