宝典目录
- CRDT宝典(一): 引言
- CRDT宝典(二): 基本概念
- CRDT宝典(三): GCounter
- CRDT宝典(四): PNCounter
- CRDT宝典(五): GSet
- CRDT宝典(六): PNSet
- CRDT宝典(七): VClock
- CRDT宝典(八): LLW-Register
- CRDT宝典(九): ORSet
- CRDT宝典(十): AWORSet
背景
分布式系统中的每一个节点都维护一个自身的counter,且每个节点只能增加自身的counter。
请设计一个CRDT(Conflict-free Replicated Data Type)来实现一套满足最终一致性的分布式系统
为了减少要理解的概念,下文描述的CRDT同时有两层意思
- 无冲突的数据类型,即类型
- 一个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