为什么分布式协作需要 CRDT?
在现代分布式系统中,多用户实时协作编辑已经成为从文档编辑到金融交易的核心需求。传统方案依赖中心化服务器和锁机制,而 CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)提供了一种优雅的数学解法:允许节点独立修改、自动合并,最终收敛到一致状态,无需协调服务器介入。
本文从数学基础到工程实现,全面剖析 CRDT 的设计哲学、核心分类、算法实现以及在生产环境中的应用实践。
第一节:数学基础——半格(Semilattice)
CRDT 的正确性建立在偏序集(Partially Ordered Set)和半格的数学理论之上。一个二元关系 ≤ 要构成偏序,必须满足自反性、反对称性和传递性。
在半格中,任意两个元素都存在最小上界(least upper bound, LUB),即 join 操作。这个操作的幂等性、交换性和结合性,恰恰是分布式系统最终收敛的数学保证——无论节点以何种顺序接收消息,最终合并结果都相同。
我们用一个直观的例子说明:假设有三个副本分别持有值 {a}、{a,b}、{a,c},它们的 join 结果是 {a,b,c},这正是 CRDT 的 State-based(CvRDT)模型的合并语义。
第二节:两大流派——State-based vs Op-based
2.1 State-based CRDT (CvRDT)
State-based 方案中,每个节点维护完整状态,通过反熵(anti-entropy)机制定期向其他节点广播状态。接收方执行 join 操作实现合并。核心要求是状态空间构成一个 join-semilattice,且更新操作是单调递增的。
其优势在于协议简单、抗网络分区能力强,代价是带宽开销随状态大小线性增长。典型实现包括 G-Counter、PN-Counter、G-Set 等。
2.2 Operation-based CRDT (CmRDT)
Op-based 方案仅传播更新操作而非完整状态。发送操作前需保证因果一致性(通过向量时钟或版本向量),接收方按因果顺序应用操作。
优势是带宽效率高(只传增量操作),但要求可靠有序通信层交付消息,且需处理并发操作的交换语义。代表实现有 OR-Set、RGA(Replicated Growable Array)等。
第三节:核心数据结构详解
3.1 G-Counter(增长计数器)
G-Counter 是最简单的 CRDT,仅支持递增操作。每个节点维护一个数组 P[i] 记录本地增量,合并时逐元素取最大值。其单调递增特性和 max 操作的幂等性天然满足 semilattice 要求。
// G-Counter 伪代码
class GCounter {
id: int
P: int[N] // N 个节点的计数
increment() { this.P[this.id] += 1 }
value(): int { return sum(this.P) }
merge(other: GCounter) {
for i in 0..N: this.P[i] = max(this.P[i], other.P[i])
}
}
在分布式追踪系统中,G-Counter 常用于全局事件计数,如 UV 统计、点击计数等场景。
3.2 PN-Counter(可增减计数器)
将 G-Counter 与一个记录递减的 N-Counter 组合,形成支持 increment/decrement 的 PN-Counter。其最终值为 P_sum - N_sum。实际应用中需注意溢出问题,通常选用足够宽的整数类型。
3.3 OR-Set(观察移除集合)
OR-Set 是最具工程价值的 CRDT 之一。它通过为每个元素关联唯一 tag(如 UUID)来解决"add-win"语义下的删除冲突问题。添加元素生成新 tag,删除则记录所有已观察到的 tag,最终元素可见当且仅当存在未被删除的 tag。
但在生产环境中,tag 集合会无限增长。工程优化手段包括:使用 dot(node-id + counter)替代 UUID、采用 Dotted DBV(Dotted Database Version Vector)压缩版本向量。
3.4 LWW-Element-Set(最后写入胜出)
为 OR-Set 的每个元素附加时间戳(通常使用 Hybrid Logical Clock),解决并发插入删除冲突:add 和 remove 操作各自携带时间戳合并时取时间戳更大的操作胜出。
LWW 的优势是内存效率高(只需一个时间戳),其弱点在于时钟偏移可能导致数据丢失。HLC 通过结合 NTP 校准和逻辑计数器缓解了这个问题。
3.5 RGA(可增长复制数组)
RGA 是协同编辑的核心数据结构,它是 Op-based CRDT 的代表。每个字符或列表项关联一个由 (node_id, clock) 构成的唯一 identifier 和指向逻辑前驱的指针。
插入操作通过标识符排序确定位置,删除使用 tombstone 标记(延迟清理)。相比 CRDT 的 LSeq,RGA 在保持确定性的同时具有 O(1) 的插入复杂度,使其成为多数生产编辑器(如 Google Docs、Etherpad)的底层选择。
第四节:Sequence CRDT 的工程实现
4.1 因果树与分数索引
协同编辑中,Sequence CRDT 的设计始终在"位置无关"与"紧凑性"之间权衡。Path-copy 方案(如 Fugue)使用二叉树路径分配确定位置,保证唯一性但消耗分数空间。LSEQ 采用指数间隔分配策略减少了分裂次数。
分数索引(Fractional Indexing)是另一种思路:新项插在 a 和 b 之间时分配 (a+b)/2 作为位置索引,如 "a" 和 "b" 之间生成 "aV"。这种方法的优势是确定性排序和紧凑表示,边界情况需要 fallback(如 Index-rejection 的随机位扩展)。
4.2 Yjs 的工程精华
Yjs 是目前生产最广泛的 CRDT 库之一,支持 Y.Map、Y.Array、Y.Text、Y.XmlFragment 等数据结构,被 500+ 产品采用(如 JupyterLab、N对XTcloud)。
// Yjs 协同编辑示例
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const doc = new Y.Doc()
const wsProvider = new WebsocketProvider('ws://localhost', 'room-name', doc)
const yText = doc.getText('shared-editor')
// 所有客户端实时同步
yText.insert(0, 'Hello CRDT!')
// 离线支持
const update = Y.encodeStateAsUpdate(doc) // 导出增量状态
Y.applyUpdate(doc, remoteUpdate) // 应用远程更新
Yjs 的关键优化包括:state vector 用于高效同步(只传缺失的 updates)、deleteset 使用区间编码压缩、item-based 结构避免全树扫描。在浏览器中支持 IndexedDB 持久化,实现断线重连后的状态恢复。
第五节:向量时钟与因果一致性
CRDT 正确运行的前提是准确跟踪因果关系。传统的 Lamport 时钟只能表达 happened-before 关系,而向量时钟(Vector Clock)可以精确表达并发关系。
对于 N 个节点,向量是一个 N 维整数数组,每个节点维护自己的分量。发送消息时附带 VC,接收方合并时各分量取最大值。当 a <= b(全部分量 a[i] <= b[i])时,a 因果先于 b。
在 CRDT 工程实现中,VC 常被优化为以下紧凑形式:
- Dotted Version Vector(DVV):仅跟踪 session 相关时钟,大幅减少空分量
- Interval Tree Clock(ITC):用树结构编码因果历史,支持任意节点动态加入
- Dense Timestamp Scheme:基于 (replica_id, counter) 的 dot 机制,避免维护完整向量
第六节:生产实践与性能调优
6.1 带宽优化
State-based CRDT 随状态增长带宽消耗上升,主要优化策略:
- Delta-state CRDT:仅传播单次更新后的状态变化量(dot + 关联值),合并代价从 O(N) 降为 O(changes)
- Periodic full sync + Delta sync:类似 Git 的 fetch/rebase 模式,先全量同步打基础,再增量 delta 同步减少传输
- Tombstone GC:采用 causal stability 判断删除信息不再需要后回收,windowed 方案定期清理过期 tombstone
6.2 内存管理
长期运行的 CRDT 节点面临 tombstone 堆积、state vector 膨胀等问题。生产实践中的应对措施:
- Compaction:将 state 编码为紧凑的 version vector,在同步窗口内只保留 active dots
- Snapshotting:周期性保存全局快照,结合 diff 实现历史状态的快速恢复
- Weak reference Tombstones:对于确认因果稳定的弱引用 tombstone 允许 GC 回收
6.3 容错与分区恢复
网络分区场景下 CRDT 的行为:允许分区双方独立修改,恢复合并后自动收敛。但业务层可能不满足于"自动合并",引入conflict-aware UI提示用户处理语义冲突。例如 Figma 在检测到并发修改设计元素时,弹出面板让用户选择保留哪个版本。
第七节:前沿进展
CRDT + AI:将 CRDT 与大型语言模型结合实现多用户协作 AI 编程助手。Cursor、Copilot Workspace 等工具正在探索 CRDT 作为会话状态的共享数据结构,实现 peer-to-peer 的协同推理。
Permissioned CRDT:在分布式权限系统中引入能力令牌(capability tokens),在 CRDT 合并时动态检查写操作权限,实现安全协作。
CRDT over DAG:面向有向无环图的 CRDT 变体(如 Diamond Types),利用 DAG 结构高效处理深度嵌套的文档编辑操作,插入复杂度为 O(log N)。
小结
CRDT 源于理论计算机科学,却是解决分布式系统"一致性、可用性、分区容忍"矛盾的工程利器。从 G-Counter 到 Sequence CRDT,从 Yjs 到 Figma 的实时协作,CRDT 已成为现代分布式基础设施不可或缺的组成部分。
展望未来,随着 WebRTC、Edge Computing 的普及,client-side CRDT 将在去中心化协作应用中发挥更大作用。下一代 CRDT 研究将聚焦于:支持运行时类型演化的 schema-flexible CRDT、基于 Info-Theoretic Bound 的最优合并策略、量子通信场景下的容错 CRDT。
如果你正在开发需要多用户协作的产品,建议从 Yjs 起步,结合 y-protocols 和 y-websocket 构建原型,然后根据业务的并发特征定制优化。记住,选择 CRDT 方案的本质,是选择用数学优雅换取工程自由。

发表评论 取消回复