无冲突复制数据类型CRDT(Conflict-free Replicated Data Types)

CRDT(Conflict-free Replicated Data Types,无冲突复制数据类型)

CRDT是分布式系统中的一种数据结构设计方法,用来解决多副本数据在并发修改时如何自动合并且不产生冲突的问题。

CRDT 的核心目标是:在没有中心协调的情况下,让多个节点各自修改数据,最终自动收敛到一致状态。


为什么需要 CRDT?

在分布式系统(比如协同编辑、离线应用)中,经常会遇到:

  • 多个用户同时修改同一份数据
  • 网络延迟或断连(离线操作)
  • 不同节点之间的数据不同步

传统方案(如锁、主从同步)会带来:

  • 延迟高
  • 可用性差
  • 容易冲突

CRDT 则提供了一种“天然可合并”的解决思路。


核心思想

CRDT 的关键是设计一种数据结构,使得:

  1. 操作可交换(commutative)
  2. 操作可重复(idempotent)
  3. 操作顺序无关(order-independent)

这样即使:

  • 消息乱序
  • 重复发送
  • 延迟到达

最终结果仍然一致(称为“强最终一致性”)。


两大类 CRDT

1️⃣ 状态型(State-based / CvRDT)

  • 每个节点维护完整状态
  • 定期同步并“合并状态”

合并函数必须满足:

  • 交换律
  • 结合律
  • 幂等性

例子:

  • G-Set(只增集合)
  • PN-Counter(正负计数器)

2️⃣ 操作型(Operation-based / CmRDT)

  • 只传播操作(而不是整个状态)
  • 要求操作是可交换的

例子:

  • 协同编辑中的插入/删除操作

常见 CRDT 示例

计数器(Counter)

比如点赞数:

  • 节点 A:+1
  • 节点 B:+1

无论同步顺序如何:
最终都是 2


集合(Set)

  • 添加元素不会冲突
  • 删除需要特殊设计(如 OR-Set)

文本编辑(协同文档)

像:

  • Google Docs
  • Notion

多人同时编辑: CRDT 可以保证文本最终一致(很多现代协同编辑系统都基于 CRDT 或类似技术)


CRDT vs 传统方案

特性 CRDT 传统锁/主从
是否需要中心节点 ❌ 不需要 ✅ 需要
是否支持离线 ✅ 很强 ❌ 较弱
冲突处理 自动 手动/复杂
延迟 较高

优点

  • 高可用(AP 系统)
  • 自动冲突解决
  • 支持离线操作
  • 低延迟

缺点

  • 设计复杂(需要数学性质保证)
  • 数据结构受限(不是所有类型都容易做成 CRDT)
  • 可能占用更多存储(需要额外元数据)

简单总结

CRDT 就像是一个“自带合并规则”的数据结构:

不管谁先改、谁后改
不管网络是否稳定

最终大家的数据都会自动变成一样


实际应用场景

用一个多人同时编辑一段文本的例子,来一步步演示 CRDT 是怎么工作的。这个过程会尽量贴近像 Google Docs 或 Notion 背后的原理。

初始文本:

"Hi"

有两个用户:

  • 用户 A(设备 A)
  • 用户 B(设备 B)

两人同时离线编辑


第一步:给每个字符一个“唯一 ID”

CRDT 的关键:每个字符不是简单位置,而是带唯一标识

比如初始状态可以表示为:

H(id=1), i(id=2)

第二步:用户同时编辑

👤 用户 A 的操作

A 想输入:

"Heyi"

操作:

  • 在 H 后插入 e
  • 在 e 后插入 y

表示为:

insert('e', after id=1, new_id=3)
insert('y', after id=3, new_id=4)

👤 用户 B 的操作

B 想输入:

"Hoi"

操作:

  • 在 H 后插入 o

表示为:

insert('o', after id=1, new_id=5)

第三步:发生网络同步(乱序到达)

现在开始同步,但注意:

消息可能乱序到达!

假设:

  • A 先收到 B 的操作
  • B 后收到 A 的操作

第四步:合并操作(核心)

在 A 的设备上

原本 A 的本地状态:

H(1) e(3) y(4) i(2)

收到 B 的操作:

insert('o', after id=1, id=5)

问题来了: H 后面已经有 e 了,还要插入 o,怎么办?


CRDT 的解决办法:确定性排序

CRDT 会规定一个规则,例如:

👉 按 ID 排序(或时间戳 + 节点 ID)

假设排序规则是:

按 id 从小到大

现在 H 后的元素有:

  • e(id=3)
  • y(id=4)
  • o(id=5)

排序后:

H e y o i

在 B 的设备上

B 原本:

H(1) o(5) i(2)

收到 A 的操作:

insert('e', after 1, id=3)
insert('y', after 3, id=4)

合并后同样排序:

H e y o i

第五步:最终一致!

不管:

  • 谁先同步
  • 操作顺序如何
  • 网络是否延迟

👉 最终两边都变成:

"Heyoi"

这就是 CRDT 的核心能力:

不同路径,收敛到同一个结果(Strong Eventual Consistency)


关键点拆解

1️⃣ 不用“位置”,用“关系”

不是:

插入到第2个位置 ❌

而是:

插入到 id=1 后面 ✅

避免位置冲突


2️⃣ 每个操作都是“可合并的”

  • 插入不会冲突
  • 删除用“标记删除”(tombstone)

3️⃣ 必须有“确定性规则”

比如:

  • ID 排序
  • Lamport 时间戳
  • 节点 ID

保证所有节点算出一样的结果

一句话总结这个过程

CRDT 在做的事情其实是:

把“文本编辑”变成一堆带唯一 ID 的操作
再用数学规则合并这些操作
保证所有设备最终结果一样

posted @ 2026-04-11 21:42  aixueforever  阅读(0)  评论(0)    收藏  举报