想象两个用户同时编辑同一份文档:Alice 在位置 3 插入 "Hello",Bob 同时在位置 5 插入 "World"。当两个修改合并时,如何保证结果正确?
CRDT(Conflict-free Replicated Data Type)是一类特殊的数据结构,满足两个数学性质:
这意味着:无论消息以什么顺序到达、重复到达多少次,最终状态都是相同的。不需要中心化服务器做仲裁。
根据 crdt.tech 的定义,CRDT 用于乐观复制 (Optimistic Replication) 场景——用户可以在任何副本上独立修改数据,即使副本离线也不影响。当副本重新连接时,CRDT 自动合并到一致状态,不需要用户干预。
| 维度 | OT (Operational Transform) | CRDT |
|---|---|---|
| 核心思想 | 收到并发操作时,变换操作使其适用于当前状态 | 数据结构本身保证合并无冲突 |
| 服务器需求 | 必须中心化——所有操作经过服务器排序 | 去中心化——任何副本可以独立修改 |
| 离线支持 | 差——需要服务器才能处理冲突 | 好——离线修改后合并自动收敛 |
| 代表系统 | Google Docs, ShareDB, Firepad | Yjs, Automerge, Figma(自研 CRDT) |
| 复杂度 | 操作变换逻辑复杂,易出 bug | 数据结构有额外开销(元数据) |
| 性能 | 快(简单的 for 循环变换偏移量) | 早期很慢,现在 Yjs 接近 OT 性能 |
| 内存开销 | 操作日志(可裁剪) | 文档元数据(早期巨大,现在优化) |
Joseph Gentle 是 Google Wave 的 OT 实现者,后来做了 ShareJS。他曾长期批评 CRDT——太慢、太占内存、缺功能。但 Martin Kleppmann 的工作改变了他:
"I was wrong. CRDTs are the future." — josephg.com
关键转折:CRDT 从"学术玩具"变成了"生产可用"。Yjs 的 Yata 结构使性能接近 OT,而 Automerge 在 Isabelle 定理证明器中验证了正确性。
只增不减的计数器。每个节点维护自己的计数器,合并时取每个节点的最大值。
可增可减的计数器。两个 G-Counter:一个计增量,一个计减量。
只增不减的集合。合并 = 并集。简单但无法删除元素。
可增可删的集合。每个元素带唯一 tag,删除只删 tag 不删元素。添加时创建新 tag,所以"先删后加"能正确生效。
用于协作编辑的核心 CRDT。每个字符有一个唯一 ID(包含节点 ID 和逻辑时间戳),插入时记录前驱字符的 ID。合并时按时间戳排序,冲突时按节点 ID 排序。
Yjs 使用改进的 Yata 结构,Automerge 使用 RGA 变体。两者的核心思想相同:给每个操作一个全局唯一、可排序的 ID。
Automerge 提供类似 JSON 的 API,但底层是 CRDT。每个 key 对应一个 CRDT 值,支持嵌套。
import * as Automerge from "@automerge/automerge"
// 节点 A:创建文档并修改
let docA = Automerge.from({ tasks: [], count: 0 })
docA = Automerge.change(docA, d => {
d.tasks.push({ text: "Buy milk", done: false })
d.count = 1
})
// 节点 B:独立修改(可能离线)
let docB = Automerge.from({ tasks: [], count: 0 })
docB = Automerge.change(docB, d => {
d.tasks.push({ text: "Write code", done: false })
d.count = 1 // 注意:两个节点都写了 count = 1
})
// 合并——无冲突!tasks 数组自动合并,count 值收敛
let merged = Automerge.merge(docA, docB)
// { tasks: [{text:"Buy milk",done:false}, {text:"Write code",done:false}],
// count: 1 }
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
// 1. 创建共享文档
const ydoc = new Y.Doc()
// 2. 创建共享类型
const ytext = ydoc.getText('content')
const ymap = ydoc.getMap('metadata')
const yarray = ydoc.getArray('items')
// 3. 连接到 WebSocket 服务器(可选,也可 P2P)
const provider = new WebsocketProvider(
'wss://your-server.com', 'room-id', ydoc
)
// 4. 绑定到编辑器(以 CodeMirror 6 为例)
import { yCollab } from 'y-codemirror.next'
const editor = new EditorView({
extensions: [
basicSetup,
yCollab(ytext, provider.awareness)
]
})
// 5. 监听变更
ytext.observe(event => {
console.log('Text changed:', ytext.toString())
// event.delta 包含具体变更内容
})
// 6. 手动合并两个文档(离线后同步)
const localDoc = new Y.Doc() // 本地修改
const remoteDoc = new Y.Doc() // 远程修改
Y.applyUpdate(localDoc, Y.encodeStateAsUpdate(remoteDoc))
// localDoc 现在包含了远程的修改,且无冲突
Yata 结构将文档编码为双链表,操作复杂度接近 O(1)。比早期 Automerge 快 10-100 倍。
文档编码紧凑,gzip 后接近原始文档大小。不像早期 CRDT 动辄 80MB+ 内存。
ProseMirror, Quill, CodeMirror, Monaco, Slate, React, Vue……覆盖主流前端技术栈。
| 维度 | Yjs | Automerge |
|---|---|---|
| 核心定位 | 协作编辑引擎 | 通用数据同步 |
| 数据模型 | 文本序列为主 | JSON-like(Map, Array, Text, Counter) |
| 正确性保证 | 测试验证 | Isabelle 形式化证明 |
| 性能 | 更快(Yata 结构优化) | 稍慢但持续改善(Rust 核心加速) |
| 生态 | 编辑器绑定丰富 | 更通用但生态较小 |
| 离线支持 | IndexedDB | 内置持久化 |
| 适合场景 | 文档编辑、代码协作 | 结构化数据同步、离线优先 App |
考虑以下场景:
如果用传统的"最后写入胜"(LWW),A 的修改可能被 B 覆盖。如果用 OT,需要中心化服务器。但 Agent 可能运行在边缘(手机、IoT 设备),中心化服务器不一定可用。
CRDT 是唯一能在去中心化环境下保证记忆一致性的方案。
// Agent 记忆的 CRDT 模型
interface AgentMemory {
// OR-Set:可增可删的知识条目
facts: ORSet<Fact>
// PN-Counter:追踪交互次数
interactionCount: PNCounter
// LWW-Register:用户偏好(最后写入胜,但带时间戳)
preferences: Map<string, LWWRegister>
// RGA:对话历史(有序、可插入)
conversationHistory: RGA<Message>
}
// 两个 Agent 实例的记忆合并
function syncMemory(local: AgentMemory, remote: AgentMemory): AgentMemory {
return {
facts: merge(local.facts, remote.facts), // 并集 + 删除传播
interactionCount: merge(local.interactionCount, remote.interactionCount), // 各节点最大值求和
preferences: mergeMaps(local.preferences, remote.preferences), // 按时间戳取最新
conversationHistory: merge(local.conversationHistory, remote.conversationHistory), // RGA 合并
}
}
OpenClaw 的 Agent 运行在多个平台(Telegram、Discord、WhatsApp 等),每个平台可能有自己的 Agent 实例。当多个实例同时修改记忆(MEMORY.md、daily notes),需要合并而不是覆盖。
当前的实现依赖 git merge 或文件覆盖,这在并发场景下可能丢失数据。CRDT 提供了一个更可靠的基础。参见 Agent 架构知识库。
CRDT 解决的是数据层面的冲突(合并总是成功的),但不解决语义层面的冲突。例如:Alice 把文档标题改为"A",Bob 改为"B"——CRDT 会保留其中一个(基于时间戳),但这可能不是用户想要的结果。
CRDT 保证最终一致性,但不保证强一致性。如果你的业务需要"读己之写"(我写入后立刻读到)或线性一致性,CRDT 不合适。银行余额、库存扣减——这些场景仍然需要事务。
Yjs 优化了文本编辑场景,Automerge 优化了 JSON 同步。如果你需要的是数据库复制(SQL 语义),它们的模型不完全匹配。选择工具要看数据模型,不只是看"支持 CRDT"。