第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 实现流程
- 赋时写入:每次
set(value)时,用当前本地时间戳(Date.now())与副本自身优先级打包。 - 增量广播:将 (value, timestamp, nodeId) 三元组以操作消息形式发送至所有副本。
- 合并接收:每个副本收到远程操作后,与本地的当前状态执行 LWW 比较合并。
- 持久化状态:本地始终保存“当前胜出”的 (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" | 1713000000123 | 10 |
| node-B | "world" | 1713000000123 | 20 |
由于时间戳相等(同一毫秒),合并规则进入优先级比较: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 监控和时钟回拨防护。本章流程示意图
图中展示两个客户端在同一毫秒写入,时间戳相等,合并逻辑依据节点优先级(20 > 10)选择客户端 B 的值 "world" 作为最终状态。