CRDT宝典 - LWWRegister

133 阅读2分钟

宝典目录

背景

在分布式系统中,有一个值、以及一个时间戳,每个节点都能更新这个值,同时更新这个时间戳。

请设计一个CRDT(Conflict-free Replicated Data Type)来实现一个满足最终一致性的分布式系统。

为了减少要理解的概念,下文描述的CRDT同时有两层意思

  1. 无冲突的数据类型,即类型
  2. 一个CRDT实例,即实例

思维链

flowchart 
      A["1. CRDT可以应用在不同的节点中"]
      A --> B["2. CRDT可以表示该值的最终值、以及时间戳的最终值"]
      B --> C["3. CRDT可以让任意节点更新这个值和时间戳"]
      C --> D["4. 可以合并多个节点的CRDT,且无论合并的顺序是怎么样的,其结果都一致,\n即Merge(A.CRDT, B.CRDT, C.CRDT) = Merge(B.CRDT, C.CRDT, A.CRDT) = a new CRDT"]
      D --> E["5. 多次合并同一个节点的CRDT,其结果都等于合并一次,\n即Merge(A.CRDT, B.CRDT, B.CRDT) = Merge(A.CRDT, B.CRDT) = a new CRDT"]             

特别说明:第二点的最终值代表应用完所有节点对该值的更改后的最终值,时间戳同样。

实现

这个CRDT如下,我们暂时给它起名LWWRegister(稍后解释其为什么叫LWWRegister)

type LWWRegister<T> = {
    value: T;
    timestamp: number;
};

因为是分布式系统,且支持弱网环境,所以这个LWWRegister在不同节点里,都不一样。 比如

// 节点A的LWWRegister
A.LWWRegister = {
    value: "value1",
    timestamp: 1000,
}
// 节点B的LWWRegister
B.LWWRegister = {
    value: "value2",
    timestamp: 1500,
}
// 节点C的LWWRegister
C.LWWRegister = {
    value: "value3",
    timestamp: 1200,
}

各自节点可以随时更新自己的值和时间戳,比如在节点A中,更新LWWRegister

const update = (node, newValue: string, newTimestamp: number) => {
    node.LWWRegister = {
        value: newValue,
        timestamp: newTimestamp,
    };
};

合并不同节点的副本,比如merge(A.LWWRegister, B.LWWRegister),需要根据时间戳选择最新的值,步骤如下

flowchart LR
    B["1.创建一个LWWRegister\n 2. 比较A.LWWRegister\n和B.LWWRegister的timestamp"]
        B1["2.1 如果A.LWWRegister.timestamp > B.LWWRegister.timestamp,\n则选A.LWWRegister的值和timestamp"]
        B2["2.2 如果B.LWWRegister.timestamp > A.LWWRegister.timestamp,\n则选B.LWWRegister的值和timestamp"]
        B3["2.3 如果A.LWWRegister.timestamp === B.LWWRegister.timestamp,\n则按照预定义的规则(例如节点ID大小)选择值"]
    C["3. 返回选择后的LWWRegister"]
    B --> B1
    B --> B2
    B --> B3
    B1 --> C
    B2 --> C
    B3 --> C

代码如下:

const merge = (regA: LWWRegister<string>, regB: LWWRegister<string>): LWWRegister<string> => {
    if (regA.timestamp > regB.timestamp) {
        return regA;
    } else if (regB.timestamp > regA.timestamp) {
        return regB;
    } else {
        // 当时间戳相同时,使用节点ID进行比较,这里假设A的ID小于B
        return /* 根据节点ID决定返回哪个 */;
    }
}

const mergedRegister = merge(A.LWWRegister, B.LWWRegister)

你按随意的顺序合并A.LWWRegister,B.LWWRegister,C.LWWRegister,其结果都一致,即最终一致性。

总结

因为这个CRDT通过最后写入优先来解决冲突,,所以叫LWWRegister(Last-Write-Wins Register)