mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1277 字
4 分钟
逻辑时钟(Logical Clock)
2026-06-13

一、为什么需要逻辑时钟#

分布式系统里有一个看似简单实则致命的问题:两台机器上的事件,谁先发生? 你可能会说,看时间戳就行了。但物理时钟不可靠——会漂移(每台机器的时钟走得快慢不一),NTP 同步时会跳变,不同机器间可能有几十毫秒甚至几百毫秒的偏差。

考虑这个场景:机器 A 在 10:00<00>.000 发送请求,机器 B 的时钟快了 50ms,在 09:59<59>.950 收到请求。按时间戳排序,B 的接收事件反而「早于」A 的发送事件——因果顺序被颠倒了。

逻辑时钟不依赖物理时间,而是用一个单调递增的计数器为事件排序。Lamport 时钟的规则很简单:本地事件时递增,收到消息时取 max(本地, 远端) + 1。这保证了:如果事件 A 因果上先于事件 B,那么 clock(A) < clock(B)。不需要同步时钟,不需要网络往返,只需要在消息里带上计数器值。

二、现实类比#

在一个所有人都在不同时区的群聊中给消息编号。不用挂钟时间(各地不同),而是给每条消息打一个序号,保证「我看到你的消息后才发的我的」——因果顺序,而非时钟顺序。

三、核心思想#

Lamport 时钟的核心规则只有三条:

  1. 本地事件:计数器 +1
  2. 发送消息:计数器 +1,将当前值附带在消息中
  3. 接收消息:clock = max(本地时钟, 消息中的时钟) + 1
sequenceDiagram participant P1 as 进程 P1 participant P2 as 进程 P2 P1->>P1: tick → 1 P1->>P1: tick → 2 P1->>P2: send(2) P2->>P2: receive: max(0, 2)+1 = 3 P2->>P2: tick → 4 P2->>P1: send(4) P1->>P1: receive: max(2, 4)+1 = 5 P1->>P1: tick → 6

因果序:P1<1> → P1<2> → P2<3> → P2<4> → P1<5> → P1<6>

属性
递增O(1)——counter++
接收O(1)——max + 1
保证若 A → B(因果),则 clock(A) < clock(B)
局限反之不成立:clock(A) < clock(B) 不意味着 A → B

四、变体与对比#

模式关系区别
向量时钟向量时钟是 Lamport 时钟的增强版Lamport 时钟只能排序;向量时钟还能检测并发事件
混合逻辑时钟(HLC)HLC 结合物理时钟和逻辑时钟HLC 提供因果排序 + 实时近似;Lamport 只有排序
物理时钟逻辑时钟的替代方案物理时钟提供真实时间但不可靠;逻辑时钟提供因果序但数字无时间含义
MVCCMVCC 使用逻辑时间戳作为版本标识逻辑时钟是排序工具;MVCC 是并发控制方案
Warning

clock(A) < clock(B) 不意味着 A 因果先于 B。两个从未通信的并发事件可以有任意的时钟值大小关系。要判断两个事件是否并发,需要向量时钟——每个节点一个计数器,按分量比较。

五、多语言实现#

Go:Lamport 时钟#

type LamportClock struct {
mu sync.Mutex
time uint64
}
func (c *LamportClock) Tick() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
c.time++
return c.time
}
func (c *LamportClock) Send() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
c.time++
return c.time
}
func (c *LamportClock) Receive(remote uint64) uint64 {
c.mu.Lock()
defer c.mu.Unlock()
if remote > c.time {
c.time = remote
}
c.time++
return c.time
}
func (c *LamportClock) Now() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.time
}

TypeScript:Lamport 时钟#

class LamportClock {
private time = 0;
/** 本地事件:递增时钟 */
tick(): void {
this.time++;
}
/** 发送事件:递增并返回时间戳 */
send(): number {
this.time++;
return this.time;
}
/** 接收事件:取 max 后递增 */
receive(remoteTimestamp: number): void {
this.time = Math.max(this.time, remoteTimestamp) + 1;
}
/** 当前时钟值 */
now(): number {
return this.time;
}
}
// 使用示例:两个进程通信
const p1 = new LamportClock();
const p2 = new LamportClock();
p1.tick(); // p1: 1
p1.tick(); // p1: 2
const msg = p1.send(); // p1: 3, 消息携带时间戳 3
p2.receive(msg); // p2: max(0, 3)+1 = 4

六、生产验证#

  • etcdkvstore.go#L53-L72store 结构体包含 currentRev int64——单调递增的修订计数器,每次写事务递增。Watch 和快照使用此修订号实现一致性读——「给我修订号 42 之后的所有变更」
  • LevelDBdbformat.h#L62-L66SequenceNumber 是每次写操作递增的 uint64_t,用于 WAL 中的写入排序、快照可见性判断和压缩时的键冲突解决
  • CockroachDB — 混合逻辑时钟(HLC),结合物理时钟 + 逻辑计数器实现可序列化事务,提供因果排序和实时边界

七、小结#

何时使用:

  • 数据库修订追踪——etcd、CockroachDB 用单调修订号实现一致性快照和 watch API
  • 缓存失效——基于 epoch 的失效:「如果你缓存的 epoch < 当前 epoch,你的数据已过期」
  • 分布式事件排序——在没有同步时钟的节点间排序消息
  • MVCC——每个事务获得一个序列号,读者看到某个时间点的一致快照
  • 乐观并发——「仅当版本匹配时更新这行」(逻辑时间戳的 compare-and-swap)

何时不用:

  • 需要物理时间——如果需要「这发生在下午 2<30>」这样的时间戳,用混合逻辑时钟(HLC)或 TrueTime
  • 检测并发事件——Lamport 时钟无法判断两个事件是并发还是因果相关,需要向量时钟
  • 单进程顺序代码——简单计数器或数组索引就够了,Lamport 机制只增加复杂性
  • 单写者事件存储——自增序列号比 Lamport 时钟更简单有效

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

逻辑时钟(Logical Clock)
https://blog.souloss.com/posts/programming/concurrency/concurrency-logical-clock/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时