思考一下,如果想要实现一个富文本编辑器的的协同编辑,你能想到哪几种方案呢?
这个是最最简单粗暴的办法:即保存最后一次修改,更早一些的其他人的修改直接被丢弃,也就是管其他人死活,我自己爽就好。
还可以利用锁机制去实现,比如:A用户正在编辑某个文档时,对此文档进行加锁处理,避免多人同时编辑,从而避免文档的内容冲突。优点:简单粗暴,但会影响用户体验。
第三种就是diff-patch了,我们可以类比基于git的版本管理,多人编辑时利用socket与服务端通信,当多人编辑时服务端进行差异对比、合并,自动进行冲突处理,在通过socket更新其他人本地的文档。弊端是会出现类似git修改同一行,纯靠服务端无法处理,需要手动处理的问题。
基于以上三种方法实现富文本编辑器的协同编辑时,无法实现真正意义的协同编辑。
于是就引出了OT和CRDT这类专门用于处理协同文档的方案,我们接下来主要是使用基于CRDT的Yjs方案实现一系列协同操作,不过也需要对OT做简单了解。
OT算法是一种用于实时协同编辑的算法,它通过操作转换来实现数据的一致性。在OT算法中,每个用户对数据的操作(如修改、删除等)都被记录下来,并在其他用户的客户端进行相应的转换,从而实现多个用户对同一份数据的协同编辑。
OT算法的优点在于它可以实时地反映用户的操作,并且可以很好地处理并发冲突。然而,OT算法需要在中心化的服务器上进行协同调度,因此对于大规模的分布式系统来说可能不太适用。
基于OT的协同编辑核心是:将文档的每一次修改看作是一个操作,即操作原子化处理,如在第N个位置插入一个字符时,客户端会将操作发送到服务端去处理。
客户端将原子化的操作发送到服务端时(必须有中央服务器进行调度),服务端对多个客户端的操作进行转换,对客户端操作中的并发冲突进行修正,确保当前操作同步到其他设备时得到一致的结果,因为对冲突的处理都是在服务端完成,所以客户端得到的结果一定是一致的,也就是说OT算法的结果保证强一致性。
转换完成后,通过网络发送到对应客户端,客户端合并操作,从而得到一致结果。
这也就意味着OT算法对网络要求更高,如果某个用户出现网络异常,导致一些操作缺失或延迟,那么服务端的转换就会出现问题。
下面是一个OT算法的协同过程:
上面这个演示体现了OT算法对网络要求更高的说法,Alice先修改的文档,由于网络的原因Bob的请求先到的服务器,但OT算法的期望是得到一致的结果,所以这一点看来也是没错的。
CRDT算法全称为Conflict-freeReplicatedDataType,即无冲突复制数据类型,是一种基于数据结构的无冲突复制数据类型算法,它通过数据结构的合并来实现数据的一致性。
在CRDT算法中,每个用户对数据的修改都会被记录下来,并在其他用户的客户端进行合并,以实现数据的一致性。CRDT算法的优点在于它可以适用于大规模的分布式系统,并且不需要中心化的服务器进行协同调度。但是,CRDT算法在处理复杂操作时可能会存在合并冲突的问题,需要设计复杂的合并函数来解决。
基于状态的CRDT更容易设计和实现,每个CRDT的整个状态最终都必须传输给其他每个副本,每个副本之间通过同步全量状态达到最终一致状态,这可能开销很大;
而基于操作的CRDT只传输更新操作,各副本之间通过同步操作来达到最终一致状态,通常很小。
下面是一个CRDT算法的协同过程:
向量时钟的基本思想是为系统中的每个节点维护一个向量,其中每个分量对应一个节点,用于记录该节点的事件发生次数。当一个节点发生事件时,它会增加自己分量的值。向量时钟的关键是在不同节点之间传递这些向量,并在合并时确保一致性。
两种方法的相似之处在于它们提供了最终的数据一致性。不同之处在于他们如何做到这一点:
综上所述,OT算法和CRDT算法各有优缺点,适用于不同的场景。如果需要实现文本的实时协同编辑,OT算法更为合适;如果需要在大规模分布式系统中实现数据一致性,CRDT算法更为合适。
CRDT不依赖于编辑器实现,使用它可以实现任意一款编辑器的协同编辑,只需要将对应的数据结构转换成CRDT的数据结构,内部会自动处理冲突和同步,并且可以不依赖中心化的服务器,在复杂的网络中表现更加稳健。
但可能存在网络先后达到顺序问题,并不能完全保证顺序是按照真实的用户意图发生,OT跟CRDT都是为了让所有的节点看到相同的内容,达到强一致性结果。
Yjs本身是一个数据结构,原理是:当两人协作时,对于文档内容修改,通过中间层将文档数据转换成CRDT数据;通过CRDT进行数据数据更新这种增量的同步,通过中间层将CRDT的数据转换成文档数据,另一个协作方就能看到对方内容的更新。
中间内容的更新是基于Yjs数据结构进行的,冲突处理等核心都是Yjs承担的,通信基于websocket或webrtc,所以我们只需要简单的使用,底层的冲突处理、光标等都不需要深入学习。
在上层Yjs支持任何大部分主流编辑器的接入,因为Yjs也可以理解为一套独立的数据模型,它与每种编辑器本身的数据模型是不同的,所以每种编辑器想要接入Yjs都必须实现一个中间绑定层,用于编辑器数据模型与Yjs数据模型转换,这个转换是双向的。
Yjs支持多种流行的文本和富文本编辑器,如
Yjs基于数据结构层面处理冲突,比OT更加稳健,对复杂网络的适应性更强。网络延时或离线编辑对数据结构来说,处理没有任何差异。
Yjs提供的Awareness(意识)模块,名如其意,让协作者能够意识到其他人的位置在哪,有效避免冲突可能性。
基于CRDT的内容合并,天然支持离线编辑,浏览器端做本地化存储。
Yjs自身提供了快照机制,保存历史版本不用保存全量数据,只是基于Yjs打一个快照,后续基于快照恢复历史版本。