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) 元数据在这里不可接受。

关联