CRDT宝典 - AWORSet

181 阅读4分钟

宝典目录

前提

我们在ORSet中实现的ORSet有如下的问题

  1. 在分布式系统中,当对Set的操作是高频的时候,因为多线程等问题,timestamp来做为操作前后判断的条件不是完全可靠的
  2. 每个元素都有一个timestamp类的元数据,其存储占用还是不小的,而且这里面存在可优化的点。

基于以上两点,我们会以另一个方式实现ORSet,而不是以timestamp时钟。

背景

在一个分布式系统中,有一个集合(Set),每个节点都能随意往里面添加、删除元素。

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

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

  1. 无冲突的数据类型,即类型
  2. 一个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()"]

特别说明:第一、二步看不懂的话,可以先深度回顾一下GCounterVClock

各自节点往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)。