引言

在分布式系统中,多个节点同时修改同一份数据是不可避免的常态。传统方案(如分布式锁、两阶段提交)要么牺牲可用性,要么依赖协调者的单点决策。CRDT(Conflict-free Replicated DataType,无冲突复制数据类型)提供了一条完全不同的路径:通过数学结构设计,使并发更新在任何顺序下都能自动合并,无需协调、无需锁、无需冲突解决。

从 Google Docs 的实时协作到 Riak 数据库的分布式计数器,从 Apple Notes 到新一代的协作文档工具 Figma、Notion,CRDT 正在重新定义"实时协作"的技术底座。本文将从理论根基出发,深入拆解 CRDT 的两大类别(State-based 和 Operation-based),分析核心数据结构的实现原理,并以工业落地案例收尾。

一、理论基础:为什么 CRDT 能保证一致性

1.1 偏序集与 Join Semi-Lattice

CRDT 的理论根基建立在偏序集(Partially Ordered Set)和 Join 半格(Join Semi-Lattice)上。一个 CRDT 的状态空间构成一个 Join 半格,即任意两个状态之间存在唯一的最小上界(Least Upper Bound, LUB)。

形式化表述:设状态空间为 \((S, \leq)\),对任意 \(a, b \in S\),存在唯一的 \(c = a \sqcup b\)(称为 join),使得:

  • \(a \leq c\) 且 \(b \leq c\)(上界)
  • 对任意 \(d\),若 \(a \leq d\) 且 \(b \leq d\),则 \(c \leq d\)(最小性)

这个性质保证了:无论两个副本以何种顺序收到彼此的状态,merge 操作的结果都相同。这正是 CRDT 无需协调即可收敛的数学根源。

1.2 单调增长与非递减性

CRDT 的核心洞察是:只允许信息增长。状态变化被设计为单调递增的(在偏序关系下),永不回退。这避免了传统系统中"撤销"和"回滚"带来的复杂性。删除操作不是真正移除数据,而是通过标记(tombstone)来记录意图。

1.3 强最终一致性(Strong Eventual Consistency, SEC)

CRDT 提供的收敛保证称为强最终一致性:

  • 最终性:经过有限时间后,所有活跃副本都会收到所有更新
  • 强收敛:任何两个收到相同更新集合(顺序可不同)的副本,状态完全相同
  • 无需协调:更新可在本地立即应用,无需与任何其他节点协商

二、State-based CRDT( CvRDT ):以状态换简洁

2.1 架构模型

State-based CRDT(也称为 Convergent Replicated Data Type, CvRDT)通过传播完整状态实现同步。每个节点维护自己的状态副本,周期性地(或基于事件触发)与其他节点交换状态并执行 merge。

同步原语:Join操作。\(S_{merged} = S_{local} \sqcup S_{remote}\)。由于 join 满足交换律和结合律,传输顺序不影响最终结果。

2.2 G-Counter(增长计数器)

G-Counter 是最简单的 CRDT 类型,只支持递增操作。其核心思想:为每个节点维护独立的计数槽,最终值为所有节点计数值之和。

// G-Counter 内部表示:数组 P[node_id] = count
class GCounter {
    constructor(nodeId) {
        this.nodeId = nodeId;
        this.P = {};           // 每个节点的计数器
        this.P[nodeId] = 0;
    }
    
    increment() {
        this.P[this.nodeId] += 1;
    }
    
    query() {
        return Object.values(this.P).reduce((a, b) => a + b, 0);
    }
    
    // 接收远程状态的 merge(逐元素取最大)
    merge(remoteP) {
        for (const [nid, count] of Object.entries(remoteP)) {
            this.P[nid] = Math.max(this.P[nid] || 0, count);
        }
    }
}

正确性证明:merge 取各分量最大值,满足 join 半格的交换律与结合律。由于各节点只增加自己的计数值,任意副本的各分量值单调递增,最终所有副本的状态向量在各分量上一致。

2.3 PN-Counter(正负计数器)

PN-Counter 将两个 G-Counter 组合:一个记录增量(P),一个记录减量(N)。最终值 \(v = \sum P_i - \sum N_i\)。

这种设计将"减法"转化为"增加减量计数器,再相减",保持了每个组件的单调递增性。代价是存储翻倍,但保证了数学优雅。

2.4 G-Set(增长集合)

G-Set 只支持添加,不支持移除。merge 即集合并操作。这是最直觉的 CRDT,没有任何复杂机制,但用途有限(如收藏夹、标签添加)。

2.5 2P-Set(双阶段集合)

2P-Set 在 G-Set 基础上添加一个墓碑集合(Remove-Set)。添加元素进入 A(Added),移除元素时将其加入 R(Removed)。最终有效集合为 \(A \setminus R\)。注意:元素一旦移除(进入 R),无法重新加入(这就是"two-phase"的含义)。使用不便但实现极简。

2.6 OR-Set(Observed-Remove Set)

OR-Set 解决了 2P-Set 的问题:每个元素携带唯一标签而非依赖等价性。添加时为每个元素生成全局唯一 tag(通常是 node_id + counter),移除时记录"我观察到的所有 tag"。并发添加+移除时,新 tag 因未被观察到而保留——实现语义上更合理的"最后写入胜出"或"添加胜出"策略。

2.7 LWW-Element-Set(最后写入胜出集合)

LWW-Element-Set 为每个元素维护时间戳。merge 时取时间戳更大的版本。虽然正确性依赖时钟同步,但实现简单、文档清晰,适合对时钟偏差不敏感的协作应用。

三、Operation-based CRDT( CmRDT ):以操作换效率

3.1 架构模型

Operation-based CRDT(Commutative Replicated Data Type, CmRDT)传播的是操作(op)而非状态。要求:

  • 因果一致性广播:下层通信层保证操作按因果顺序交付(causal delivery)
  • 交换律:并发操作的执行顺序不影响最终结果
  • 幂等性:同一操作不应被执行两次

优势:无需传递完整状态,带宽效率高(只需发送操作)。劣势:依赖因果序传输层(如向量时钟、Lamport 时钟)。

3.2 Op-based Counter

最简单形式:广播 inc 和 dec 操作(每种各一条)。由于加法和减法天然交换,任意执行顺序结果相同。但实践中几乎没有意义(不如 G-Counter 灵活)。

3.3 Op-based Set with Unique Tags

与 OR-Set 类似,但 add/remove 作为因果广播的操作传递。每个 add 附带唯一 tag,remove 携带"观察到的 tag 集合"。更高效的传输换来与 State-based 等价的偏序语义。

四、进阶结构:List/Sequence CRDT

协作文档最核心的需求是有序列表。如何在分布式环境下维护一个各节点的列表一致?这是 CRDT 研究中最具挑战性的领域。

4.1 Logoot 与 LSeq

Logoot(2006)是最早的有序列表 CRDT 之一。核心:为每个字符/元素分配一个位置标识符(Position Identifier),标识符为带优先级的路径列表。插入时生成介于两个现有位置之间的新标识符,永不冲突。

LSeq 进一步优化了位置标识符的密度问题。将标识符组织为树结构,根据密度决定添加深度或分裂,有效控制了标识符膨胀。

4.2 RGA(Replicated Growable Array)

RGA 基于时间戳链表思想。每个插入产生一个唯一的节点 ID(基于 Lamport 时钟 + nodeId),元素形成一棵基于因果关系的"树"。通过右兄弟指针维护兄弟间的顺序。RGA 在实践中被 TiKV 和不少原型系统采用。

4.3 YATA 与 Yjs 框架

Yjs 是最流行的开源 CRDT 库,基于 YATA(Yet Another Transformation Approach)算法。Yjs 的创新点:

  • 将冲突解决建模为关联关系图而非树
  • 提供 Y.Text(文本)、Y.Array(数组)、Y.Map(映射)等高阶抽象
  • 高效的双向操作编码(支持 OT 与 CRDT 混合场景)
  • 性能测试:支持百万级操作/秒的处理能力

Yjs 已被集成到 120+ 个开源应用中,是工业级 CRDT 的事实标准。

4.4 Automerge 与 Columnar Encoding

Automerge 采用列式存储(Columnar Encoding)优化压缩。独特的设计:

  • 将文档操作流按列(作者、操作类型、目标、值)而非行存储
  • 压缩率极高:一个百万次编辑的文档可被压缩到几 MB
  • 支持完整的二进制 CRDT 文档格式(.automerge 格式)

五、案例研究与工业落地

5.1 Riak 与 Cloud Types

Basho 的 Riak KV 是最早全面部署 CRDT 的工业数据库。提供的 Cloud Types 包括:G-Counter、PN-Counter、G-Set、OR-Set、Flag(布尔型)、Register(最后写入胜出寄存器)、Map(嵌套字典)。

5.2 Apple Notes 的离线协作

Apple Notes 使用 CRDT(具体基于 OR-Set 和 LWW-Register 的组合)帮用户在 iPhone、iPad、Mac 间无缝同步笔记。离线编辑后恢复网络时自动合并,用户使用无需关心。

5.3 Figma 的多用户向量编辑

Figma 的设计文件基于 CRDT 允许数十用户同时编辑相同画布。图形对象的属性用 Map CRDT 表示,图层顺序用 Sequence CRDT 维护。即使一个用户在另一个用户的屏幕上,同时拖拽、缩放、编辑颜色——最终状态总能一致。

5.4 Neon Database 的 Serverless Postgres

Neon 在其 serverless Postgres 的分支合并中使用 CRDT 概念来维护计算节点间的状态一致性,特别是模式和权限信息的跨分支同步。

5.5 Phoenix LiveView 与 CRDT

Phoenix 框架的 LiveView 通过 CRDT 来实现跨多个 WebSocket 连接的状态广播,确保所有客户端看到一致的界面状态——即 Elixir 的"实时 UI"建筑块。

六、设计选择与权衡

6.1 State-based vs Operation-based

维度State-based (CvRDT)Operation-based (CmRDT)
传输内容完整状态(含 tombstone)操作(小数据量)
依赖要求仅需可靠传输需因果序广播
收敛保证无条件收敛需因果 + 幂等
存储开销高(需留存历史)低(可压缩后GC)
实现复杂度低高(因果跟踪)
适用场景节点少、写频率高节点多、细粒度更新

6.2 Tombstone 问题与 GC 策略

State-based CRDT 的核心痛点是 tombstone 堆积。解决方案包括:

  • 版本向量截断:当确信所有副本都已同步后,丢弃旧 tombstone
  • Delta-CRDT:只传播变更增量而非完整状态
  • 因果稳定性检测:利用向量时钟判断哪些操作已被所有副本稳定接收

6.3 正确性证明与验证工具

CRDT 的正确性不能靠 review 保证,需要形式化方法:

  • TLA+:Amazon 和 Microsoft 常用 TLA+ 证明 CRDT 的收敛性
  • Iris(Coq框架):专为并发程序验证设计的分离逻辑框架
  • CRDT验证工具:calf(Coq 库)提供 CRDT 正确性的形式化验证

七、CRDT 与 OT 的对比

OT(Operational Transformation)是另一种经典的协作编辑解决思路(如 Google Docs、Etherpad 早期使用)。DT 与 OT 的对比是分布式系统常青话题:

  • OT:智能但复杂。需要变换函数保证:当两个并发操作放在一起时,后者需要被"变换"以容纳前者。工程上,变换函数的正确性证明极为困难。Google 的 Jupiter 变体虽有改良,但复杂度仍高。
  • CRDT:简单但"暴力"。用数据结构设计的数学性质天然避免冲突。无需中央协调器,无需复杂的变换逻辑。代价是状态(特别是 tombstone)会增长。
  • 结论:随着 CRDT 理论和工程优化( delta-CRDT、高效的编码方案),CRDT 正成为实时协作的首选。Yjs、Automerge 等新一代 CRDT 库在性能和存储上已基本追平甚至超越 OT 实现。

八、动手实践:从零实现一个 Counter CRDT

下面以 Go 语言为例,完整演示一个生产级的 PN-Counter CRDT:

package crdt

import "sync"

// PNCounter 表示一个带正(increment)和负(decrement)的分布式计数器
type PNCounter struct {
    mu sync.RWMutex
    id string           // 节点标识
    P  map[string]int64 // 各节点的增量
    N  map[string]int64 // 各节点的减量
}

func NewPNCounter(nodeID string) *PNCounter {
    return &PNCounter{
        id: nodeID,
        P:  map[string]int64{nodeID: 0},
        N:  map[string]int64{nodeID: 0},
    }
}

func (c *PNCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.P[c.id]++
}

func (c *PNCounter) Decrement() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.N[c.id]++
}

func (c *PNCounter) Value() int64 {
    c.mu.RLock()
    defer c.mu.RUnlock()
    var pSum, nSum int64
    for _, v := range c.P { pSum += v }
    for _, v := range c.N { nSum += v }
    return pSum - nSum
}

func (c *PNCounter) Merge(other *PNCounter) {
    c.mu.Lock()
    defer c.mu.Unlock()
    // 逐节点取最大值(join 半格)
    for nid, v := range other.P {
        if cur, ok := c.P[nid]; !ok || v > cur {
            c.P[nid] = v
        }
    }
    for nid, v := range other.N {
        if cur, ok := c.N[nid]; !ok || v > cur {
            c.N[nid] = v
        }
    }
}

// Marshal 返回状态的序列化表示(用于网络传输)
func (c *PNCounter) Marshal() ([]byte, error) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    // JSON 编码(生产环境常用 Protobuf)
    type state struct {
        P map[string]int64 `json:"p"`
        N map[string]int64 `json:"n"`
    }
    return json.Marshal(state{P: c.P, N: c.N})
}

func (c *PNCounter) Unmarshal(data []byte) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    var s state
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    remote := &PNCounter{P: s.P, N: s.N}
    remote.id = c.id
    c.Merge(remote)
    return nil
}

九、前沿展望

CRDT 研究方向的前沿正快速演进:

  • CRDT 与 AI 结合:利用 LLM 辅助冲突语义判断(尤其在文本融合领域)
  • 3D/多媒体 CRDT:从文本 CRDT 扩展到 3D 场景树、音视频时间线
  • 跨协议互操作:CRDT as a Service(CaaS)——浏览器、移动端、IoT 设备统一协作引擎
  • 量子安全 CRDT:将因果广播与后量子通信协议集成
  • 移动端 CRDT:Resoult(原名 Automerge-repo)正在推进移动端 / 嵌入式 CRDT 运行时

结语

CRDT 将"民主化一致性"的理想变为现实:节点平等,不靠锁,不靠协调,仅凭数据结构本身的数学性质走向一致。从学术理论的 Join 半格,到 Figma、Notion、Apple Notes 的百亿级协作场景,CRDT 证明了——好的数学结构能直接映射为好的工程。

理解 CRDT,不仅需要掌握其形式化模型,更需要在实践中领悟"单调增长"与"收敛性"的哲学。下一个五年,CRDT 将从协作编辑的利基走向通用分布式状态管理的基础设施——而今天正是理解它的最佳时刻。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部