宝典目录
- CRDT宝典(一): 引言
- CRDT宝典(二): 基本概念
- CRDT宝典(三): GCounter
- CRDT宝典(四): PNCounter
- CRDT宝典(五): GSet
- CRDT宝典(六): PNSet
- CRDT宝典(七): VClock
- CRDT宝典(八): LLW-Register
- CRDT宝典(九): ORSet
- CRDT宝典(十): AWORSet
背景
在一个分布式系统中,有一个集合(Set),每个节点都能往里面添加元素,但只能进行添加元素的操作。
请设计一个CRDT(Conflict-free Replicated Data Type)来实现一套满足最终一致性的分布式系统
为了减少要理解的概念,下文描述的CRDT同时有两层意思
- 无冲突的数据类型,即类型
- 一个CRDT实例,即实例
思维链
flowchart TD
A["1. CRDT可以应用在不同的节点中"]
A --> B["2. CRDT可以表达所有节点往集合中添加的元素"]
B --> C["3. CRDT可以让任意节点往这个Set添加元素"]
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如下,我们暂时给它起名GSet(稍后解释其为什么叫GSet)
const GSet:Set<T> = new Set()
因为是分布式系统,且支持弱网环境,所以这个GSet在不同节点里,都不一样。
比如
// 节点A的数据副本
A.GSet = ["element1", "element2", "element3"]
// 节点B的数据副本
B.GSet = ["element1", "element2", "element4"]
// 节点C的数据副本
C.GSet = ["element1", "element2", "element3", "element5"]
各自节点往GSet中添加元素,比如A节点往GSet中添加元素,通过A.GSet.add(element)即可
合并不同节点的GSet,比如Merge(A.GSet,B.GSet),就是将B.GSet中的元素添加到A.GSet中
// 节点A的数据副本
A.GSet = ["element1", "element2", "element3"]
// 节点B的数据副本
B.GSet = ["element1", "element2", "element4"]
// 任意节点都能往GSet中添加元素
GSet[A.id].add("elementX")
const add = (node, value: T) => {
node.GSet.add(value)
}
// 合并两个GSet
const merge = (GSetA: GSet, GSetB: GSet) => {
const newGSet: GSet = new Set([...GSetA, ...GSetB])
return newGSet
}
const newGSet = merge(A.GSet, B.GSet)
你按随意的顺序合并A.GSet,B.GSet,C.GSet,其结果都一致,即最终一致性。
QA
问:为什么不设计成以下的格式
const GSet: Record<string, Set<T>> = {
[A.id]: new Set(["element1"]),
[B.id]: new Set(["element1", "element2"]),
[C.id]: new Set(["element1", "element2", "element3"]),
}
答:这样的设计会导致不同id的Set里有大量的重复元素,浪费存储;且add、merge操作都比原文的方式要复杂
总结
因为只能往集合中添加东西,这个CRDT有一个专属的名字Grow-only Set,简称GSet。