🔀 CRDT:无冲突复制数据类型

为什么 Google Docs 能多人同时编辑?数学保证的冲突自由
数据采集:2025-05 · 参考:crdt.tech, yjs.dev, automerge.org, josephg.com

问题:并发修改如何解决?

想象两个用户同时编辑同一份文档:Alice 在位置 3 插入 "Hello",Bob 同时在位置 5 插入 "World"。当两个修改合并时,如何保证结果正确?

🧮 CRDT 的数学保证

CRDT(Conflict-free Replicated Data Type)是一类特殊的数据结构,满足两个数学性质:

交换律 (Commutativity): merge(A, B) = merge(B, A)
结合律 (Associativity): merge(merge(A, B), C) = merge(A, merge(B, C))
幂等律 (Idempotence): merge(A, A) = A

这意味着:无论消息以什么顺序到达、重复到达多少次,最终状态都是相同的。不需要中心化服务器做仲裁。

根据 crdt.tech 的定义,CRDT 用于乐观复制 (Optimistic Replication) 场景——用户可以在任何副本上独立修改数据,即使副本离线也不影响。当副本重新连接时,CRDT 自动合并到一致状态,不需要用户干预。

CRDT vs OT

维度OT (Operational Transform)CRDT
核心思想收到并发操作时,变换操作使其适用于当前状态数据结构本身保证合并无冲突
服务器需求必须中心化——所有操作经过服务器排序去中心化——任何副本可以独立修改
离线支持差——需要服务器才能处理冲突好——离线修改后合并自动收敛
代表系统Google Docs, ShareDB, FirepadYjs, Automerge, Figma(自研 CRDT)
复杂度操作变换逻辑复杂,易出 bug数据结构有额外开销(元数据)
性能快(简单的 for 循环变换偏移量)早期很慢,现在 Yjs 接近 OT 性能
内存开销操作日志(可裁剪)文档元数据(早期巨大,现在优化)

💡 Joseph Gentle 的转变(Google Wave 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 定理证明器中验证了正确性。

CRDT 类型详解

📊 Counters(计数器)

G-Counter (Grow-only Counter)

只增不减的计数器。每个节点维护自己的计数器,合并时取每个节点的最大值。

Node A: {A:3, B:0, C:0}
Node B: {A:0, B:5, C:0}
Merge: {A:3, B:5, C:0} = 8

PN-Counter (Positive-Negative Counter)

可增可减的计数器。两个 G-Counter:一个计增量,一个计减量。

P: {A:3, B:5, C:0} // 增量
N: {A:1, B:0, C:0} // 减量
Value = sum(P) - sum(N) = 8 - 1 = 7

📦 Sets(集合)

G-Set (Grow-only Set)

只增不减的集合。合并 = 并集。简单但无法删除元素。

A: {1, 2, 3}
B: {2, 3, 4}
Merge: {1, 2, 3, 4}

OR-Set (Observed-Remove Set)

可增可删的集合。每个元素带唯一 tag,删除只删 tag 不删元素。添加时创建新 tag,所以"先删后加"能正确生效。

A: add(x,tag1), rm(x,tag1)
B: add(x,tag2)
Merge: {x} // tag1 被删,tag2 保留

📝 Text / Sequences(文本序列)

RGA (Replicated Growable Array)

用于协作编辑的核心 CRDT。每个字符有一个唯一 ID(包含节点 ID 和逻辑时间戳),插入时记录前驱字符的 ID。合并时按时间戳排序,冲突时按节点 ID 排序。

Yjs 使用改进的 Yata 结构,Automerge 使用 RGA 变体。两者的核心思想相同:给每个操作一个全局唯一、可排序的 ID。

🗺️ Maps & JSON(结构化数据)

Automerge 的 JSON-like CRDT

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 }

🔀 CRDT 合并演示

🧪 OR-Set 交互式合并

📱 节点 A(Alice)
{ 🍎, 🍌, 🍒 }
💻 节点 B(Bob)
{ 🍌, 🍒, 🍇 }
⬇️ merge() ⬇️
✅ 合并结果
{ 🍎, 🍌, 🍒, 🍇 }
点击按钮模拟并发操作,然后合并

Yjs:生产级 CRDT

🏆 Yjs 关键数据(来源:yjs.dev

  • npm 周下载量 90万+——最流行的协作编辑 CRDT 引擎
  • 支持 ProseMirror、Quill、CodeMirror、Monaco 等主流编辑器
  • 网络无关——不依赖中心化服务器,可 P2P
  • 离线支持——IndexedDB 持久化,断网后继续编辑
  • Yata 结构:优化了 RGA 的存储和性能

Yjs 实战代码

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 现在包含了远程的修改,且无冲突

Yjs 的核心优势

🚀 性能

Yata 结构将文档编码为双链表,操作复杂度接近 O(1)。比早期 Automerge 快 10-100 倍。

📦 体积

文档编码紧凑,gzip 后接近原始文档大小。不像早期 CRDT 动辄 80MB+ 内存。

🔌 生态

ProseMirror, Quill, CodeMirror, Monaco, Slate, React, Vue……覆盖主流前端技术栈。

Automerge:学术级正确性

🔬 Automerge 关键数据(来源:automerge.org

  • 由 Martin Kleppmann(Cambridge 教授)和 Ink & Switch 实验室开发
  • 核心设计在 Isabelle 定理证明器中形式化验证——不是"测试通过",是"数学证明正确"
  • 支持者:Fly.io, Prisma, GoodNotes, NLNet, ARIA
  • 定位:"Version control for your data"——数据版本的 Git
  • 语言绑定:Rust (automerge-core), Python, Swift

Automerge vs Yjs

维度YjsAutomerge
核心定位协作编辑引擎通用数据同步
数据模型文本序列为主JSON-like(Map, Array, Text, Counter)
正确性保证测试验证Isabelle 形式化证明
性能更快(Yata 结构优化)稍慢但持续改善(Rust 核心加速)
生态编辑器绑定丰富更通用但生态较小
离线支持IndexedDB内置持久化
适合场景文档编辑、代码协作结构化数据同步、离线优先 App

🤖 Agent 记忆同步与 CRDT

为什么 Agent 记忆需要 CRDT?

考虑以下场景:

  • Agent 实例 A 在手机上运行,实例 B 在服务器上运行
  • 两者都修改了共享记忆——A 添加了"用户偏好深色主题",B 添加了"用户正在学习 Rust"
  • 手机断网了,无法联系服务器
  • 当手机重新联网时,两个版本的"记忆"需要合并

如果用传统的"最后写入胜"(LWW),A 的修改可能被 B 覆盖。如果用 OT,需要中心化服务器。但 Agent 可能运行在边缘(手机、IoT 设备),中心化服务器不一定可用。

CRDT 是唯一能在去中心化环境下保证记忆一致性的方案。

Agent 记忆 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 的关系

OpenClaw 的 Agent 运行在多个平台(Telegram、Discord、WhatsApp 等),每个平台可能有自己的 Agent 实例。当多个实例同时修改记忆(MEMORY.md、daily notes),需要合并而不是覆盖。

当前的实现依赖 git merge 或文件覆盖,这在并发场景下可能丢失数据。CRDT 提供了一个更可靠的基础。参见 Agent 架构知识库

常见误区

❌ "CRDT 解决了所有冲突"

CRDT 解决的是数据层面的冲突(合并总是成功的),但不解决语义层面的冲突。例如:Alice 把文档标题改为"A",Bob 改为"B"——CRDT 会保留其中一个(基于时间戳),但这可能不是用户想要的结果。

❌ "CRDT 可以替代数据库事务"

CRDT 保证最终一致性,但不保证强一致性。如果你的业务需要"读己之写"(我写入后立刻读到)或线性一致性,CRDT 不合适。银行余额、库存扣减——这些场景仍然需要事务。

❌ "Yjs/Automerge 可以直接用于所有场景"

Yjs 优化了文本编辑场景,Automerge 优化了 JSON 同步。如果你需要的是数据库复制(SQL 语义),它们的模型不完全匹配。选择工具要看数据模型,不只是看"支持 CRDT"。

🔗 延伸阅读

外部资源