Woot 算法
这个算法应该算是早起的 crdt 的雏形了,这个方案避免了时序向量的使用,但是需要为每个字符都保存一组元数据,对删除的元素也只是设为不可见,元数据会在整个文档的生命周期中存在。
时序向量的弊端:随着副本数量的增加而不断增大规模,这会限制系统的伸缩性。
OT 与 CRDT 的一点不同:OT 算法需要中心服务器的参与,而 CRDT 是针对 P2P 环境下的,无需中心服务器参与内容修改的合并计算,更加节省服务器资源,同时更加适用于加密通信的场景,也就是无审查的场景。
设计
将操作分为两类
- ins(a<e<b) 在 a 和 b 之间插入 e
- del(e) 删除 e
与其它算法基于位置 id 的设计不同,WOOT 是基于元素来定义操作的。这也意味着必须为每个 atoms 都赋予唯一 id,并且这些 id 构成 total order set 。这也决定了墓碑机制的必要性,即元素无法被真正删除。不然无法保证操作的可交换性。
同时,基于元素的操作,也意味着操作的可执行性依赖于其定义中的元素是否存在,如果没有满足条件,则该操作将被滞后执行。
如上图所示,每一个插入操作都会产生两个新的顺序关系。然而,这些顺序关系并非完全闭合的,也就是并不是每个字符都能进行比较。
比如,当 site3 收到 op2 时,a 和 b 之间存在元素 3 和 1,那么 2 到底应该放到哪里呢,因此 2 还需要额外的信息来和 3 与 1 进行比较。但是如果先和 3 比较的结果,与先和 2 比较的结果不同怎么办呢?一旦和 3 进行比较得到了状态 231,那么如果有某个 sitex 上只有 1 的话,那 2 的位置将会出现不一致(12)。
因此,必须要限制用来比较的对象,其所在的顺序范围是一致的,也就是说,如果 a< x,y,z < b,同时 a<x<y,那么 z 就不能在 a,b 范围内与 x 进行比较,而只能与满足 a < y < b 的 y 进行比较。这样一来,即使存在其它站点还没有 x,甚至还没有 y 的时候,z 插入后,再接收到 y 和 x 的插入操作时,y 也只是与 z 进行比较,从而获得一致的顺序(因为 x 的插入依赖于 y 的存在,因此始终会先执行 y 再执行 x 的插入)。
也就是说,只有在相同范围内进行的比较才能保证顺序无关,即 commute 。
实现
定义一:每个字符用一个元组来表示
定义二:字符 c 的前一个字符为 ,后一个字符为 ,每个站点拥有唯一 id ,一个逻辑时间戳 以及一个操作的缓存池
定义三:一个字符的id 是一对值 ,ns 是站点的 id,ng 是一个自然数(也就是添加这个字符时当前站点的时间戳)
定义四:ins(c) 插入字符 c,根据和来确定插入的位置,del(c) 删除字符 c,事实上是将这个字符的 v 设为 false$
算法
为了保证 convergence,任意两个操作之间应该可交换,但是根据上面对插入和删除操作的定义,并不能保证每组操作都是可交换的。
上图是主要的程序结构,不断从操作缓冲池中取出操作并执行。
上面这部分是 WOOT 算法实现 convergence 的核心逻辑。也就是在本文的【设计】这一部分说明的,在确定字符插入位置的时候,需要通过递归地调用插入程序来不断缩小位置的范围,并最终找到正确的位置,每个端都按照这样的过程进行插入操作,就可以保证最终一致性。
总结
WOOT 算法相比之前的实现,最大的优势就是不再依赖于 vector clocks,其主要目的在于解决端到端网络场景下的数据最终一致性问题,这只是大规模协同编辑的一小步,许多其他问题仍然有待解决(在写作本文的西历 2006 年)
- 如何确保数据安全
- 如何提供 awareness
- 如何实现协作进程
- 如何只同步整个文档的片段