CRDT宝典 - yata算法

宝典目录

背景

分布式系统中有一个可索引序列(用数组为例),每个节点均可以往序列里面插入、删除元素

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

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

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

思维链

没什么难度的,借用DotKernel的思路,数组的每个元素 = 一个操作 = 一个dot,直接上代码

ts 复制代码
// A节点的数据副本
// id由节点id和操作序列counter组成
// value是值
A.DotKernel-Array: Array<{
    id: [string, number],
    value: string
}> = [
    {
        id: [A.id, 1],
        value: 1
    },
    {
        id: [A.id, 2],
        value: 4
    }
]

如果我们想在数组的某个元素的后面添加一个元素,比如A.DotKernel-Array[0]后面加一个元素,如果我们直接

ts 复制代码
A.DotKernel-Array[1] = {
        id: [A.id, 3],
        value: 2
}

然后将1以后的元素都往后偏移一位。

那么问题来了

ts 复制代码
{
        id: [A.id, 3],
        value: 2
}

表达不了其在A.DotKernel-Array[0]之后。

那简单呀,我们加一个指针

ts 复制代码
{
        id: [A.id, 3],
        value: 2,
        pointer: [A.id, 1]
}

另一个问题出现,当我们在数组的开头添加一个元素时,没有左指针只能指向空,如果是单节点的话,到没什么问题。但是分布式系统中,每个节点的第一个元素的了pointer都可能指向空,那么在合并时就蒙了。

还有另一个问题,当你想在某个元素的左侧添加元素时怎么办?

显然一个指针是不够的,我们需要两个指针。

ts 复制代码
{
        id: [A.id, 3],
        value: 2,
        leftPointer: [A.id, 1],
        rightPointer: [A.id, 2]
}

完美,这样别的节点就知道A节点是在[A.id, 1][A.id, 2] 之间插入了一个[A.id, 3]

那么我们想要删除某个位置的元素呢?

简单呀!我们直接删掉这个元素。

但是有问题呀!别人不知道这个删除操作,而且如果别的节点在这个被删除的元素的右侧/左侧添加了元素的话,那么就蒙了。

所以我们不能硬删,得软删,我们将元素的value设置为null即可,这样别人就知道这个元素被删除了。

那么合并两个节点的DotKernel-Array呢?思维链如下:(里面全是伪代码?千万不要较真!)

flowchart LR A["1. 新建一个newDotKernel-Array = \n\nA.DotKernel-Array。\n\n2. 遍历B.DotKernel-Array"] A --> B["2.1 如果B.DotKernel-Array[id].value为null\n\n且newDotKernel-Array[id].value不为null,\n\n代表B节点的操作操作,A节点不知道,\n\n则newDotKernel-Array[id].value = null"] A --> C["2.2 如果newDotKernel-Array[id]不存在,\n\n且B.DotKernel-Array[id].leftPointer\n\n和B.DotKernel-Array[id].rightPointer\n\n在newDotKernel-Array中都存在,\n\n代表B节点的新增操作,A节点不知道,\n\n则将B.DotKernel-Array[id]插入newDotKernel-Array中"] E["3. 返回新的DotKernel-Array"] B --> E C --> E

2.2步里的插入操作,我们用B.DotKernel-Array[id]leftPointerrightPointernewDotKernel-Array中找到插入位置,然后插入B.DotKernel-Array[id]

但是如果newDotKernel-ArrayleftPointerrightPointer之间就有元素怎么办,即别的节点也在这之间添加过元素,这就是冲突,我们如何解决?

针对同一个节点的来的冲突操作,因为item.id里有节点的操作序列counter,所以同一个节点的冲突操作可以组成一个有序的序列。

针对不同节点的冲突操作,如果不做操作的话,那合并后的操作序列就是乱序的(不难理解吧,我就不给例子了),那我们怎么办呢?

很简单咯,我们可以用item.id里的节点id作为比较的根据,通过这样的方式,节点id较大的操作永远在节点id较小的操作之前,不论在任何节点合并newDotKernel-Array时都一样,即最终一致性。

这两点缺一不可噢,否则无法保证最终一致性。

不知道你们发现没,我们不需要考虑A节点被删除的元素被其他的元素恢复的问题!!!!!!!!因为我们设定好不可以将值为null的元素恢复为非null的操作,如果有这样的需求,即通过重新插入一个值相同的元素即可。这帮我们避免了很多问题!!!!

实现

ps:代码里面很多没考虑性能,只是作为示例,不要较真。

ts 复制代码
A.DotKernel-Array: DotKernel-Array = [
    {
        id: [A.id, 1],
        value: 1
    },
    {
        id: [A.id, 2],
        value: 4
    },
    {
        id: [B.id, 1],
        value: 2
    }
]
B.DotKernel-Array: DotKernel-Array = [
    {
        id: [B.id, 1],
        value: 2
    },
    {
        id: [B.id, 2],
        value: 2
    }
]

// 添加元素
const addElement = (dotKernelArray: DotKernel-Array, element: {
    id: [string, number],
    value: string
}, index:number) => {
    const newDotKernelArray = dotKernelArray.slice()
   // 获取实际的index,包括被删除的元素
   const actualIndex = newDotKernelArray.findIndex((item) => {
        if(index < 0 ) {
            return true
        }
        if(item.value !== null) {
            index--
        } else {
            return false
        }
   })

   // 插入元素,代码就在下面两行
   integrate(newDotKernelArray, actualIndex, element, originLeftIndex, originRightIndex)
   return newDotKernelArray
}

// 在合适的位置集成元素
const integrate = (dotKernelArray: DotKernelArray, actualIndex: number, targetElement: {
    id: [string, number],
    value: string
}, originLeftIndex: number, originRightIndex: number) => {
    let finalIndex = originRightIndex
    for(let i = originRightIndex; i >= originLeftIndex; i--) {
        const element = dotKernelArray[i]
        if(targetElement.id[0] > element.id[0] || (targetElement.id[1] >element.id[1])) {
            finalIndex = i
        }
    }
    dotKernelArray.splice(finalIndex, 0, targetElement)
}

// 删除元素
const deleteElement = (dotKernelArray: DotKernelArray, element: {
    id: [string, number],
    value: string
}, index:number) => {
    const newDotKernelArray = dotKernelArray.slice()
    const actualIndex = newDotKernelArray.findIndex((item) => {
        if(index < 0 ) {
            return true
        }
        if(item.value !== null) {
            index--
        } else {
            return false
        }
    })
    newDotKernelArray[actualIndex].value = null
    return newDotKernelArray
}

// 合并
const merge = (dotKernelArray1: DotKernelArray, dotKernelArray2: DotKernelArray) => {
    const newDotKernelArray = dotKernelArray1.slice()
    // 将B节点中,A不知道的删除操作应用到newDotKernelArray
    dotKernelArray2.forEach((item) => {
        if(item.value === null) {
            deleteElement(newDotKernelArray, item, item.id[1])
        }
    })

    // 将B节点中,A不知道的新增操作应用到newDotKernelArray
    dotKernelArray2.forEach((item) => {
        if(!dotKernelArray1.has(item) && dotKernelArray1.has(item.leftPointer) && dotKernelArray1.has(item.rightPointer)) {
            addElement(newDotKernelArray, item, item.id[1])
        }
    })

    return newDotKernelArray
}

你按随意的顺序合并A.DotKernel-Array,B.DotKernel-Array,C.DotKernel-Array,其结果都一致,即最终一致性。

QA

问:不允许将value为null的元素恢复为非null的元素,也就是说如果用户频繁删除一个元素,然后恢复它,这样会多无意义的墓碑数据。

答:好问题噢,yjs里有介绍,如何合并墓碑数据(笑)

问:这个CRTD的场景我觉得用双向链表更好

答:你可以尝试用双向链表实现add、del、merge、integrate,看看是可索引序列性能高,还是双向链表性能高。

问:可以接入delta的理念吗?

答:自然,因为这属于state-based CRDT,如下

ts 复制代码
A.Delta-DotKernel-Array: {
    values:DotKernel-Array,
    deltas:DotKernel-Array
} = {
    values: [ 
        {
            id: [A.id, 1],
            value: 1
        }
    ],
    deltas: [
        {
            id: [A.id, 1],
            value: 1
        }
    ]
}

总结

这个可索引序列的CRDT就是yata算法的核心,恭喜,state-based CRDT快学到头啦!!!!。

后面还有operation-based CRDT。

相关推荐
88号技师44 分钟前
2024年12月一区SCI-加权平均优化算法Weighted average algorithm-附Matlab免费代码
人工智能·算法·matlab·优化算法
IT猿手1 小时前
多目标应用(一):多目标麋鹿优化算法(MOEHO)求解10个工程应用,提供完整MATLAB代码
开发语言·人工智能·算法·机器学习·matlab
88号技师1 小时前
几款性能优秀的差分进化算法DE(SaDE、JADE,SHADE,LSHADE、LSHADE_SPACMA、LSHADE_EpSin)-附Matlab免费代码
开发语言·人工智能·算法·matlab·优化算法
2401_882727571 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
我要学编程(ಥ_ಥ)1 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
NoneCoder2 小时前
CSS系列(36)-- Containment详解
前端·css
埃菲尔铁塔_CV算法2 小时前
FTT变换Matlab代码解释及应用场景
算法
anyup_前端梦工厂2 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js