第4章 Last-Writer-Wins Register:时间戳与优先级合并

Last-Writer-Wins Register(LWW Register)是最直观的 CRDT 之一:每个副本为值关联一个时间戳,冲突时选择“最后写入”的值。时间戳通常由本地时钟(Date.now())生成,并辅以唯一优先级(如节点 ID)打破时钟相等时的平局。本章将深入原理、实现细节及生产级优化。

4.1 原理:时间戳 + 优先级合并

LWW Register 的核心决策规则为:

  • 比较两值携带的时间戳,较大的视为“最后写入”。
  • 若时间戳相等,则比较优先级(如节点 UUID / 递增版本号),优先级高的胜出。

这种设计保证了全序关系:任意两个写入都能唯一确定最终值,从而满足 CRDT 的收敛性。

关键概念:时间戳不必是物理时间,只需满足单调递增。使用 Date.now() 是常见入门方式,但生产环境需警惕时钟偏移(见 §4.6)。

4.2 实现流程

  1. 赋时写入:每次 set(value) 时,用当前本地时间戳(Date.now())与副本自身优先级打包。
  2. 增量广播:将 (value, timestamp, nodeId) 三元组以操作消息形式发送至所有副本。
  3. 合并接收:每个副本收到远程操作后,与本地的当前状态执行 LWW 比较合并。
  4. 持久化状态:本地始终保存“当前胜出”的 (value, timestamp, nodeId) 三元组。

4.3 代码实现(JavaScript)

class LWWRegister {
  constructor(nodeId) {
    this.nodeId = nodeId;
    this.value = null;
    this.timestamp = 0;          // 使用 0 表示未初始化
    this.lastWriter = nodeId;    // 初始拥有者
  }

  // 写入当前值
  set(newValue) {
    const now = Date.now();
    this.value = newValue;
    this.timestamp = now;
    this.lastWriter = this.nodeId;
    return { value: newValue, timestamp: now, nodeId: this.nodeId };
  }

  // 合并远程操作
  merge(remote) {
    if (remote.timestamp > this.timestamp) {
      this.value = remote.value;
      this.timestamp = remote.timestamp;
      this.lastWriter = remote.nodeId;
    } else if (remote.timestamp === this.timestamp) {
      // 时间戳相同,按节点 ID 字符串字典序(或数值)比较
      if (remote.nodeId > this.lastWriter) {
        this.value = remote.value;
        this.lastWriter = remote.nodeId;
      }
    }
  }

  // 读取当前值
  getValue() {
    return this.value;
  }
}

4.4 冲突解决示例

假设两个节点 node-A(优先级 10)和 node-B(优先级 20)同时写入一个键:

节点写入值时间戳 (Date.now())优先级 (nodeId)
node-A"hello"171300000012310
node-B"world"171300000012320

由于时间戳相等(同一毫秒),合并规则进入优先级比较:20 > 10,所以最终值为 "world",拥有者为 node-B。

若时间戳不同,例如 node-A 的 1713000000123 而 node-B 的 1713000000124,则后者直接胜出,无需比较优先级。

4.5 调试与验证

  • 单元测试:构造时间戳相等/不等、优先级高低组合,验证 merge() 输出符合预期。
  • 并发模拟:使用 Promise.all 模拟多个节点同时执行 set(),然后分别合并,检查所有副本最终值一致。
  • 日志断点:在 merge 入口打印两方三元组,便于跟踪判定路径。
调试技巧:在时间戳完全相同的情况下,故意交换节点 ID 大小,确认合并结果翻转,即可验证优先级分支逻辑正确。

4.6 常见错误

  • 时钟漂移:物理时钟不同步导致“未来时间戳”使早期写入永久被覆盖。解决方案:改用混合逻辑时钟(HLC)或向量时钟。
  • 时间戳非单调:操作系统时间重置(如 NTP 回拨)可能生成小于上次的时间戳。应在 set() 时 Math.max(Date.now(), lastTimestamp + 1) 强制单调。
  • 优先级冲突解决不当:比较优先级时应基于全局唯一的、可排序的标识(如 UUID + 自增序列),避免仅依赖不确定的 Math.random()。

4.7 性能与稳定性优化

  • 批量合并:当收到大量操作时,先按时间戳排序再依次合并,减少重复比较。
  • 状态压缩:LWW Register 状态只有三元组,可放心缓存;无需像 GCounter 那样保留全量历史。
  • HLC 集成:将 Date.now() 替换为混合逻辑时钟,兼顾物理时钟的近似性和逻辑时钟的单调性,是生产标配。
稳定性警告:在分布式系统中,不应依赖 Date.now() 的精确性做全局排序。若必须使用物理时钟,建议结合 NTP 监控和时钟回拨防护。

本章流程示意图

LWW Register 写入冲突合并流程 客户端A (prio=10) 客户端B (prio=20) 合并逻辑 最终值 "world" ts=1713000000123 ts=1713000000123 时间戳相等 → 节点ID 20 > 10 → B胜

图中展示两个客户端在同一毫秒写入,时间戳相等,合并逻辑依据节点优先级(20 > 10)选择客户端 B 的值 "world" 作为最终状态。