宝典目录
- CRDT宝典(一): 引言
- CRDT宝典(二): 基本概念
- CRDT宝典(三): GCounter
- CRDT宝典(四): PNCounter
- CRDT宝典(五): GSet
- CRDT宝典(六): PNSet
- CRDT宝典(七): VClock
- CRDT宝典(八): LLW-Register
- CRDT宝典(九): ORSet
- CRDT宝典(十): AWORSet
前提
我们在ORSet中实现的ORSet有如下的问题
- 在分布式系统中,当对Set的操作是高频的时候,因为多线程等问题,timestamp来做为操作前后判断的条件不是完全可靠的
- 每个元素都有一个timestamp类的元数据,其存储占用还是不小的,而且这里面存在可优化的点。
基于以上两点,我们会以另一个方式实现ORSet,而不是以timestamp时钟。
背景
在一个分布式系统中,有一个集合(Set),每个节点都能随意往里面添加、删除元素。
请设计一个CRDT(Conflict-free Replicated Data Type)来实现一套满足最终一致性的分布式系统
为了减少要理解的概念,下文描述的CRDT同时有两层意思
- 无冲突的数据类型,即类型
- 一个CRDT实例,即实例
思维链
flowchart TD
A["1. CRDT可以在不同的节点中使用"]
A --> B["2. CRDT可以表达所有节点最终往Set中添加的元素集合"]
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"]
E --> F["6. 在PNSet的基础上,解决PNSet的问题即可"]
F --> G["7. 如果我们能比较同一元素的`添加`和`删除`操作的先后顺序,就可以解决问题"]
G --> H["8. 除了时间戳的方式,我们还可以使用VClock比较操作的先后顺序"]
H --> I["9. 在合并时,只保留操作顺序较后的元素,即最新的状态"]
特别说明:第八点:VClock
实现
这个CRDT如下,它名字是ORSet
const ORSet[<Record<T, VClock>>, <Record<T, VClock>>] = [
{
'element1': {
[A.id]: 1,
[B.id]: 2,
},
'element2': {
[A.id]: 1,
[B.id]: 1,
},
'element3': {
[A.id]: 1,
[B.id]: 3,
},
},
{
'element2': {
[A.id]: 2,
[B.id]: 2,
[C.id]: 3,
},
}
]
很明显,这个ORSet的值为["element1", "element3"],因为ORSet[1][element2].compare(ORSet[0][element2) === 1]
因为是分布式系统,且支持弱网环境,所以这个ORSet在不同节点里,都不一样。
比如
// 节点A的数据副本
A.ORSet = [
{
'element1': {
[A.id]: 1,
[B.id]: 2,
},
'element2': {
[A.id]: 1,
[B.id]: 1,
},
'element3': {
[A.id]: 1,
[B.id]: 3,
},
},
{
'element2': {
[A.id]: 2,
[B.id]: 2,
[C.id]: 3,
},
}
]
// 节点B的数据副本
B.ORSet = [
{
'element1': {
[A.id]: 2,
[B.id]: 1,
},
'element2': {
[A.id]: 3,
[B.id]: 3,
},
'element4': {
[A.id]: 2,
[B.id]: 4,
},
},
{
'element2': {
[A.id]: 5,
[B.id]: 2,
[C.id]: 1,
},
}
]
各自节点往ORSet中添加元素,比如A节点往ORSet中添加元素element2,思维链如下:
flowchart TD
A["1. 如果 A.ORSet[0][element2] 存在\n则 A.ORSet[0][element2] = new VClock(A.ORSet[0][element2]).inc(A)\n delete A.ORSet[1][element2]"]
A --> B["2. 如果 A.ORSet[1][element2]存在 \n则A.ORSet[0][element2] = new VClock(A.ORSet[1][element2]).inc()\n delete A.ORSet[1][element2]"]
B --> C["3. A.ORSet[0][element2] = VClock.zero()"]
特别说明:第一、二步看不懂的话,可以先深度回顾一下GCounter和VClock
各自节点往ORSet中删除元素,比如A节点往ORSet中删除元素,思维链如下:
flowchart TD
A["1. 如果 A.ORSet[1][element2] 存在\n则 A.ORSet[1][element2] = new VClock(A.ORSet[1][element2]).inc()\n delete A.ORSet[0][element2]"]
A --> B["2. 如果 A.ORSet[0][element2] 存在\n则A.ORSet[1][element2] = new VClock(A.ORSet[0][element2]).inc()\n delete A.ORSet[0][element2]"]
B --> C["3. A.ORSet[1][element2] = VClock.zero()"]
ORSet的值为:删除ORSet[0]中ORSet[1]存在且VClock较旧的元素后得到的Set
合并不同节点的ORSet,比如merge(A.ORSet, B.ORSet),思维链如下:
flowchart TD
A["1. 创建一个mergeAddMap,\n 其值等于A.ORSet[0]和B.ORSet[0]的`并集`"]
A --> B["2. 创建一个mergeDelMap,\n其值等于A.ORSet[1]和B.ORSet[1]的`并集`"]
B --> C["3. 遍历mergeAddMap,删除其VClock小于mergeDelMap的VClock的元素"]
C --> D["4. 遍历mergeDelMap,删除其VClock(小于、等于、冲突)mergeAddMap的VClock的元素"]
D --> E["5. 返回new ORSet([mergeAddMap, mergeDelMap])"]
特别说明:第四步删除mergeDelMap中VClock(小于、等于、冲突)mergeAddMap的VClock的元素,很明显,我们更倾向于添加的元素,而不是删除的元素,所以本CRDT解决冲突的算法叫Add-Win策略(添加操作优先)
代码如下:
// 节点A的数据副本
A.ORSet = [
{
'element1': {
[A.id]: 1,
[B.id]: 2,
},
'element2': {
[A.id]: 1,
[B.id]: 1,
},
'element3': {
[A.id]: 1,
[B.id]: 3,
},
},
{
'element2': {
[A.id]: 2,
[B.id]: 2,
[C.id]: 3,
},
}
]
// 节点B的数据副本
B.ORSet = [
{
'element1': {
[A.id]: 2,
[B.id]: 1,
},
'element2': {
[A.id]: 3,
[B.id]: 3,
},
'element4': {
[A.id]: 2,
[B.id]: 4,
},
},
{
'element2': {
[A.id]: 5,
[B.id]: 2,
[C.id]: 1,
},
}
]
// 添加元素
const add = (node, element) => {
// 如果node.ORSet[0][element]存在
if (node.ORSet[0][element]) {
node.ORSet[0][element] = new VClock(node.ORSet[0][element].value() + 1);
delete node.ORSet[1][element]; // 标记为已删除
}
// 如果node.ORSet[1][element]存在
else if (node.ORSet[1][element]) {
node.ORSet[0][element] = new VClock(node.ORSet[1][element].value() + 1);
delete node.ORSet[1][element]; // 标记为已删除
} else {
// 如果都不存在,初始化为零时钟
node.ORSet[0][element] = VClock.zero();
}
}
// 删除元素
const remove = (node, element) => {
// 如果node.ORSet[1][element]存在
if (node.ORSet[1][element]) {
node.ORSet[1][element] = new VClock(node.ORSet[1][element].value() + 1);
delete node.ORSet[0][element];
}
// 如果node.ORSet[0][element]存在
else if (node.ORSet[0][element]) {
node.ORSet[1][element] = new VClock(node.ORSet[0][element].value() + 1);
delete node.ORSet[0][element];
} else {
// 如果都不存在,初始化为零时钟
node.ORSet[1][element] = VClock.zero();
}
}
// 获取ORSet的值
const value = () => {
const result = new Set();
// 遍历ORSet[0]中的所有元素
for (const [element, addClock] of Object.entries(node.ORSet[0])) {
// 检查该元素是否在ORSet[1]中
const removeClock = node.ORSet[1][element];
// 如果元素不在删除集合中,或者添加时间戳大于删除时间戳,则保留该元素
if (!removeClock || addClock.compare(removeClock) >= 0) {
result.add(element);
}
}
return result;
}
// 合并两个ORSet
const merge = (ORSetA: ORSet, ORSetB: ORSet) => {
// 1. 创建mergeAddMap和mergeDelMap
const mergeAddMap = {};
const mergeDelMap = {};
// 2. 合并A和B的添加集合
for (const [element, clock] of Object.entries(ORSetA[0])) {
mergeAddMap[element] = clock;
}
for (const [element, clock] of Object.entries(ORSetB[0])) {
if (!mergeAddMap[element]) {
mergeAddMap[element] = clock;
}else {
mergeAddMap[element] = mergeAddMap[element].merge(clock);
}
}
// 3. 合并A和B的删除集合
for (const [element, clock] of Object.entries(ORSetA[1])) {
mergeDelMap[element] = clock;
}
for (const [element, clock] of Object.entries(ORSetB[1])) {
if (!mergeDelMap[element]) {
mergeDelMap[element] = clock;
}else {
mergeDelMap[element] = mergeDelMap[element].merge(clock);
}
}
// 4. 删除mergeAddMap中VClock小于mergeDelMap的元素
for (const [element, clock] of Object.entries(mergeAddMap)) {
if (mergeDelMap[element] && clock.compare(mergeDelMap[element]) === -1) {
delete mergeAddMap[element];
}
}
// 5. 删除mergeDelMap中VClock小于等于或冲突的元素
for (const [element, clock] of Object.entries(mergeDelMap)) {
if (mergeAddMap[element] && clock.compare(mergeAddMap[element]) !== 1) {
delete mergeDelMap[element];
}
}
return new ORSet([mergeAddMap, mergeDelMap]);
}
const newORSet = merge(A.ORSet, B.ORSet)
你按随意的顺序合并A.ORSet,B.ORSet,C.ORSet,其结果都一致,即最终一致性。
QA
问:本文实现ORSet的方案,其元信息很大,没看出来比timestamp有什么优势
答:本文的实现方案是基础班,我们马上会实现一个优化版,其元信息会小很多。
总结
因为我们本CRDT通过写入优先的方式来解决冲突,所以也叫AWORSet(全称:Add-Win Observed-Remove Set)。
同样通过最后写入优先的方式来解决冲突的CRDT,也可以叫LWWORSet(全称:Last-Write-Win Observed-Remove Set)。