CRDT宝典 - GCounter

236 阅读2分钟

宝典目录

背景

分布式系统中的每一个节点都维护一个自身的counter,且每个节点只能增加自身的counter。

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

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

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

思维链

flowchart 
      A["1. CRDT可以应用在不同的节点中"]
      A --> B["2. CRDT可以表达任意节点的counter"]
      B --> C["3. CRDT可以让任意节点增加自身的counter"]
      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如下,我们暂时给它起名GCounter(稍后解释其为什么叫GCounter)

const GCounter: Record<string, number> = {
    [A.id]: 1,
    [B.id]: 2,
    [C.id]: 3,
}

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

// 节点A的GCounter
A.GCounter = {
    [A.id]: 1,
    [B.id]: 1,
    [C.id]: 1,
}
// 节点B的GCounter
B.GCounter = {
    [A.id]: 1,
    [B.id]: 2,
    [C.id]: 1,
}
// 节点C的GCounter
C.GCounter = {
    [A.id]: 0,
    [B.id]: 1,
    [C.id]: 2,
}

各自节点只能增加自身的counter,比如在节点A中,只能增加GCounter[A.id]+=1

合并不同节点的副本,比如Merge(A.GCounter,B.GCounter),需要将A.GCounter和B.GCounter中的相同id的item的值进行合并,步骤如下

flowchart LR
    B["1.创建newGCounter \n2.合并A、B节点的key为一个Set,遍历Set"]
        B1["2.1 如果A.GCounter[id]存在,但是B.GCounter[id]不存在,\n那么将newGCounter[id] = A.GCounter[id]"]
        B2["2.2 如果B.GCounter[id]存在,但是A.GCounter[id]不存在,\n那么将newGCounter[id] = B.GCounter[id]"]
        B3["2.3 如果A.GCounter[id]和B.GCounter[id]都存在,\n那么newGCounter[id]= Max( A.GCounter[id], B.GCounter[id])"]
    C["4.返回newGCounter"]
    B --> B1
    B --> B2
    B --> B3
    B1 --> C
    B2 --> C
    B3 --> C
A.GCounter = {
    [A.id]: 1,
    [B.id]: 1,
    [C.id]: 1,
}
// 节点B的数据副本
B.GCounter = {
    [A.id]: 1,
    [B.id]: 2,
    [C.id]: 1,
}
// 某个节点只能增加自身GCounter中id为自身的counter,比如A节点只能增加GCounter[A.id]的counter,默认+1
const inc = (node) => {
    node.GCounter[node.id] += 1
}
// 合并两个GCounter
const merge = (GCounterA: GCounter, GCounterB:GCounter) => {
    const newGCounter = {}
    const ids = new Set([...Object.keys(GCounterA), ...Object.keys(GCounterB)])
    ids.forEach(id => {
        // 我就不写这么多if else了,直接缩写
        newGCounter[id] = Math.max(GCounterA[id] || 0, GCounterB[id] || 0)
    })
    return GCounter
}
const newGCounter = merge(A.GCounter, B.GCounter)

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

QA

问:为什么inc操作是默认+1

答:阅读完以后的文章,你便会知道

总结

因为其特殊的场景,这个CRDT有一个专属的名字Grow-only Counter,简称GCounter