23.5 混合逻辑时钟 (HLC)
一句话
HLC 用一对
(物理时间, 逻辑计数)同时拿到两个好处:因果单调(像 Lamport,不因时钟回拨而乱序)与贴近真实时间(像物理时钟,便于运维与范围查询);元数据 O(1),是大规模系统里 “要时间感又要定序” 的默认选择。
三种时钟的对比
| 时钟 | 元数据 | 因果单调 | 有 “真实时间” 语义 | 能判并发 |
|---|---|---|---|---|
| 物理时钟 | O(1) | ❌(回拨即乱) | ✅ | ❌ |
| Lamport | O(1) | ✅ | ❌(纯计数) | ❌ |
| 向量时钟 | O(N) | ✅ | ❌ | ✅ |
| HLC | O(1) | ✅ | 近似(物理分量) | ❌(单值不可比并发) |
HLC 的取舍很清晰:放弃 “判并发”(那是向量时钟的活),换取 O(1) 元数据 + 时间感。
更新规则(三个动作)
本地事件 / 发送:l++;若 (pt, l) <= (last_pt, last_l) 则 pt = last_pt,否则 pt = now()
接收消息 (m.pt, m.l):
pt = max(now(), last_pt, m.pt)
if pt == last_pt == m.pt: l = max(last_l, m.l) + 1
elif pt == last_pt: l = last_l + 1
elif pt == m.pt: l = m.l + 1
else: l = 0
要点:物理分量只增不减(即使本机时钟回拨,也用 last_pt 兜住),逻辑分量在 “同一物理毫秒内” 递增。
示例:时钟回拨也不会乱序
正常: 事件 A: (t=100, l=0) 事件 B: (t=100, l=1) 事件 C: (t=101, l=0)
回拨: 本机时钟从 101 跳回 99,但 HLC 仍是 (t=101, l=1) → 仍大于 C
于是 “后发生的事件” 的 HLC 一定更大,因果不乱
| 场景 | 物理时钟 | HLC |
|---|---|---|
| 时钟回拨 2ms | 后事件时间戳变小 → 乱序 | 物理分量不回退 → 仍单调 |
| 同一毫秒内多事件 | 时间戳相同 → 无法排序 | 逻辑分量递增 → 可排序 |
| 展示给用户 | 直观 | 取物理分量即可,仍直观 |
代码:HLC 的核心
type HLC struct{ pt int64; l uint32 }
func (h *HLC) Now() (int64, uint32) {
now := time.Now().UnixMilli()
if now > h.pt { h.pt, h.l = now, 0 } else { h.l++ }
return h.pt, h.l
}
func (h *HLC) Recv(mPt int64, mL uint32) (int64, uint32) {
now := time.Now().UnixMilli()
maxPt := max3(now, h.pt, mPt)
switch {
case maxPt == h.pt && maxPt == mPt: h.l = max(h.l, mL) + 1
case maxPt == h.pt: h.l++
case maxPt == mPt: h.l = mL + 1
default: h.l = 0
}
h.pt = maxPt
return h.pt, h.l
}
反模式
| 反模式 | 后果 |
|---|---|
| 只存逻辑分量、不存物理分量 | 丢失 “时间感”,范围查询与运维排障困难 |
| 用 HLC 判 “并发写冲突” | 单值不可比,无法识别并发(要用向量/Dotted VV) |
| 允许物理分量回退 | 因果乱序,等于没用 HLC |
| 逻辑分量用 int32 且高频 | 同毫秒事件过多时溢出 |
| 跨系统比较 HLC 却不校时 | 物理分量差异大,排序 “看起来” 偏了 |
| 把 HLC 当 “严格物理时间” 展示 | 用户看到的时间可能比真实时间 “超前” 几毫秒 |
三条铁律
🔑 物理分量只增不减;回拨用
last_pt兜住。
🔑 要判并发就用向量时钟/Dotted VV;HLC 只定序。
🔑 HLC 的比较必须
(pt, l)一起比,不能只比物理分量。
自检
Q1 HLC 与 Lamport 的最大区别是什么?
▸ 答案:HLC 的计数带物理时间分量:它不只是 “谁先谁后”,还近似 “什么时候”。这让它既能保证因果单调,又能用于 “按时间范围查询/运维展示”;Lamport 只有逻辑计数,完全没有时间语义。
Q2 时钟回拨时 HLC 为什么还能保持单调?
▸ 答案:因为更新规则里 物理分量取 max:即使 now() 变小,也不会退回到比 last_pt 更小的值,而是继续用 last_pt 并递增逻辑分量。于是 “后发生的事件” 的 HLC 仍然更大。
Q3 为什么 HLC 不能判断两个写是否并发?
▸ 答案:因为它是单值(一个 (pt, l) 对),只能给出全序;并发写的 HLC 可能一大一小,但无法区分 “真的有因果” 还是 “只是时间戳不同”。判并发需要向量时钟(每个节点一个分量)。
Q4 HLC 的逻辑分量什么时候会增长?
▸ 答案:同一物理毫秒内发生多个事件时(或收到消息的物理分量相同):物理分量不变,逻辑分量 +1。它保证 “同毫秒内的事件也能排序”,避免时间戳相等。
Q5 跨机房部署时 HLC 要注意什么?
▸ 答案:物理分量来自各自时钟,机房之间可能有几毫秒偏移:HLC 仍然因果单调(不会乱序),但 “跨机房的物理分量” 不完全可比。所以 “按 HLC 做时间范围查询” 要留余量,或只用于 “排序” 而非 “精确时间点”。
Q6 HLC 与 “混合逻辑时钟” 的 “混合” 指什么?
▸ 答案:指 物理时钟 + 逻辑时钟的混合:物理部分提供 “接近真实时间”,逻辑部分提供 “因果单调”。它不是 “两种算法二选一”,而是在一个值里同时携带两种信息。
Q7 哪些系统用了 HLC?
▸ 答案:CockroachDB(HLC 做事务时间戳)、MongoDB(cluster time 类似 HLC)、一些多主/边缘同步系统。共同诉求是 “大规模 + 要时间感 + 要因果单调”,而向量时钟的 O(N) 元数据在这里不可接受。