CRDT宝典 - VClock

208 阅读2分钟

宝典目录

前提

我们在前文学习了GCounter,它代表分布式系统中节点自身增加的值,以及所有节点的counter的和。

让我们回顾一下它的数据结构

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

它代表A节点的GCounter,其中A节点增加的值为1,B节点增加的值为2,C节点增加的值为2,这个GCounter的值为1+2+2=5

我们给它换一个名字,叫VTime

背景

分布式系统中的每一个节点都维护一个自身的counter,节点只能增加自身的counter,且能比较不同节点的CRDT的新旧

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

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

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

思维链

flowchart TD
      A["1. CRDT可以应用在不同的节点中"]
      A --> B["2. CRDT可以表达任意节点的counter"]
      B --> C["3. CRDT可以让任意节点增加自身的counter"]
      C --> D["4. 两个节点的CRDT可以进行新旧比较,判断哪个节点的CRDT更加新,哪个节点更加旧"]
      D --> E["5. 可以合并多个节点的CRDT,且无论合并的顺序是怎么样的,其结果都一致,\n即Merge(A.CRDT, B.CRDT, C.CRDT) = Merge(B.CRDT, C.CRDT, A.CRDT) = a new CRDT"]
      E --> F["6. 多次合并同一个节点的CRDT,其结果都等于合并一次,\n即Merge(A.CRDT, B.CRDT, B.CRDT) = Merge(A.CRDT, B.CRDT) = a new CRDT"]
      F --> G["7. 很明显,`VTime`能满足这个CRDT的大部分需求,但是它不能满足`比较两个CRDT的新旧`的需求"]
      G --> H["8. 而比较两个`VTime`的新旧 = 比较两个`GCounter`的新旧 = 比较两个`GCounter`中所有节点的`Counter`的新旧"]
      H --> I["9. 所以,我们可以基于`VTime`来实现这个CRDT"]

实现

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

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

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

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

各自节点只能增加自身的counter,同GCounter

合并不同节点的VClock,同GCounter

比较两个VClock的新旧,我们设立四种状态来表达两个VClock的新旧关系

  • flag = 1,表示A.VClockB.VClock
  • flag = 0,表示A.VClockB.VClock一样新
  • flag = -1,表示A.VClockB.VClock
  • flag = -2,表示A.VClockB.VClock中有冲突,代表冲突状态
flowchart LR
    B["1.初始化一个flag = 0\n2. 合并A.VClock和B.VClock的key为Set\n3.遍历Set"]
        B1["3.1 如果A.VClock[id] === B.VClock[id]),\n则继续循环"]
        B2["3.2 如果(flag === 1 或 flag = 0)\n且(( A.VClock[id]存在 且 B.VClock[id]不存在)\n或(A.VClock[id] > B.VClock[id] ))\n则flag = 1,继续循环"]
        B3["3.3 如果(flag === -1 或 flag = 0)\n且(( B.VClock[id]存在 且 A.VClock[id]不存在)\n或(B.VClock[id] > A.VClock[id] ))\n则flag = -1,继续循环"]
        B4["3.4 否则,flag = -2,跳出循环"]
    C["4.返回flag"]
    B --> B1
    B --> B2
    B --> B3
    B --> B4
    B1 --> C
    B2 --> C
    B3 --> C
    B4 --> C

代码如下

// 节点A的VClock
A.VClock = {
    [A.id]: 1,
    [B.id]: 1,
    [C.id]: 1,
}
// 节点B的VClock
B.VClock = {
    [A.id]: 1,
    [B.id]: 2,
    [C.id]: 1,
}
const compare = (A.VClock, B.VClock) => {
    const flag = 0
    const set = new Set([...Object.keys(A.VClock), ...Object.keys(B.VClock)])
    for (const id in set) {
        // 2.1 如果相等则继续循环
        if (A.VClock[id] === B.VClock[id]) {
            continue
        }
        // 2.2 如果flag为1或0,且A比B新,则flag=1
        if ((flag === 1 || flag === 0) && 
            ((A.VClock[id] && !B.VClock[id]) || (A.VClock[id] > B.VClock[id]))) {
            flag = 1
            continue
        }
        // 2.3 如果flag为-1或0,且B比A新,则flag=-1 
        if ((flag === -1 || flag === 0) &&
            ((B.VClock[id] && !A.VClock[id]) || (B.VClock[id] > A.VClock[id]))) {
            flag = -1
            continue
        }
        // 2.4 否则有冲突,flag=-2
        flag = -2
        break
    }
    return flag
}

总结

因为能比较两个VClock的新旧,且不借用时间戳的概念,其属于一种逻辑时钟,所以叫VClock

VClock接下来有大用处