CRDT 详解
12月 26, 2025
一、什么是 CRDT?#
CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)是一种分布式数据结构,能够在网络中的多个节点间自动解决冲突,最终达到一致状态。
简单来说:
CRDT 让多个人可以同时编辑同一份数据,而不需要中央服务器仲裁,冲突会自动被解决。
二、为什么需要 CRDT?#
传统编辑的问题:
// 用户 A 和用户 B 同时编辑
// 场景:两人都想修改同一段文字
用户A: "Hello World" → "Hello Universe"
用户B: "Hello World" → "Hello Everyone"
// 传统方式会出现冲突,需要服务器裁决
// 结果不确定,取决于谁先提交
CRDT 的解决方案:
// CRDT 知道每个字符的来源和顺序
// 即使同时编辑,也能自动合并
最终结果: "Hello Universe and Everyone"
// 或自动排序: "Hello Everyone and Universe"
三、CRDT 核心原理#
1. 唯一标识符(ID)#
每个操作都有唯一标识:
// 传统方式:按位置操作(有问题)
{
operation: "insert",
position: 5,
text: "Beautiful "
}
// CRDT:按 ID 操作(无冲突)
{
operation: "insert",
id: { clientId: 1, clock: 15 }, // 全局唯一
text: "Beautiful "
}2. 逻辑时钟#
// 每个客户端维护自己的时钟
interface ClientState {
clientId: number; // 客户端 ID
clock: number; // 逻辑时钟
// 每次操作前先递增时钟
nextClock(): number {
return ++this.clock;
}
}
// 客户端 A: ID=1, clock=10
// 客户端 B: ID=2, clock=10
// A 插入: { clientId: 1, clock: 10 }
// B 插入: { clientId: 2, clock: 10 }
// 时钟比较: (1, 10) vs (2, 10)
// 如果 ID 不同,按 ID 排序
// 结果: A 的内容在前,B 的内容在后
3. 偏序关系#
// CRDT 使用偏序关系确定操作顺序
function compare(a: OpId, b: OpId): number {
// 1. 时钟小的在前
if (a.clock !== b.clock) return a.clock - b.clock;
// 2. 时钟相同,ID 小的在前
return a.clientId - b.clientId;
}
// 示例
const op1 = { clientId: 1, clock: 5 };
const op2 = { clientId: 2, clock: 5 };
const op3 = { clientId: 1, clock: 6 };
// 排序结果: op1 < op2 < op3
四、CRDT 主要类型#
1. CmRDT(操作型 CRDT)#
只传输操作(Operation):
// 传输的操作
interface Operation {
type: 'insert' | 'delete';
id: OpId;
value?: string; // insert 时需要
length?: number; // delete 时需要
}
// 发送方
function sendOperation(op: Operation) {
broadcast(op); // 广播给所有客户端
}
// 接收方
function receiveOperation(op: Operation) {
apply(op); // 应用操作
}2. CvRDT(状态型 CRDT)#
传输完整状态:
// 状态合并
interface State {
version: number;
data: Map<string, string>;
}
// 合并两个状态
function merge(state1: State, state2: State): State {
const result = { ...state1 };
// 取版本号最大的
result.version = Math.max(state1.version, state2.version);
// 合并数据(后写入覆盖先写入)
for (const [key, value] of state2.data) {
result.data.set(key, value);
}
return result;
}五、常见的 CRDT 数据结构#
1. G-Set(Grow-only Set)#
只能添加,不能删除:
class GSet<T> {
private items = new Set<T>();
add(item: T) {
this.items.add(item);
return this;
}
merge(other: GSet<T>): GSet<T> {
const result = new GSet<T>();
// 合并两个集合
for (const item of this.items) result.add(item);
for (const item of other.items) result.add(item);
return result;
}
}2. 2P-Set(Two-Phase Set)#
添加和删除分开处理:
class TwoPSet<T> {
private added = new Set<T>(); // 添加集
private removed = new Set<T>(); // 删除集
add(item: T) {
this.added.add(item);
}
remove(item: T) {
this.removed.add(item);
}
contains(item: T): boolean {
return this.added.has(item) && !this.removed.has(item);
}
merge(other: TwoPSet<T>): TwoPSet<T> {
const result = new TwoPSet<T>();
result.added = new Set([...this.added, ...other.added]);
result.removed = new Set([...this.removed, ...other.removed]);
return result;
}
}3. LWW-Register(最后写入胜出寄存器)#
class LWWRegister<T> {
private value: T;
private timestamp: number;
private owner: string;
set(value: T, owner: string) {
this.timestamp = Date.now();
this.value = value;
this.owner = owner;
}
merge(other: LWWRegister<T>): LWWRegister<T> {
if (other.timestamp > this.timestamp) {
return other;
}
return this;
}
}4. 文本 CRDT(Yjs 的核心)#
核心思想:每个字符都有唯一 ID
// Yjs 的文本实现
class YText {
private chars = new Map<string, Char>();
insert(index: number, text: string, clientId: number) {
// 生成唯一 ID
const id = {
clientId,
clock: this.nextClock++,
};
// 创建字符对象
for (const char of text) {
const charId = { ...id, offset: index++ };
this.chars.set(toString(charId), { id: charId, content: char });
}
// 按 ID 排序存储
this.reindex();
}
delete(index: number, length: number) {
// 软删除:标记删除
const ids = this.getIdsAtIndex(index, length);
for (const id of ids) {
this.chars.get(toString(id)).deleted = true;
}
}
merge(other: YText): YText {
// 合并两个文本
const merged = new YText();
// 遍历所有字符
for (const [id, char] of this.chars) {
merged.chars.set(id, char);
}
for (const [id, char] of other.chars) {
// 冲突解决:ID 大的在后
if (merged.chars.has(id)) {
// 保留更新的版本
if (char.timestamp > merged.chars.get(id).timestamp) {
merged.chars.set(id, char);
}
} else {
merged.chars.set(id, char);
}
}
return merged;
}
toString(): string {
// 按 ID 排序后拼接
return Array.from(this.chars.values())
.filter(c => !c.deleted)
.sort((a, b) => compareIds(a.id, b.id))
.map(c => c.content)
.join('');
}
}六、Yjs 中的 CRDT 实现#
1. Doc(文档根)#
import * as Y from 'yjs';
// 创建文档
const ydoc = new Y.Doc();
// 获取共享类型
const yText = ydoc.getText('content'); // 共享文本
const yArray = ydoc.getArray('items'); // 共享数组
const yMap = ydoc.getMap('metadata'); // 共享 Map
// 监听变更
ydoc.on('update', (update: Uint8Array, origin: unknown) => {
console.log('文档更新:', update);
});
// 事务
ydoc.transact(() => {
yText.insert(0, 'Hello');
yArray.push([1, 2, 3]);
yMap.set('key', 'value');
});2. Text(文本)#
const yText = ydoc.getText('content');
// 插入
yText.insert(0, 'Hello World');
// 删除
yText.delete(0, 5);
// 获取内容
console.log(yText.toString()); // "World"
// 监听
yText.observe(event => {
console.log('操作类型:', event.transaction.local ? '本地' : '远程');
console.log('插入:', event.delta.filter(d => d.insert));
console.log('删除:', event.delta.filter(d => d.delete));
});
// 撤销
const undoManager = new Y.UndoManager(yText);
undoManager.undo();
undoManager.redo();3. Array(数组)#
const yArray = ydoc.getArray('items');
// 添加
yArray.push([1, 2, 3]); // 末尾添加
yArray.unshift(0); // 开头插入
// 获取
console.log(yArray.toArray()); // [0, 1, 2, 3]
// 遍历
yArray.forEach(item => console.log(item));
// 监听
yArray.observe(event => {
console.log('插入:', event.changes.added);
console.log('删除:', event.changes.deleted);
});4. Map(映射)#
const yMap = ydoc.getMap('metadata');
// 设置
yMap.set('title', 'My Doc');
yMap.set('author', 'John');
// 获取
console.log(yMap.get('title')); // "My Doc"
// 删除
yMap.delete('author');
// 遍历
yMap.forEach((value, key) => {
console.log(`${key}: ${value}`);
});七、CRDT vs OT(Operational Transformation)#
| 特性 | CRDT | OT |
|---|---|---|
| 冲突解决 | 本地自动解决 | 需要服务器仲裁 |
| 延迟 | 低(无需等待) | 高(需要服务器确认) |
| 离线编辑 | ✅ 支持 | ❌ 困难 |
| 一致性 | 最终一致性 | 强一致性 |
| 复杂度 | 数学证明复杂 | 实现复杂 |
| 服务器 | 轻量(只转发) | 需要处理冲突 |
| 典型实现 | Yjs, Automerge | Google Docs |
OT 的问题:
// Google Docs 使用的 OT
// 需要服务器按顺序处理操作
// 操作序列必须严格排序
OpA → OpB → OpC
// 如果顺序错了,结果就错了
OpA → OpC → OpB // 可能导致不一致
CRDT 的优势:
// CRDT:操作顺序不影响最终结果
// 两个操作 A 和 B,无论以什么顺序应用,结果都相同
// 应用 A 然后 B
apply(A);
apply(B); // 结果: "AB"
// 应用 B 然后 A
apply(B);
apply(A); // 结果: "AB"
八、CRDT 的实际应用#
1. 代码示例:多人协作编辑器#
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// 1. 创建文档
const ydoc = new Y.Doc();
// 2. 连接 WebSocket
const provider = new WebsocketProvider(
'wss://demos.yjs.dev',
'my-room-name',
ydoc
);
// 3. 获取共享文本
const yText = ydoc.getText('editor');
// 4. 创建编辑器绑定
const editor = new Editor({
extensions: [
// 将 Yjs 绑定到编辑器
Collaboration.configure({
document: ydoc,
}),
],
});
// 5. 监听用户状态
provider.awareness.setLocalState({
user: {
name: 'Alice',
color: '#ff0000',
},
});
provider.awareness.on('change', () => {
console.log('在线用户:', provider.awareness.getStates());
});2. 代码示例:离线优先应用#
import * as Y from 'yjs';
import { IndexeddbPersistence } from 'y-indexeddb';
// 创建文档
const ydoc = new Y.Doc();
// 本地持久化
const persistence = new IndexeddbPersistence('my-app', ydoc);
persistence.on('synced', () => {
console.log('本地数据加载完成');
});
// 离线编辑
ydoc.getText('notes').insert(0, '这是在离线时编辑的');
// 重新上线后同步
// IndexeddbPersistence 会自动处理同步
九、CRDT 的优缺点#
优点#
- 离线支持:可以离线编辑,联网后自动同步
- 无冲突:自动解决冲突
- 低延迟:无需等待服务器确认
- 去中心化:可以 P2P 直连
- 可扩展性:易于添加新客户端
缺点#
- 存储开销:每个字符都有元数据
- 内存占用:历史操作会累积
- 复杂度:实现和调试困难
- 最终一致性:不是强一致性
- 垃圾回收:需要清理历史数据
十、解决缺点的方法#
1. 压缩历史#
// Yjs 压缩
const ydoc = new Y.Doc();
// 获取状态向量
const stateVector = Y.encodeStateVector(ydoc);
// 压缩到特定版本
const compressed = Y.encodeStateAsUpdate(ydoc, stateVector);
// 合并更新
Y.applyUpdate(ydoc, compressed);2. 垃圾回收#
// 清理已删除的内容
const gc = new Y.Doc();
Y.gc(gc);
// 清理特定文档
Y.mergeUpdates([
Y.encodeStateAsUpdate(ydoc1),
Y.encodeStateAsUpdate(ydoc2),
]);十一、总结#
CRDT 的核心价值:
┌─────────────────────────────────────────────────┐
│ CRDT 的核心 │
├─────────────────────────────────────────────────┤
│ │
│ 1. 每个操作都有全局唯一标识 │
│ ↓ │
│ 2. 操作可以以任意顺序应用 │
│ ↓ │
│ 3. 最终结果总是相同的 │
│ ↓ │
│ 4. 冲突自动解决,无需人工干预 │
│ │
├─────────────────────────────────────────────────┤
│ │
│ 适用场景: │
│ ✓ 实时协作编辑器 │
│ ✓ 离线优先应用 │
│ ✓ P2P 应用 │
│ ✓ 分布式数据库 │
│ │
└─────────────────────────────────────────────────┘理解 CRDT 是构建现代协作应用的关键,它让"多人同时编辑"变得简单可靠。BlockSuite/Yjs 正是利用了 CRDT 的这些特性,才能实现流畅的离线编辑和实时协作体验。