一、为什么需要 CRDT — 从 CAP 定理到最终一致性的困境
在分布式系统的设计权衡中,CAP 定理告诉我们,网络分区(Partition Tolerance)不可避免的一致性(Consistency)与可用性(Availability)的取舍是永久性的。当网络出现分区时,传统的强一致性协议如 Paxos、Raft 会牺牲可用性来保证一致性——这意味着在网络恢复之前,部分节点将无法响应写入请求。
然而,在许多互联网场景中,系统可以容忍短暂的不一致,只要最终能达到一致状态。这就是最终一致性(Eventual Consistency)的出发点。但传统的最终一致性依赖于读修复(Read Repair)、反熵(Anti-Entropy)或向量时钟(Vector Clocks)来检测冲突,而冲突解决往往需要应用层介入甚至人工干预。
CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)的出现改变了这一格局。CRDT 是一类精心设计的数据结构,使得在任意网络条件下,多个副本独立执行的操作无需协调即可自动收敛到相同的确定性状态。它不需要向量时钟来检测冲突,不需要锁机制来保证一致性,也不需要应用层来编写冲突解决逻辑——收敛是数学保证的。
从 Google Docs 的实时协作编辑,到 Riak KV 数据库中的数据类型,再到 Figma 的多人设计工具,CRDT 已经成为构建高可用、低延迟、离线优先(Offline-First)应用的基石技术。
二、CRDT 数学基础 — 半格(Semilattice)、偏序集与单调收敛
CRDT 的理论根基植根于序理论(Order Theory)和格理论(Lattice Theory)。理解这些数学概念是掌握 CRDT 设计原理的关键。
2.1 偏序集(Partially Ordered Set, Poset)
偏序集是一个集合 P 配备一个二元关系 ≤,满足:
- 自反性:∀a ∈ P, a ≤ a
- 反对称性:若 a ≤ b 且 b ≤ a,则 a = b
- 传递性:若 a ≤ b 且 b ≤ c,则 a ≤ c
在 CRDT 中,数据状态构成偏序集,操作为状态转移。当且仅当两个状态可比时,我们可以确定性地判断它们之间的新旧关系。
2.2 半格(Join Semilattice)
半格是一种特殊的偏序集,其中任意两个元素都有最小上界(join ∨)。CRDT 要求所有副本的状态构成一个 join-semilattice,并且每个操作的效果对应一个单调递增的状态转移。
关键是:join 操作必须满足交换律、结合律和幂等律。这意味着无论操作以何种顺序到达,最终 join 的结果都相同——这就是无冲突的数学本质。
2.3 单调收敛与最终一致性
如果一个状态空间 (S, ≤) 满足:
- 单调递增性:每个操作使状态在偏序中单调递增
- 合并操作是 join 半格的 join 运算
那么所有副本在接收到相同操作集后,都会收敛到相同的状态——即半格的 join(所有状态)。
三、两大流派:状态基 CvRDT vs 操作基 CmRDT
CRDT 分为两大类型,它们在通信模式、网络假设、收敛保证和生产适用性方面有本质区别。
3.1 状态基 CRDT(CvRDT)
状态基 CRDT 通过同步完整的状态数据来实现收敛。发送方将其当前状态传输给接收方,接收方通过 join 操作合并。
通信模型:可靠因果序广播或 gossip 反熵传播。
合并语义:state_merged = state_local ∨ state_remote(半格 join)
优点:幂等性、交换律天然容忍消息丢失重复,新节点可直接获取全量状态。
缺点:传输完整状态的开销可能很大,即使只修改了少量数据。
3.2 操作基 CRDT(CmRDT)
操作基 CRDT 不传输状态,而是传输操作本身。每个操作在任何顺序下执行都产生相同最终状态。
通信模型:可靠因果序广播,必须去重、保持因果序、保证最终送达。
优点:每次只传输增量操作,网络开销低,适合高频率更新场景。
缺点:依赖底层广播的因果序保证。”消息丢失需额外检测修复机制。
3.3 两种流派的选择
实践中两者常结合使用:CmRDT 用于实时操作传播(低延迟),CvRDT 用于反熵同步(gossip 全量状态)作为恢复后端。例如 Yjs 的 Y.Text 使用基于操作的更新,同时通过状态向量进行 CvRDT 式的差异同步来弥补丢失的更新。
四、核心数据结构深度解析
4.1 Last-Write-Wins Register (LWW-Register)
LWW-Register 是最简单的 CRDT,通过时间戳决定哪个写入是新的。每个写入携带逻辑时间戳 (value, timestamp, node_id)。join 操作选择 timestamp 较大的状态,时间戳相同时按 node_id 决出高者(tiebreaker)。
适用场景:键值缓存、配置管理、购物车最后修改优先场景。
局限:丢失并发写入,不适用于需要保留所有历史的场景。
4.2 G-Counter(增长计数器)
G-Counter 只支持递增操作的分布式计数器。每个节点维护独立计数分量,join 为各分量取最大值。
状态:向量 [p1, p2, ..., pn],递增:节点 i 将 pi 加 1,查询:value = Σ pi,合并:pi_merged = max(pi_local, pi_remote)。
这是经典的 join-semilattice:(max, vector) 运算满足结合律、交换律、幂等律。
4.3 PN-Counter(正负计数器)
PN-Counter 由两个 G-Counter 组合而成:P 用于递增,N 用于递减。最终值为 value = ΣP - ΣN。
适用场景:分布式投票系统、库存扣减计数、社交网络关注/取关计数。
4.4 OR-Set(Observed-Remove Set)
OR-Set 解决 Set 中并发添加/删除的冲突问题。为每个元素关联一组唯一种子(tag),添加操作生成新种子,删除操作将当前所有种子移入墓碑集集(tombstone set)。
查询:元素 e 存在当且仅当有存活的种子不在 tombstone 集合中。合并:tombstones = tombstones_l ∪ tombstones_r,adds = adds_l ∪ adds_r。
适用场景:实时协作中的元素级操作集、分布式文件系统的元数据管理。
4.5 LWW-Element-Set(带时间戳的元素集合)
对每个元素的添加和删除时间戳进行跟踪。查询时,如果 add_time 大于 remove_time,则元素存在(add-wins 语义)。
适用场景:购物车项管理、CRUD 应用中的对象级冲突解决。
4.6 MV-Register(多值寄存器)
MV-Register 在遇到并发写入时保留所有并发版本,由应用层决定如何呈现。
适用场景:协同文本编辑中对富文本属性的并发标记。
4.7 序列 CRDT(Sequence CRDT)
序列 CRDT 是协同编辑场景中最具挑战性的数据类型。需在字符级别实现无冲突的插入和删除。
主要算法对比:
- Logoot:为每个字符分配唯一位置标识符(整数列表+边界策略),确定性但浪费 ID 空间。
- LSEQ:Logoot 的改进版,使用多叉树分配位置标识符,动态选择基数减少 ID 空间增长。
- RGA(Replicated Growable Array):基于因果树,通过因果依赖排序插入顺序。
- YATA(Yet Another Transformation Approach):Yjs 的底层算法。为每个节点记录 origin 和 rightOrigin,并发插入时基于左邻居 ID 确定序序。
五、主流 CRDT 库对比
5.1 Yjs
Yjs 是目前生产中使用最广泛的 CRDT 库,使用 YATA 序列 CRDT 算法,提供高性能协同文本编辑和多种数据类型。
核心特性:
- Y.Doc 分布式文档容器,支持深层嵌套的 Y.Map、Y.Array、Y.Text、Y.XmlFragment
- 二进制编码格式(lib0 编码),效率高
- 状态向量(State Vector)机制:先交换状态向量,只传输缺失的操作
- UndoManager、Awareness(光标/选区/用户状态)
- 丰富 provider:y-websocket、y-webrtc、y-indexeddb、y-dat 等
同步协议:
- 客户端连接时发送 Sync Step 1(含状态向量)
- 对方计算差异,返回 Sync Step 2(缺失的更新)
- 后续增量通过 Update 消息传播
5.2 Automerge
Automerge 由 Ink & Switch 实验室开发,使用纯 JSON 数据模型,任意 JSON 值都可直接作为文档内容。
核心特性:
- JSON 原生 API:doc.counter = 1、doc.list.push(item) 直接映射到 CRDT 操作
- Automerge 2.0 引入 Rust 后端通过 WASM 提升性能
- 完整操作历史日志和 fork/merge 语义
与 Yjs 的区别:Automerge 的 JSON API 更直观;Yjs 在大量并发操作时通常性能更好,且 Yjs 生态更完整。
5.3 工业级应用框架
Cola:构建实时协作应用的全栈框架。Zero:分布式 CRDT 数据库。
六、实战:构建实时协同编辑器
6.1 使用 Yjs + y-websocket 搭建协同编辑器
完整的生产框架包含三个核心部分:WebSocket 服务器、浏览器客户端持久化、以及网络优化。
服务器端(Node.js):
const { WebSocketServer } = require('ws')
const { setupWSConnection, setPersistence } = require('y-websocket/bin/utils')
const { LeveldbPersistence } = require('y-leveldb')
// 持久化层:文档历史保存到 LevelDB
const ldb = new LeveldbPersistence('./yjs-storage')
setPersistence({
bindState: async (docName, ydoc) => {
const persistedYdoc = await ldb.getYDoc(docName)
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc))
},
writeState: async (docName, ydoc) => {
// 批量写入优化
}
})
const wss = new WebSocketServer({ port: 1234 })
wss.on('connection', setupWSConnection)
浏览器客户端:
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { yCollab } from 'y-codemirror.next'
import { EditorState } from '@codemirror/state'
import { EditorView, basicSetup } from 'codemirror'
const ydoc = new Y.Doc()
const wsProvider = new WebsocketProvider(
'wss://example.com', 'doc-room-1', ydoc
)
const ytext = ydoc.getText('codemirror')
const undoManager = new Y.UndoManager(ytext)
// Awareness(光标位置、用户信息)
wsProvider.awareness.setLocalStateField('user', {
name: 'Alice', color: '#30bced'
})
const state = EditorState.create({
doc: ytext.toString(),
extensions: [basicSetup, yCollab(ytext, wsProvider.awareness)]
})
const view = new EditorView({ state, parent: document.body })
6.2 网络架构优化
- 传输层:WebSocket 中央服务器模式做为主路径,WebRTC(y-webrtc)做为 P2P 加速路径;弱网下自动回退。
- 分区合并:状态向量差异计算 + gossip 反熵定时同步(每 30-60 秒)。
- 横向扩展:部署 Redis 后端同步服务器间状态,实现跨实例客户端协作。
6.3 生产级部署注意事项
- 持久化:y-leveldb 或 y-redis 存储,每 N 次操作做一次 baseline snapshot,结合增量更新。
- 垃圾回收:Yjs 的 GC 通过 tombstone fencing 机制实现:只有当 tombstone 操作已被所有已知副本接收后才可清除。
- 一致性验证:在不同拓扑下执行相同操作序列,比较最终二进制状态哈希(Ylib lib0 编码具备确定性)。
- 监控:Prometheus 观测同步延迟、传输文档大小、tombstone 数量等。
七、CRDT 在数据库与存储系统中的应用
7.1 Riak KV 中的 CRDT
Riak KV 是 CRDT 在生产数据库中最早落地的案例之一,实现了 PN-Counter、OR-Set、嵌套 CRDT Map 等原生数据类型。通过 gossip 协议传递操作和反熵消息,在 AP 系统中提供最终强致性保证。
7.2 Redis Enterprise CRDT
Redis Enterprise Active-Active Geo-Distribution 基于 CRDT 实现:
- LWW Register:单键级别 last-write-wins 冲突解决
- SET CRDT:OR-Write Set 语义
- Hash CRDT:字段级别细粒度冲突解决
- Counter CRDT:PN-Counter 全局计数
开发者在应用层无需关心冲突解决逻辑——数据库层面自动处理。
7.3 边缘计算中的 CRDT
IoT 和边缘计算场景中,设备经常断网。CRDT 实现离线优先:设备离线时缓存操作,重连后自动同步;多边缘节点间通过 gossip 传播;云端聚合全局状态。Automerge 可直接在浏览器/移动端运行,配合 IndexedDB + Service Worker 实现完整离线优先架构。
八、性能调优与可观测性
8.1 同步协议优化
- 状态向量差异化同步:每次连接交换状态向量,只传输缺失操作。复杂度从 O(总操作数) 降低到 O(缺失操作数)。
- 二进制编码:Yjs lib0 编码每个操作通常小于 100 字节。
- 批量更新:多操作打包为一个 Update 消息,减少 RTT 和网络开销。
8.2 垃圾回收策略
Tombstone 持久化是 CRDT 设计的关键取舍:避免并发删除-重新添加场景下的 ID 冲突问题,但带来无限状态膨胀。生产环境需设定 GC 触发条件和监控 tombstone 数量。Automerge 在每次 sync 后自动 compact 减少冗余历史。
8.3 内存优化
- 近期操作保留在内存,远期操作压缩为 baseline snapshot
- Delta 存储(存储状态 diff 而非原始操作)
- 共享存储前缀(相同 tag 集合跨副本共享)
8.4 Prometheus 监控指标
- crdt_sync_latency_seconds:同步延迟直方图
- crdt_update_bytes_total:每次同步传输字节数
- crdt_state_size_bytes:当前文档状态大小
- crdt_gc_duration_seconds:GC 耗时
- crdt_tombstone_count:tombstone 数量观测
- crdt_conflict_total:应用层冲突计数
九、CRDT vs OT(Operational Transformation)
OT 与 CRDT 是实现协同编辑的两大技术路线。
OT:依赖 transform 函数重新排序操作,需要中央服务器保证全序,离线支持较弱,transform 函数需覆盖所有操作组合(O(n²) 增长),代表系统:Etherpad、Google Docs(旧版)、ShareDB。
CRDT:无需中央服务器,天然支持 P2P/去中心化,原生支持离线优先,数据结构复杂但组合性好,有 tombstone 内存开销,代表系统:Yjs、Automerge、Figma、Notion、Zed。
当前趋势:Figma、Notion、Zed 等新一代协作工具选择 CRDT。Google Docs 的最新版本据报道也迁移到 CRDT 方案。CRDT 在去中心化和离线优先场景的优势使其成为更通用的选择。
十、总结与展望
CRDT 从纯数学理论到生产级应用已走过二十多年。核心结论:
- 理论基础坚实:半格、偏序集、交换律/结合律/幂等律保证收敛的数学确定性。
- 两大类型互补:状态基 CvRDT 适合反熵同步,操作基 CmRDT 适合实时增量传播,两者常结合使用。
- 序列 CRDT 最复杂:YATA(Yjs)、RGA、Logoot 各有所长,Yjs 在性能和生态上目前领先。
- 生产生态成熟:Yjs + provider 组合已成为实时协作应用的事实标准。
- 数据库融合:Riak KV、Redis Enterprise 将 CRDT 作为原生数据类型提供。
前沿展望:
- 分布式计算:CRDT 与 Actor 模型结合,实现去中心化计算协调层
- 形式化验证:Coq/Isabelle 对 CRDT 实现进行机器证明(VeriFx 等已有工作)
- AI + CRDT:多人 AI 协作工作流的实时同步与版本追踪
- WASM + CRDT:通过 WebAssembly 实现跨语言 CRDT 运行时
- 协议标准化:推动 CRDT 编码格式和同步协议标准化,实现跨应用互操作
掌握 CRDT,意味着拥有打开高可用、离线优先、去中心化实时协作系统的钥匙。在 Web 3.0 和边缘计算的时代浪潮中,CRDT 正从高级技巧演变为必备基础。

发表评论 取消回复