时间:逻辑时钟与向量时钟
单机上,代码执行顺序就是时间顺序。x = 1 在 y = x + 1 之前,结果不可能出错。这是铁律。
到了分布式系统,铁律就碎了。几台机器各自有自己的时钟,NTP 同步只能到毫秒级。更重要的是,消息在网络中的到达顺序不固定——你没办法靠「看时间戳」判断两个操作谁先谁后。这是时间维度的核心问题:先发生,到底是什么?
为什么时间成了问题
单机上时间天然有序,因为只有一个执行线程(或者多线程共享一把锁)。但分布式系统里没有全局时钟,也没有共享内存。两台机器上各发生一件事,光看本地时间戳根本判断不了先后。
现实系统不是没有时间,是不敢信任时间。MySQL 主从复制里的时间戳(TIMESTAMP)只是锦上添花——真正决定顺序的是 binlog 位点加 GTID(全局事务 ID),一个单调递增的序号,不依赖墙上的钟。Kafka 的 offset 也是同样思路:每个分区里的消息编号从 0 开始依次递增,消费者靠 offset 判断先后,和系统时间无关。HDFS 的 NameNode 干脆是单点——没有多节点竞争,时间问题直接绕过去了。
这些系统的共同点:用序号或单点替代时间,回避了「谁先谁后」的难题。但碰到真正松散耦合的多节点系统——比如多个数据中心各自独立运行,节点对等、没有单点——你就没法靠序号绕了。必须面对时间本身。
逻辑时钟:只关心因果
Leslie Lamport 在 1978 年提出了一个重要思想:别盯着墙上时间,只关心事件之间的因果关系。
逻辑时钟(Lamport Timestamp)的规则很简单。每个节点维护一个整数计数器,初始为 0。节点自己发生事件时,计数器加 1。节点发消息时,把当前计数器值带上;收到消息时,把本地计数器和消息里的值取最大,再加 1。
这样得到的数字,不反映真实时间,但反映因果关系。如果事件 A 发生在事件 B 之前(A → B),那 A 的时间戳一定小于 B。反过来不成立:A < B 不代表 A 先于 B——两个并发事件(没有因果关系,各自独立发生)的时间戳也可能是一大一小,但大小没意义。
逻辑时钟能判断偏序(partial order)。A 影响了 B,时间戳能看出来。A 和 B 各自独立、互不影响——逻辑时钟认不出这是并发,只知道时间戳一大一小。
向量时钟:区分因果和并发
逻辑时钟的痛点——认不出并发——向量时钟(Vector Clock)补上了。
想法不复杂。逻辑时钟是每个节点记一个整数,向量时钟是每个节点记一个向量: [A:3, B:5, C:2],含义是「我知道 A 发生了 3 件事,B 发生了 5 件,C 发生了 2 件」。规则类似:自己做事,自己的计数器加 1;发消息带上整个向量;收消息时,逐位取最大。
比较两个向量时,规则很直观:如果每个位置都 ≤ 对方,那就是有因果关系的先后(前者在前);如果有大有小——A 的计数器大但 B 的计数器小——那就是并发冲突,两个事件独立发生,谁先谁后说不清。

这个能力在真实系统里是实打实的。Amazon 的 Dynamo 论文(2007)里,向量时钟是核心机制。每个写操作附一个向量时钟,同时写同一个 key 的不同客户端生成不同时钟,并行存为多个版本。读的时候客户端拿到多个版本,自己解决冲突或者交给应用逻辑。DynamoDB 后来把冲突推到应用层——客户端看到多版本,自己决定用哪个。
Riak 走了同样的路线,向量时钟是标准配置,冲突检测靠比较向量。Cassandra 没走这条路——Cassandra 用简单时间戳(最后写入者胜 LWW),实现简单但代价是:两个并发写,时间戳大的覆盖小的,哪怕逻辑上不该覆盖。所以 Cassandra 适合写少读多、冲突不敏感的场景,向量时钟适合冲突敏感、需要精确检测并发的场景。
时钟与一致性
逻辑时钟和向量时钟,讲的是怎么判断先后。但这个「先后」怎么对外暴露给应用,就是一致性模型的事。
如果系统对外表现得像只有一个副本——所有操作看起来瞬间完成,就是线性一致性(Linearizability)。向量时钟能支持的是因果一致性(Causal Consistency):有因果关系的操作按顺序看到,没有因果关系的并发操作顺序随意。这是两个不同的承诺,一个强一个弱,一个贵一个便宜。
逻辑时钟和向量时钟,给的是一套工具——不同系统拿这套工具,选了不同的一致性承诺。
小结
时间是分布式的第二个难题。逻辑时钟告诉你:别盯着墙上时间,只关心因果关系。向量时钟补了一句:因果关系能判,但并发就是并发——认不出来并发是 bug,认出来并发是数据结构设计的问题。
MySQL、HDFS、Kafka 用序号或单点绕过了时间问题,但碰到真正去中心化的系统,逻辑时钟和向量时钟就是必修课。不是每个系统都要向量时钟,但如果你的节点之间要对等、要独立运行,了解「先发生」怎么判是基本功。
寒蝉 Hancic


