Skip to main content

CRDT 冲突算法

CRDT 冲突算法

CRDT 全称为 Conflict-free Replicated Data Type,即无冲突复制数据类型。

OT 的缺陷

由于是 OT 是强一致性的设计,所有操作都需要在服务器端进行转换,从而避免冲突,因此 OT 算法对网络的要求比较高,如果某个用户出现网络异常,导致一些操作缺失或延迟,那么服务端的转换就会出现问题。因此,OT 算法不太适合用于分布式的系统,但分布式系统又是目前解决超级复杂问题的唯二选择。

CAP 理论

CAP 理论由 Eric Brewer 于 2000 年提出,是三个英文单词的首字母缩写:

  • C:Consistency 表示一致性,所有节点访问的数据必须是最新的、完全一致的。例如:如果在分布式数据库 A 节点写入了一条记录,然后立刻去 B 节点读取,能立刻能读到这个值,否则就不是强一致性。
  • A:Availability 表示可用性,系统在任何时刻都能正常响应请求,哪怕部分节点挂了、网络抖动,也能访问。例如:用户访问一个网站,即使有一个数据中心宕机了,系统也要保证能继续返回内容。
  • P:Partition Tolerance 表示分区容忍性,指的是系统能容忍网络出现分区故障,有些节点之间的通信失败了,系统仍要能继续运行。

Trade-offs

CAP

Consistency

Availability

Partition Tolerance

CA: Strong but no Partition Tolerance

CP: Strong Consistency under Partition, but not always Available

AP: Available and Partition-Tolerant, but may lose Consistency

在这三者中,如果 P 是必须的,这在分布式系统中这是无法避免的。在这样的背景下,系统最多只能保证 C 和 A 中的 一个。也就是说:

  • C + P (放弃可用性)。
  • A + P (放弃强一致性)。

核心思想

CRDT 的核心思路就是 放弃强一致性,转而追最终一致性,这是 CRDT 的一大特点。

在 CRDT 系统中,各个副本可以并发、独立地进行、本地修改,而无需等待远程同步或获得锁。即便出现网络延迟,甚至设备离线,只要最终各个副本都收到相同的操作或状态,就能够自动合并为一致的最终结果,而不需要中心协调或额外的冲突解决机制。

所以说 CRDT 的设计哲学是:

只要所有副本最终都收到相同的信息(状态或操作),不管谁先谁后、什么时候收到,最终状态就一定一样。

具体示例

初始文档为 abc,有两个用户 A 和 B 进行如下的编辑,采用 CRDT 算法。

首先需要设计一种带 ID 的数据结构,每个字符有一个唯一的 ID,如:

{
// 当前的字符
char: 'x',

// 当前这个字符位置的 id 以及客户端的 id
id: [position_id, client_id]
}

对于用户 A 来讲,文本内容为 abc,初始文档按照上面的数据结构来设计,如下表:

字符ID(简写)说明
a[1, A]初始由 A 写入
b[2, A]-
c[3, A]-

用户 A 要插入 Xa 的后面,a 的 ID 为 [1, A],插入后用户 A 会生成一个新的 ID:

// 表示插入到 position_id 1 和 2 之间
[1.5, A]

对于用户 A 来讲文档结构就变成:

[ [1, A]: 'a', [1.5, A]: 'X', [2, A]: 'b', [3, A]: 'c' ]

用户 B 插入 Y 在最前面,对于用户 B 来说,会生成一个新 ID:

[0.5, B]

对于用户 B 来讲,整个文档结构就变成:

[ [0.5, B]: 'Y', [1, A]: 'a', [2, A]: 'b', [3, A]: 'c' ]

在CRDT数据结构的设计中,文档的内容和作者并非强绑定的关系,每个字符节点对应的只有一个唯一的 ID。字符的 ID 反映的是该字符最初是谁插入的、在哪里插入的,而不是当前谁持有、谁读取或者谁同步到了它。

合并副本

最后需要对副本进行合并,两个副本并不存在冲突,只需要按照 ID 进行排序就可以了。

[0.5, B][1, A][1.5, A][2, A][3, A]
Y → a → X → b → c

在进行比较的时候,通常有一组比较的策略,例如可以先比较位置,位置相同比较客户端 ID。

理论实现

CRDT 是一套理论,基于这套理论,有各种不同的实现,不同的实现自然 ID 的设计也会有所不同。

CRDT 算法ID 结构特点
RGA[前一字符 ID, 副本 ID]通过链表式插入
Logoot[路径 ID, 用户 ID]ID 可排序且稀疏
Yjs类似 Logoot + 优化实际使用偏移机制

数据结构

在 CRDT 中,数据结构的设计非常关键,要求设计出来的数据结构具备以下的特点:

  1. 可交换性:只要每个副本使用相同的合并函数,那么不论先后,最终能得到相同的结果。这个特点可以无视消息的到达顺序,不需要去协调谁先谁后。
merge(a, b) === merge(b, a)
  1. 可结合性:合并的顺序可以自由的嵌套,不影响结果。同样可以保证多个副本汇合合并的时候不需要在意操作顺序。
merge(merge(a, b), c) === merge(a, merge(b, c))
  1. 幂等性:同样的数据或者操作,即便合并多次,结果也不会变化。网络往往是不稳定的,存在重发消息的情况,因此必须保证多个副本合并多次保持同一状态。
merge(state, state, state, ...) === state

冲突免疫

CRDT 的数据结构的设计确保所有操作天然不冲突,即使并发发生也无需判断谁赢谁输,这一点是和 OT 之间的最大区别。

[ [1, A]: 'a', [2, A]: 'b', [3, A]: 'c' ]

// 用户A : 插入一个 c
[ [1, A]: 'a', [2, A]: 'b', [3, A]: 'c', [4, A]: 'c' ]

// 用户B:删除一个 c
[ [1, A]: 'a', [2, A]: 'b', [3, A]: 'c'(delete)]

// 两个副本进行合并
[ [1, A]: 'a', [2, A]: 'b', [4, A]: 'c']

CRDT数据结构

在 2011 年发表的 CRDT的论文 中,论文作者明确提出并设计了一些具有代表性的 CRDT 数据结构,每一个都体现了 CRDT 的核心原则:无需协调、天然可合并、保证最终一致性。论文中正式提出并详解的数据结构包括:

名称类型特点总结支持操作
G-CounterCvRDT只能递增,每个副本维护自己计数增加
PN-CounterCvRDT支持加减,使用两个 G-Counter增加 / 减少
G-SetCvRDT只能添加元素,不能删除添加
2P-SetCvRDT添加 + 删除(删除不可恢复)添加 / 删除
LWW-Element-SetCvRDT添加/删除带时间戳,最后写入者生效添加 / 删除
OR-SetCvRDT使用唯一 ID 跟踪添加/删除操作添加 / 删除
Graph(有向图)CvRDT节点和边都是 OR-Set 构建,支持惰性删除添加节点 / 边 / 删除

G-Counter

G-Counter,英文全称是 Grow-only Counter,这是一个 只能递增 的分布式计数器,每个副本维护自己的局部值, 所有副本最终通过取最大值进行合并,从而保证最终一致。

G-Counter 的核心是一个 以副本 ID 为 key 的映射表:

{
// 这里的 A、B、C 就是副本 ID,也就是客户端 ID
A: 3,
B: 5,
C: 0
}

每个副本(比如客户端 A、B、C)只能操作自己的那一项

该数据结构支持的操作有:

  1. increment() 增加计数。只能增加,不支持减!
  2. merge() 合并另一个副本的状态,对每个副本 ID 的值取最大值
  3. value() 计算当前总和,将所有客户端的计数全部累加起来。

举一个例子,初始的时候,两个客户端 A 和 B,因此就有两个副本,分别是 副本 A 和副本 B

副本 A: { A: 3 } // 在客户端A,用户点击了 3 次
副本 B: { B: 4 } // 在客户端B,用户点击了 4 次
// 假设现在没有联网,两者都是处于离线状态
  • A 的 G-Counter 里,只知道自己加了 3 次

  • B 的 G-Counter 里,只知道自己加了 4 次

假设此时,A 又本地加了 1:A: { A: 4 },B 又本地加了 1:B: { B: 5 },那么此时各副本的状态为:

  • A 的副本状态是 { A: 4 }
  • B 的副本状态是 { B: 5 }
  • 注意:现在仍然是离线状态

接下来假设来网了,两个副本开始互相同步状态。这里就涉及到要做一个 merge() 操作,把两个 G-Counter 合并成一个一致的状态merge() 具体如何做呢?合并规则很简单:对每个副本 ID,取它在两个副本中值的最大值。我们来计算:

A 的值:max(A副本的A, B副本的A) = max(4, undefined) = 4
B 的值:max(A副本的B, B副本的B) = max(undefined, 5) = 5

undefined 代表这个副本没记录过这个 ID。

合并出来的结果为 { A: 4, B: 5 },这个结果现在被 A 和 B 同时拥有,也就是它们两个副本的状态同步了。

  • A 的副本状态是 { A: 4, B: 5}
  • B 的副本状态是 { A: 4, B: 5}

因此总的值就为:

value = 4 + 5 = 9

这个值就是当前这个计数器代表的全局计数值

应用场景
  1. 点赞数 / 浏览量:每个副本只需要往上加。
  2. 分布式日志计数:多节点写日志,最后总数一致即可。
  3. 活跃节点统计:每个节点上报“活跃数”,总数即在线节点数。

CRDT类型

在论文中,提到了 CRDT 可以分为两种类型:

  1. 状态型 CRDT,英文缩写为 CvRDT,Convergent Replicated Data Type
  2. 操作型 CRDT,英文缩写为 CmRDT,Commutative Replicated Data Type
CvRDT

定义:每个副本维护自己的完整状态,副本之间周期性同步整个状态对象,合并时使用一个具有数学特性的 merge() 函数,使多个副本最终收敛为一致的状态。

特点

特性说明
同步单位整个“状态”(state)
合并机制merge() 函数(满足 交换 + 结合 + 幂等)
合并时机异步、周期性、最终到达
抗延迟/断网非常强(副本之间互相独立演进)
要求副本持有完整状态

经典的 CvRDT 例如:

  • G-Counter:每个副本存一份 { replicaId: number },合并时取最大值
  • OR-Set:add/remove 是状态,合并时做集合并集
  • Graph CRDT:节点/边状态可直接合并
CmRDT

定义:每个副本本地执行修改,同时将操作(operation)发送给其他副本,操作之间是可交换的(Commutative),所以不同副本按任意顺序执行都能得到相同结果。

特点

特性说明
同步单位操作(operation)
合并机制每个副本按本地逻辑执行操作
操作要求必须是“可交换的”
网络要求每个操作都必须可靠地送达一次
对操作顺序的依赖无(顺序无关)

经典的 CmRDT 例如:

  • RGA(Replicated Growable Array):文本插入操作是 insert(afterId, value),谁先谁后无所谓
  • Logoot、Yjs:插入/删除基于唯一 ID,执行顺序不影响最终顺序
  • 聊天消息流 CRDT:每条消息是一个 append 操作

两者对比如下表所示:

对比维度CvRDTCmRDT
同步的内容状态(整个结构)操作(insert/delete等)
合并方式merge() 函数顺序无关执行所有操作
是否要求操作可交换否(状态可合并即可)✅ 是
是否要完整状态✅ 是❌ 否(只要操作流)
网络要求支持延迟/离线要求操作可靠送达(但可乱序)
实现复杂度简单清晰实现更复杂,需处理引用 ID、时钟等
OT vs CRDT

两种方法的相似之处在于它们提供了最终的数据一致性。不同之处在于他们如何做到这一点:

  • OT 通过算法控制保证数据一致性:操作通过发送到服务端,一旦收到就会进行转换( OT 控制算法)。
  • CRDT 通过数据结构保证数据一致性:操作是在本地 CRDT 上进行的,它的状态或者操作通过网络发送并与副本的状态合并。

其他不同点

  1. OT 是基于中央服务器进行转换操作;而 CRDT 只需要与同伴交换新数据,不需要中央服务器,每个客户端都可以是独立完整的版本,支持离线进行操作,因此稳定性很高
  2. OT 相较于 CRDT 发展更早,技术体系更为成熟,服务端压力大,客户端无需像 CRDT 那样存储大量额外元数据,因此压力较小。服务端重客户端轻
  3. OT 用复杂性换来了对用户预期的实现;而 CRDT 则更加关注数据结构,复杂性低,不过随着数据结构的复杂度上升,算法的时间和空间复杂度也会呈指数上升的,会带来性能上的挑战。服务端轻客户端重
  4. OT 处理不同数据模型需要实现不同的转换算法;CRDT 只需要转化数据结构。
  5. CRDT 去中心化的特征使其很容易实现离线编辑

另外在这篇 文章 中,作者也提到了为什么 AFFiNE 选择 CRDT 而非 OT 来构建协作编辑器。

AFFiNE 是一个开源的现代化协同文档编辑器项目,该项目采用了 Notion + Miro + Obsidian 的混合风格,强调:

  • 支持多人协作
  • 支持本地编辑
  • 支持 markdown + 富文本 + 白板
  • 基于 CRDT 技术实现同步

AFFiNE 目标是构建一个“本地优先、支持多人实时协作、集文档、白板、任务于一体”的全功能办公工具。