23.3 Lamport 逻辑时钟
一句话
Lamport 时钟给每个事件一个 “只增不减的整数”,保证 “有因果则一定有序”;但它不能反过来用——时间戳小不代表因果在前,时间戳相等也不代表并发(本机算例:并发的 a 与 e 都是 1)。
更新规则(两条)
1. 本地事件:clock = clock + 1
2. 收到消息:clock = max(本地 clock, 消息里的 clock) + 1
这两条保证:若 a → b(因果),则 clock(a) < clock(b)。注意是 “若…则”,不是 “当且仅当”。
实测:Lamport 能判断什么、不能判断什么
事件:p1 有 a、b;b 发消息给 p2 产生 c;p2 再有 d;p3 有 e(与 p1/p2 无消息往来):
| 事件 | Lamport 值 |
|---|---|
| a | 1 |
| b | 2 |
| c | 3(接收 b) |
| d | 4 |
| e | 1 |
| 判断 | Lamport 结论 | 是否正确 |
|---|---|---|
| b → c(因果) | 2 < 3,能判断 | ✅ |
| a 与 e 是否并发 | 都是 1,无法区分 | ❌(实际并发) |
| d 与 e 的先后 | 4 vs 1,看起来 e 更早 | ❌(实际无因果关系) |
结论:Lamport 只保证 “因果不乱”,不保证 “无因果的可比”。把它当 “全局时间” 用,就会把并发当成先后。
代码:Lamport 时钟
type Clock struct{ v uint64 }
func (c *Clock) Tick() uint64 { c.v++; return c.v } // 本地事件
func (c *Clock) Recv(remote uint64) uint64 { // 收到消息
if remote > c.v { c.v = remote }
c.v++
return c.v
}
三个用途(都是 “定序”,不是 “计时”)
| 用途 | 说明 |
|---|---|
| 全序广播 | 按 Lamport 值给消息排序,得到 “所有人一致的顺序” |
| 互斥 | Ricart-Agrawala 用 Lamport 值决定谁先进入(见 20.3 分布式互斥 (Ricart-Agrawala / 全票通过)) |
| 日志排序 | 把多机日志按逻辑时钟排成 “可解释的顺序” |
反模式
| 反模式 | 后果 |
|---|---|
| 用 Lamport 值判断 “谁先发生” | 把并发当先后,因果判断出错 |
| 用 Lamport 值做 “数据版本” | 无法检测冲突(并发写会有相同/不可比的值) |
| 忘记 “接收时取 max + 1” | 跨进程因果丢失 |
| 把 Lamport 当物理时间展示 | 用户看到 “时间倒流” |
| 单机多线程共享一个 clock 不加锁 | 计数错乱,因果断裂 |
| 用 Lamport 做 “去重键” | 不同事件可能同值,去重误杀 |
三条铁律
🔑 Lamport 保因果,不保并发可辨。
🔑 接收消息必须取 max + 1,否则跨进程因果丢失。
🔑 要检测冲突就用向量时钟(见 23.4 向量时钟)。
自检
Q1 为什么 Lamport 值小不代表 “发生得更早”?
▸ 答案:因为 Lamport 只维护 “因果单调”:两个无因果关系的事件(并发)可以有任意大小的值。本例中并发的 a 与 e 都是 1,d 是 4 但与 e 无因果关系——用值比较会得出错误结论。
Q2 为什么 “接收消息要取 max + 1”?
▸ 答案:因为接收事件因果依赖于发送事件:必须保证 clock(接收) > clock(发送)。取 max(本地, 消息) + 1 同时满足 “比本地之前的事件大” 与 “比发送事件大”,从而把跨进程的因果链串起来。
Q3 Lamport 时钟能用来检测 “并发写冲突” 吗?
▸ 答案:不能。并发写的 Lamport 值可能相等或不可比,无法判断 “是否冲突”。检测冲突需要向量时钟(比较各分量)或版本号 + 读时校验。Lamport 只适合 “定一个所有人一致的顺序”。
Q4 全序广播为什么用 Lamport 而不是物理时钟?
▸ 答案:因为物理时钟有漂移与回拨,不同节点可能给出不一致的顺序;Lamport 是逻辑的,只要按规则更新,所有节点对同一对事件的相对顺序判断一致(在因果范围内),适合做 “全序”。
Q5 多线程/多协程共享 Lamport 时钟要注意什么?
▸ 答案:要原子更新:Tick 与 Recv 都是 “读-改-写”,并发下会丢更新,导致两个事件拿到同一个值,因果断裂。用原子操作或加锁;更常见的是 “每个线程一个时钟 + 合并时取 max”。
Q6 Lamport 与 “版本号” 的关系?
▸ 答案:单键的单调版本号本质是 “单副本的 Lamport 时钟”;多副本下单个版本号不够(无法表达并发),要升级成向量时钟或 HLC。版本号能表达 “同一副本内的先后”,不能表达 “跨副本的因果”。
Q7 什么时候 “Lamport 就够用”?
▸ 答案:当需求只是 “给事件一个一致的顺序”(日志排序、全序广播、互斥排队),而不需要 “检测并发/冲突” 时。一旦要判断 “两个写是否冲突”,就必须换成向量时钟或带因果元数据的方案。
关联
- 前置:23.1 物理时钟与漂移、23.6 happened-before 关系
- 并列:23.4 向量时钟、23.5 混合逻辑时钟 (HLC)
- 下游:20.3 分布式互斥 (Ricart-Agrawala / 全票通过)、23.2 NTP 与 TrueTime