首页 › programming › 离线编辑为何永不冲突:CRDT解码

离线编辑为何永不冲突:CRDT 如何驯服并发

导语:一个看似不可能的承诺

想象一个场景:你和同事各自打开同一份设计稿、同一篇文档、同一块白板,然后同时在上面写写画画。更要命的是,你们俩的手机都断了网——一个在飞机上,一个在地铁里。几个小时后重新连上网,两份被各自改得面目全非的副本,竟然能自动合并成一份,而且一字不差、一条不丢、从不出错。

这听起来像魔法,但它正是你每天都在用的 Google Docs、Figma、Notion、飞书背后的核心技术难题。视频《How Collaborative Text Editors Don't Break》(PawelCodeStuff 频道)用一整期节目,把支撑这套魔法的底层数据结构——CRDT(Conflict-Free Replicated Data Type,无冲突复制数据类型)——拆得明明白白。

为什么这个视频值得每个开发者认真看一遍?因为它回答了一个困扰分布式系统三十年的根本问题:当多个副本各自演进、消息乱序到达、甚至永远无法对齐时间,我们凭什么还能保证最终一致? 这篇解读会带你从"为什么会冲突"出发,一路走到 CRDT 的数学内核(半格、交换律、幂等)、两大实现流派、最难啃的文本序列问题、工业级落地(Yjs/Automerge/Zed/Figma),以及它身上那些被刻意回避的代价。

视频背景卡片

项目 内容
视频标题 How Collaborative Text Editors Don't Break
频道 PawelCodeStuff(@pawel_code_stuff)
视频链接 https://www.youtube.com/watch?v=EL-VoBcUIJk
内容形式 深度技术讲解(长视频拆解)
核心主题 CRDT 如何让多人协同文本编辑在断网、乱序、并发下仍最终收敛、永不冲突
适合人群 前端/全栈工程师、分布式系统学习者、做协同功能的产品技术决策者
风格定位 代码级讲解 + 概念可视化,属于 CRDT 解读类在开发者社区广泛传播的代表

核心内容拆解

一、问题起点:并发编辑为何天然就会"打架"

视频的开场其实是在制造焦虑感,但说得极准:只要有"共享可变状态",就必然有"冲突"。

回到最朴素的情形。一份文档存在服务器上,A 和 B 同时拉取到本地。A 把第 3 行改成 "Hello",B 把第 3 行改成 "World"。两人各自把改动推回去——服务器该信谁的?

最朴素的答案是 Last-Write-Wins(最后写胜出):看时间戳,谁晚谁赢。但时间戳在分布式系统里是个谎言:两台机器的时钟根本不同步(NTP 误差可达几十毫秒,更别说时钟漂移和闰秒),而且即便时间戳一致,"晚"也不代表"对"。于是 B 的 "World" 覆盖了 A 的 "Hello",A 的输入悄无声息地蒸发了——用户毫无感知,数据已经丢了。

另一个朴素答案是加锁:谁先抢到编辑权,谁独占。但这直接摧毁了协同体验的根基——"两个人能同时在一个文档上工作"。Figma 早期就坚决反对加锁,因为设计的灵魂就在于实时看到对方的光标在动。

视频反复强调的一个点(这也是它最想破除的直觉):冲突不是 bug,冲突是并发的必然副产品。只要允许多个副本独立写、再异步同步,就一定会有"两个不同操作作用在同一份状态上"的时刻。与其试图消灭它,不如设计一种结构,让冲突在数学上根本不可能产生坏结果——这就是 CRDT 的全部出发点。

延伸分析:这个起点其实和 CAP 定理、PACELC 是一脉相承的。当网络可能分区(P)时,你要么牺牲可用性(C 一致性强但锁死),要么牺牲强一致性(A 高可用但允许临时分歧)。CRDT 选的是后者——拥抱弱一致,但承诺"最终一致(eventual consistency)",而且把"最终一致"从一个模糊的工程口号,升级成了一条可证明的数学定理。

二、旧答案 OT:为什么 Google Docs 那套不够用

在 CRDT 之前,业界主流是 OT(Operational Transformation,操作变换),1989 年由 Ellis 和 Gibbs 提出,后被 Google Docs、Google Wave 发扬光大。视频里把它作为"前辈"做了简短但精准的对比。

OT 的思路是:不去设计特殊的数据结构,而是去"变换"操作本身。当两个并发操作 $o_1$、$o_2$ 同时发生,定义一个变换函数 $T$,让 $o_1 \cdot T(o_1, o_2)$ 和 $o_2 \cdot T(o_2, o_1)$ 作用后的结果相同。简单说就是"我执行我的操作之前,先根据你刚做的操作把它修正一下",从而让两个本来顺序相关的操作,变得和执行顺序无关。

听起来优雅,但 OT 有两个致命弱点,视频点到即止,我帮你展开:

第一,变换函数 $T$ 极其容易写错。 经典论文里,光是两个操作(插入/删除)之间的变换,就要满足所谓的 $TP_1$(转换性质 1)和 $TP_2$(转换性质 2)两条数学性质。学界花了十几年才搞清楚,业界流传的很多 OT 实现其实是不满足 $TP_2$ 的——也就是说,在特定的并发组合下,副本之间会永久地分叉,再也合不回去。这就是为什么 Google Docs 早期论文 Implant(Jupiter 协议)要那么小心。

第二,OT 强依赖一个中心服务器来对所有操作排出一个全局总序。 这意味着它天生不适合 P2P、不适合长离线。你飞机上离线编辑三小时,重连后要把这三小时的操作"重放"并对每个并发操作做变换——变换函数的累积误差很容易爆炸。

CRDT 的革命性正在于此:它把"保证收敛"的责任,从脆弱的运行时变换算法,转移到了坚固的数据结构本身。 你不需要算对每一步变换,只要数据结构满足几条数学性质,收敛就被自动保证。

三、CRDT 的数学内核:交换、结合、幂等——以及"半格"

这是整期视频、也是整个 CRDT 理论的灵魂。我用尽量朴素的话讲清楚。

CRDT 的承诺可以浓缩成一句话:任意两个副本,无论经历过怎样乱七八糟的操作序列,只要它们收到过相同的操作集合,最终状态必然完全相同。

要做到这点,操作(或状态的合并)必须同时满足三条代数性质:

  1. 交换律(Commutative):$a \circ b = b \circ a$。先做 A 再做 B,和先做 B 再做 A,结果一样。消息乱序到达?无所谓。
  2. 结合律(Associative):$(a \circ b) \circ c = a \circ (b \circ c)$。操作分批到达、分组不同?无所谓。
  3. 幂等律(Idempotent):$a \circ a = a$。同一条消息由于网络重传到达两次?无所谓,做两遍等于做一遍。

这三条加在一起,意味着消息可以任意乱序、任意重复、任意丢失后重传,合并结果都不变。这对一个不可靠的异步网络来说,简直是量身定做。

视频里更妙的点(也是它"魔法感"的来源)是引入单调性(monotonicity)和半格(semilattice)的概念。把系统的每一个可能状态想象成空间里的一个点,合并操作就是"取两个点的最小上界(least upper bound,join)"。CRDT 保证:状态只会沿着半格"向上"走,永远单调增长,永远不回退。两个副本各自往上爬,它们爬过的点可能不同,但因为半格里任意两点都有唯一的最小上界,它们最终一定会爬到同一个点上——这就是"必然收敛"的数学保证。

延伸分析:这条单调性其实就是著名的 CAP 里的 AP + Eventual Consistency 的形式化。CRDT 用一个抽象代数结构(join-semilattice),把"最终一致"从一句工程师的祈祷,变成了一条可证明的定理。这是分布式系统里少见的"用数学把工程问题彻底解决"的范例。

四、两大流派:CvRDT(状态派)vs CmRDT(操作派)

视频在讲完数学后,转向工程实现,CRDT 分成两大流派:

状态派 CvRDT(Convergent,收敛型):每个副本只管维护自己的完整状态,同步时直接把整个状态发给对方,对方用一个满足交换/结合/幂等的 merge() 函数合并。代表:G-Counter、G-Set。优点是极其简单、极鲁棒——连消息顺序和重复都不用管,反正每次都是 merge(本地, 远端)。缺点是带宽开销大,每次同步要传整份状态。

操作派 CmRDT(Commutative,交换型):不传状态,只传操作(op)本身。op1: 在位置 3 插入 'x'、op2: counter++。优点是带宽极省,缺点是要求底层消息层提供因果序广播(causal broadcast)——也就是保证每个副本看到操作的顺序满足因果关系,且操作要满足所谓的"前条件(precondition)"。一旦消息层不可靠,操作派很容易出错。

视频给出的实用判断(也是工业界共识):简单的计数器、集合用状态派;高吞吐、对带宽敏感的用操作派;文本编辑这种大体量场景,通常用操作派或二者混合。

五、经典积木:从计数器到集合

理解 CRDT,必须动手过一遍它的"标准零件"。视频和配套代码里反复出现这几块:

这几块积木的共性是:它们都通过"把信息只增地累加"来回避删除和顺序问题。这是 CRDT 设计哲学的核心——绝不删除信息,只追加信息。

六、最难啃的骨头:文本序列 CRDT(视频的主战场)

前面都是热身,视频真正的标题"How Collaborative Text Editors Don't Break"指向的,是 CRDT 里最难、也最有商业价值的一类:序列/文本 CRDT。

为什么文本特别难?因为文本编辑的核心操作是"在中间插入"和"在中间删除"。经典做法用位置下标(在第 5 个字符后插入),但位置是会漂移的——A 在位置 5 插入一个字符的同时,B 也在位置 5 插入,合并后谁排前?而且只要前面有人插入,后面所有人的"位置 5"就全都错了。

视频的核心解法(也是 Yjs/Automerge/RGA 一脉的思路):彻底放弃位置下标,给每个字符分配一个不可变的、全局唯一的"身份标识(identity)",并用它的邻居关系来表达顺序。

具体来说,每个字符是一个节点,记录三件事:{id, value, leftOrigin}——它的唯一 id、它的字符内容、以及它"插在哪个字符的右边"。这样插入就变成"挂到某个已知节点之后",而不是"放到某个位置号"。两个并发插入同一个 leftOrigin 之后?用 id 的大小作为稳定的决胜规则(tie-breaker),于是无论消息怎么乱序,所有副本排出来的字符序列都完全一致。

这里还有两个视频强调、但很多人会忽略的坑:

第一,删除不是真删,是"墓碑化(tombstone)"。 因为并发,你删除一个字符的瞬间,可能别人正往它后面插字符。如果你真把节点删了,那个并发插入就"挂到了虚空",再也合不回来。所以 CRDT 文本里,删除只是把节点标记为"已删除"(tombstone),它仍然留在结构里维持拓扑。这直接导致了元数据膨胀——一份 100KB 的文档,CRDT 内部可能存了好几 MB 的墓碑和 id。

第二,垃圾回收(GC)几乎是无解的。 你什么时候能安全地清理一个墓碑?只有当你确信所有副本都已经看到了这次删除时。但在一个开放系统里(节点随时加入离开、P2P 对等),你永远无法知道"所有副本"是谁。这就是为什么 Yjs、Automerge 的 GC 都很保守,长期运行下内存和存储压力是个真实的工程难题。

视频用代码一步步实现了一个最小可用的文本 CRDT,这种"从零搓一个"的讲解方式,让那些抽象的 id、leftOrigin、tombstone 变得肉眼可见——这是它比纯概念视频高明的地方。

七、工业级落地:谁在用,怎么用的

理论再美,落不了地也是空中楼阁。CRDT 在 2015 年后真正爆发,靠的是几个关键工程库:

八、代价:CRDT 不是银弹

视频相对克制,没有大谈 CRDT 的缺点。我帮你补上这至关重要的一半:

  1. 元数据开销巨大:墓碑、唯一 id、版本向量(version vector),让 CRDT 状态往往膨胀数倍到数十倍。
  2. 垃圾回收是结构性难题:如前述,开放系统里几乎无法安全清理墓碑。
  3. 不是所有冲突都有"正确答案":两个用户把同一个文件改名成不同名字,CRDT 会诚实地给你两个文件——数学上收敛了,语义上却可能是 bug。语义冲突(semantic conflict)CRDT 救不了,得靠上层业务逻辑。
  4. CPU/内存成本:大文档的文本 CRDT 合并在客户端可能造成卡顿,需要精心的性能工程(这正是 Yjs、Zed 大量投入的地方)。
  5. 心智复杂度:调试一个并发合并不一致的 CRDT bug,比调试普通 bug 难一个量级。

知识结构图

graph TD
    A[并发编辑的根本困境<br/>多副本独立写 + 异步同步 = 必然冲突] --> B{如何保证最终一致?}

    B -->|旧答案: 变换操作| C[OT 操作变换<br/>Google Docs / Wave]
    C --> C1[弱点1: 变换函数T易写错<br/>不满足TP2则永久分叉]
    C --> C2[弱点2: 依赖中心服务器排总序<br/>不适合P2P/长离线]

    B -->|新答案: 设计结构| D[CRDT 无冲突复制数据类型<br/>把收敛责任从算法移到数据结构]

    D --> E[数学内核: 三条代数性质]
    E --> E1[交换律 Commutative]
    E --> E2[结合律 Associative]
    E --> E3[幂等律 Idempotent]
    E --> F[半格 Join-Semilattice + 单调性<br/>状态只增不减 → 必然收敛]

    D --> G{两大实现流派}
    G -->|传整份状态| H[CvRDT 状态派<br/>merge函数取逐元素max<br/>简单鲁棒 但带宽大]
    G -->|只传操作| I[CmRDT 操作派<br/>需因果序广播<br/>省带宽 但更复杂]

    D --> J[经典积木]
    J --> J1[G-Counter / PN-Counter]
    J --> J2[G-Set / 2P-Set / OR-Set]
    J --> J3[LWW-Register]
    J --> K[最难: 文本序列CRDT<br/>放弃位置 用唯一id+leftOrigin<br/>删除=墓碑 元数据膨胀]

    K --> L[工业落地]
    L --> L1[Yjs 生态最成熟]
    L --> L2[Automerge local-first]
    L --> L3[Zed 自研焊进编辑器]
    L --> L4[Figma 自研混合方案<br/>CRDT并非唯一答案]

    style D fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px
    style E fill:#fff3e0,stroke:#e65100
    style K fill:#fce4ec,stroke:#ad1457

实战启示

如果你是产品/应用开发者,要做协同功能: 第一反应不应该是"自己撸一套同步逻辑",而是直接评估 Yjs(前端富文本/画布协同事实标准)或 Automerge(强调本地优先、JSON 状态)。自己实现 OT 几乎必然踩 $TP_2$ 的坑,自己实现 CRDT 文本更是地狱难度。站在巨人的肩膀上,把精力放在业务语义而不是底层收敛证明上。

如果你在做离线优先(offline-first)应用: CRDT 是目前唯一在数学上能保证"断网随便改、重连自动合"的技术路径。Martin Kleppmann 的 local-first 宣言值得作为你的产品哲学——数据主权还给用户,服务器降级为可选项。笔记类、清单类、设计类工具尤其契合。

如果你在做分布式系统/数据库: CRDT 的心智模型(单调半格、幂等合并)远超"协同编辑"这一应用。多地域最终一致、边缘计算、P2P 网络、去中心化存储,凡涉及"弱一致但求最终收敛"的场景,CRDT 都是首选武器。Redis Active-Active、Riak、Cassandra 的计数器都是这套思想的体现。

认知升级——最重要的那一点: CRDT 教会我们的,不只是"如何做协同编辑",而是一种重新分配责任的工程哲学:当一个运行时问题总是出错,别去修运行时,去改数据结构,让错误在结构上变得不可能发生。 这和"用类型系统消灭空指针""用不可变数据消灭竞态"是同一种高级智慧——把正确性从"靠人小心"升级为"靠结构保证"。这种思维一旦内化,你看任何分布式系统的设计都会通透很多。

当然,也要清醒: 评估 CRDT 时务必先问三个问题——状态膨胀能否接受?语义冲突(如重命名)上层能否处理?目标场景是否真的需要最终一致而非强一致?不是所有协同都需要 CRDT,简单场景上一个乐观锁 + 中心服务器可能又快又省。

延伸学习路径

  1. 奠基论文:Marc Shapiro 等人 2011 年的 A Comprehensive Study of Convergent and Commutative Replicated Data Types(INRIA RR-7687)——CRDT 的"圣经",把状态派/操作派、所有积木、收敛证明一次讲透。
  2. 理念宣言:Kleppmann 等人的 Local-First Software: You Own Your Data(local-first.web)——理解 CRDT 为何重要的最佳背景读物,理想与现实的平衡讲得极好。
  3. 知识总站:crdt.tech ——CRDT 类型一览与库的导航,概念速查利器。
  4. 工程标杆博客:Zed 官方博客 How CRDTs make multiplayer text editing part of Zed's DNA——工业级自研 CRDT 的取舍与性能优化实战。
  5. 动手实践:Yjs 官方教程与 Demos、Automerge 的 quickstart——跑通一个最简单的多人协同 demo,胜过读十篇概念文章。

一句话总结

CRDT 用一个单调半格的代数结构,把"多副本最终一致"从一句工程祈祷变成了一条可证明的定理——它让离线、并发、乱序的协同编辑从"会崩"变成"不可能崩",代价是你必须接受"信息只增不删"的膨胀与克制。


参考来源

  1. How Collaborative Text Editors Don't Break — PawelCodeStuff — 本文解读的核心视频
  2. How CRDTs make multiplayer text editing part of Zed's DNA — Zed Blog — 工业级自研文本 CRDT 的取舍与实现
  3. About CRDTs — crdt.tech — CRDT 类型一览与生态库导航
  4. How Figma's multiplayer technology works — Figma Blog — 自研非纯 CRDT 协同方案的真实工程权衡

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入