这是【数据库研究前沿】系列的第 22 篇。本文把两条看起来不在同一条线上、但本质上在回答同一个问题的研究重新拼在一起:什么样的分布式数据管理可以不协调(coordination-free)。这个问题在过去十年反复被工程实践重新提出,每次以略有不同的面目出现——跨区域多写、本地优先(local-first)应用、协作编辑器、全球化计数、边缘侧同步——但骨架都一样。
一条线来自 UC Berkeley 的 Hellerstein 与合作者:2010 年 CIDR 的 The Declarative Imperative 提出 CALM 猜想,2020 年 CACM 正式把它写成 CALM 定理——一个分布式程序可以不协调地计算等价于集中式语义的结果,当且仅当它是单调的(monotonic)。围绕这条线长出来的是 Bloom/BloomL 编程语言、Dedalus 时序逻辑、以及近几年的 Hydro(Hydroflow、Hydroflow+)编译器与 VLDB 2025 的 Keep CALM and CRDT On(Laddad、Power 等)。
另一条线来自 INRIA / Nova LINCS 的 Shapiro、Preguiça、Baquero、Zawirski 2011 年的 Conflict-free Replicated Data Types:先识别满足半格(semilattice)结构的状态,再用”合并是 join”把多副本的冲突消灭在代数层面。十五年过去,CRDT 从论文里的 G-Counter、OR-Set 走到了 Yjs、Automerge 这种驱动真实多人协作编辑器的工业库,也走进了 Riak、Redis Enterprise、Azure Cosmos DB 这类商用数据库的功能清单。
两条线在 2024–2025 年开始显著地合流。原因很简单:CRDT 回答了”当你决定不协调时,数据结构该怎么设计”;CALM 回答了”什么时候你可以不协调”。前者是充分性的工程实现,后者是必要性的理论刻画。本文上半回到理论与综述层(CALM、单调性、CRDT 分类、Bloom/Hydro、VLDB 2025 新贡献);下半转到工程落地(Yjs、Automerge、Riak DT 的取舍,以及业务侧如何在设计评审时判断”这段逻辑能不能无协调”)。
版本说明 本文写作窗口为 2026 年 Q2:Yjs 13.x、Automerge 2.2.x、Riak 3.2(已归档)、Hydroflow 0.7 系列、Materialize 0.120 系列。论文以 CIDR 2010、PODC 2011、CACM 2020、VLDB 2023–2025 已公开版本为准;对无法从公开渠道确认的细节,本文标注”(待核实)“。
一、为什么又要谈 CALM 与 CRDT
1.1 协调是分布式系统里最贵的那一项开销
任何一个做过跨区域多写(multi-master)的团队都知道:机器故障不是最贵的,协调才是。Paxos、Raft、2PC、串行化快照隔离(SSI)——这些协议的可用性模型看起来很不同,但它们有一个共同的成本结构:
- 至少一个 RTT 的网络等待;
- 对慢节点与分区敏感(等不到 ACK 就不能前进);
- 每次协调都让系统回落到”最慢节点决定吞吐”的模型。
这也是 PACELC(Abadi 2012)能从 CAP 里把”正常运行期也要权衡 latency 与 consistency”这条细线单独拎出来的原因:即使没有分区,协调本身就是一笔 tax。
1.2 coordination-free 不是一种技术,而是一种判定
过去十年工业界在”能不能不协调”这个问题上做了很多局部尝试:因果一致性(Causal Consistency)、最终一致性(Eventual Consistency)、会话保证(Session Guarantees)、事务隔离级别的放松……但它们都是”给定某个一致性模型,怎么实现”,很少有人正面回答”给定这段业务逻辑,它是否有可能不协调地实现”。
CALM 定理把这个问题变成了一个可判定(decidable,针对 Datalog/Bloom 这样的声明式程序)的属性:逻辑是单调的 → 存在 coordination-free 的实现;不单调 → 必定需要协调,或者降级正确性。
从”给实现找一致性模型”变成”给逻辑找可实现性”——这是一次视角翻转。
1.3 本文想回答的三件事
- 单调性究竟是什么,它和 CRDT 的半格结构是不是一回事?
- Hydro / Keep CALM and CRDT On 在 2025 年把这套理论推到了哪里?
- 工程上,面对一个业务需求(购物车、库存扣减、协作编辑、计数器),我们怎么判断”能不能无协调”,以及什么时候必须回到 Paxos?
1.4 读这篇之前可以准备的三点
- 对分布式一致性模型有基本印象(线性一致、串行化、快照隔离、因果一致);仓库 一致性模型全家谱 足够作为前置。
- 知道 Raft / Paxos 大致在做什么;Raft 深入 一节够用。
- 对半格(semilattice)、单调函数、幂等有一点抽象代数直觉;没有也没关系,§二 与 §三 会用例子补齐。
二、CALM 定理再读
2.1 定义:单调性(monotonicity)
考虑一个以集合为输入、以集合为输出的程序
f。如果对任意输入 I₁ ⊆ I₂,都有
f(I₁) ⊆ f(I₂),则称 f
是单调的。
直观理解:新到的消息只会让答案”长”,不会让已有答案被撤回。
- 并集
∪、投影π、选择σ、自然连接⋈——单调; - 差集
−、否定¬、聚合(min/max 除外的大部分)、某些带相等条件的连接——非单调; count、sum、avg都是非单调的:新数据来了会推翻旧答案。
注意”单调”是对输入集合的单调,不是对时间的单调。一条被”最终删除”的记录只要曾经参与了并集的产生,它的贡献就不会被撤回。
2.2 CALM 定理的陈述
Hellerstein 与 Alvaro 在 CACM 2020 的版本可以用一句话复述:
一个分布式、异步、可能乱序的分布式程序拥有 coordination-free、与集中式等价的实现,当且仅当它在声明式层面可被表达为一个单调的程序。
“当”方向是一个构造性证明:所有单调的 Datalog 程序都可以用 eventual delivery + local evaluation 执行,不需要跨节点的 barrier。 “仅当”方向是一个不可能性结果:非单调操作需要等待”所有输入到齐”这个事实本身,这一等就是协调。
2.3 为什么 “coordination-free” 不等于 “consistency-free”
CALM 定理里 coordination-free 特指”跨节点不需要协调”,它并不等于”没有任何一致性保证”。一个单调程序在 coordination-free 下仍然保证:
- confluence:任意网络重排都导向同一结果;
- determinism modulo inputs:给定输入集合,最终输出唯一。
这两条是 CALM 定理真正有价值的部分:它告诉你”放弃协调”之后你还能保住什么。对比之下,“最终一致性”只保证收敛但不保证收敛到什么、也不保证 determinism。Alvaro 2011 CIDR Consistency Analysis in Bloom 里把这一点讲得很明白:不是所有最终一致都等价,单调性下的最终一致比一般最终一致强得多。
2.4 单调性与 CRDT 半格的关系
CRDT 的核心是:状态 S 构成一个
join-semilattice (S, ⊔),合并函数
⊔
满足可交换、可结合、幂等(commutative,
associative, idempotent,合称 CAI)。
- 状态空间是半格;
- 状态演化是半格上单调上升的轨迹;
- 两个副本任意顺序合并都收敛到相同的上确界(join)。
这与 CALM 的单调性有严格的对应:CRDT 的状态更新是
CALM 意义下的单调函数,而 CRDT
的查询(读)常常是非单调的投影。例如一个 OR-Set
的状态更新单调,但如果你对它做
count(distinct),结果就不再单调——因为后续”删除事件”可能让计数回落。
这是一个被工程实践反复证伪的误解:“用了 CRDT 就不用协调了”。 正确的说法是:只要你的业务逻辑落在 CRDT 的单调子集里,就不用协调;一旦读路径要求非单调属性(确切计数、全局最小余额、互斥占用),协调就以某种形式回来了。
2.5 协调的最小不可缩减成本
即使业务是非单调的,也不是每次操作都必须全局协调。CALM 的精细化版本(Ameloot 等 2013、PODC 2016 的 CRON 结果)给了两个可操作的减法:
- 局部协调足够:如果非单调的关键集中在单一分区(如”同一账户的并发扣款”),那么协调只发生在该分区内,其他并行工作可以无协调。
- 协调可以懒:可以在线无协调地处理,在读之前做一次协调(asymmetric read/write cost);或者反过来,写时协调、读时并行。
工程上这两条对应两个熟悉的模式:sharded serial zone(按键做单点序列化,其他一切并行)和 write-through consensus, read-anywhere(写入 Raft,读节点异步追 log)。
2.6 CALM 的边界:什么它不告诉你
CALM 告诉你”能不能 coordination-free 实现”,但不告诉你:
- 具体的实现长什么样——构造仍然需要工程师根据业务给出。
- 状态代价是多少——一个单调程序可能要求维护指数级状态(例如带时间戳的 OR-Set 在大流量下 tombstone 爆炸)。
- 读的延迟是多少——CALM 只约束”正确性不依赖协调”,不约束读路径的复杂度。
- 运维代价——CRDT 实现的 GC、版本向量压缩、冲突提示,都是 CALM 之外的工程问题。
这是为什么后来工具链方向(Bloom / Hydro / Keep CALM and CRDT On)重要:光有定理不够,还要有能自动把”单调性”翻译成”具体实现”的工具。
三、CRDT 综述:从 Shapiro 2011 到 delta/pure-op
3.1 原始分类:state-based vs op-based
Shapiro 等 2011 的原论文给出两条路径:
- CvRDT(state-based):副本间同步整份状态;合并函数是半格 join;对网络无任何假设,即使消息重排、重复、丢失都没问题(幂等 + 交换 + 结合)。
- CmRDT(op-based):副本间同步操作;要求操作在因果序下传递、每条操作恰好一次;对传输层要求较强,但状态开销小。
两类在表达力上是等价的(有互相模拟的构造),工程上各有场景:CvRDT 适合离线优先、同步间隔长、消息可能丢的场景(Riak DT、Bucardo 风格同步);CmRDT 适合在线实时协作(Yjs、Automerge 的大部分算法)。
3.2 常见数据类型的单调结构
| CRDT | 半格 | 典型操作 | 陷阱 |
|---|---|---|---|
| G-Counter | (ℕⁿ, max per slot) |
inc |
不能减 |
| PN-Counter | 两个 G-Counter 之差 | inc / dec |
读是非单调的 |
| G-Set | (2^U, ∪) |
add |
不能删 |
| 2P-Set | 两个 G-Set | add / remove |
删后不能再加 |
| OR-Set | 元素带唯一 tag | add / remove |
合并语义”add wins” |
| LWW-Register | (V, 取最大时间戳) |
set |
需要时钟;丢更新 |
| MV-Register | 元素带版本向量 | set |
读可能看到多版本 |
| RGA / Logoot / YATA | 带位置标识的序列 | insert / delete |
文本编辑专用,见 §3.4 |
| JSON CRDT | 嵌套 map/list/register | 任意 JSON 路径 | 并发下 map/list 规则复杂 |
“陷阱”那一列是每个做过 CRDT 生产系统的团队都踩过的点。它们都指向同一件事:合并的单调性不等于读语义的直观性。
3.3 delta-CRDT:工程化的 state-based
纯 state-based CRDT 的最大问题是”同步整份状态”不现实。Almeida、Shoker、Baquero 2016 的 Delta State Replicated Data Types 给了一个精巧的工程化路径:每次本地修改只产生一个 delta(也是半格上的一个元素),只在网络上传输这些 delta,但合并时仍然走 join。这相当于把 CvRDT 的”什么都可以丢”保留下来,同时把网络开销做到和 op-based 可比的水平。仓库里另一篇 delta-CRDT 专论 对此有独立展开。
3.4 RGA / Logoot / YATA:序列 CRDT 的三条路
文本编辑(协作 IDE、富文本)需要的是”序列 CRDT”——对同一个字符串并发插入/删除,最后所有副本看到一模一样的文本。主要有三条技术路线:
- RGA(Replicated Growable Array, Roh 2011):每个元素带一个因果 tombstone 链,删除是 mark 而非物理删;
- Logoot / LSEQ(Weiss、Urso、Molli 2009–2013):用”稠密有序标识符”代替整数下标,插入是在标识符空间里找中点;
- YATA(Nicolaescu、Jahns、Derntl、Klamma 2016):Yjs 使用的算法,用 lamport 时间戳 + 左邻 ID 解决并发插入的顺序。
这三条线的共同困境是tombstone 的长期累积。Yjs 13 和 Automerge 2 都在做”历史压缩(garbage collection)“,但压缩必须保证副本间有共识——这又回到了协调。这是一个活跃的研究开放问题。
3.5 Pure op-based 与因果稳定性
Baquero、Almeida、Shoker 2014 提出的 Pure Op-Based CRDTs 把”传播什么”压缩到极致:只传原始操作(不含状态),依赖因果稳定性(causal stability)时才真正施加到状态上。这条线的代表实现是 Antidote DB、Legion、以及部分 AntidoteSQL 原型。
3.6 编程接口的分层:为什么工程师容易被”类型选错”困住
大多数 CRDT
库对外暴露的是数据结构(OR-Set、Counter、Map),而不是代数层面(semilattice、commutative
monoid)。这让业务开发者在选型时经常选错:
- 需要”可删可重加”的标签系统,选了
2P-Set—— 删了以后不能再加,用户投诉; - 需要”带过期的集合”,选了
OR-Set—— tombstone 永不回收,状态爆炸; - 需要”计数同时要查精确值”,选了
PN-Counter—— 单调写没问题,但跨副本读经常看到滞后的总和,要理解这是最终一致而非实时一致。
好的工程实践是在 CRDT 库之上再封一层业务语义,把”添加商品”、“移除标签”映射到具体的 CRDT 原语,并在文档里明确”这个操作的读一致性保证是什么”。这一步通常被忽略,直到线上出问题。
四、Bloom 语言、Hydro 与 VLDB 2025 新贡献
4.1 Bloom / BloomL:单调性作为一等公民
Alvaro、Conway、Hellerstein 2011 的 Bloom 把
Dedalus(时序扩展的
Datalog)变成一门编程语言,关键设计是类型系统能静态识别出每条规则的单调性——用半格类型
lmax、lmin、lbool、lset、lmap
声明数据,编译器可以告诉你”哪些规则需要协调点”。BloomL(SOCC
2012)把这一切延伸到任意用户自定义半格。
在 Bloom 之后,这条语言路线几乎停滞了十年;原因之一是它与主流工业语言(Go、Rust、Java)生态脱节,工程团队很难用 Bloom 写真实服务。
4.2 Hydro:把 CALM 搬到 Rust
Hellerstein 团队 2022 年之后把精力转向 Hydro:基于 Rust 的一组库与编译器(Hydroflow、Hydroflow+、Hydro Deploy),目标是把 Bloom 的静态单调性分析嵌入到一个现代工业级的数据流语言里。核心机制:
- 算子按”流(流式、可追加)“与”累积(lattice 状态)“区分;
- 编译期可以推出哪些 subgraph 是单调的,哪些需要 barrier / stratification;
- 运行期底层是 async Rust,工程可观察性靠 tokio 生态。
Hydro 还处在快速演化期(待核实最新版本号),但它是目前”把 CALM 做成能跑真实后端的工具链”最扎实的一次尝试。
4.3 Keep CALM and CRDT On(VLDB 2025)
Laddad、Power、Milano、Cheung、Crooks、Hellerstein 2024 年发表的 Keep CALM and CRDT On(VLDB 2025)把两条线直接焊接在一起。主要贡献(以官方论文摘要与作者在 CIDR / VLDB 现场的 talk 版本为准;部分实现细节待核实):
- CRDT 作为 Hydroflow 的一等类型。把半格作为类型系统里可以被静态检查的结构,用户写 Rust 代码时,编译器直接验证”这个类型的合并满足 CAI”。
- CALM 分析与 CRDT 组合的交叉优化。例如:如果一段子图是单调的,并且下游的消费者只做”读当前 join 值”,那么中间状态可以用 delta-CRDT 的 delta 流代替整份状态——这是 CALM 推出来的放宽。
- 局部非单调段的自动降级。对检测到的非单调子图,编译器能自动插入协调点(Raft 或 escrow),并给出”这段协调的代价在延迟预算里占多少”的静态估计。
- 与 Anna(Wu et al. SIGMOD 2018/2019)这类 coordination-free KV 的集成。把数据底座的半格与应用层的半格对齐,避免”数据库是 CRDT,应用层又手写协调”的重复税。
这篇论文最大的意义不在于任何单点创新,而是第一次把”单调性 → 可实现 → 有具体 CRDT 编码 → 可由编译器自动选取”这条链条,端到端在一个工业级工具链里打通。它让 CALM 从”理论告诉你边界”变成”工具告诉你怎么做”。
4.4 与其他同期工作的定位
- DBSP / Differential Dataflow(McSherry, Budiu 等)是另一条”单调性驱动”的增量计算路线,见 下一篇 IVM 与 DBSP。
- Noria / Readyset(Gjengset 2018 MIT, OSDI)基于 differential dataflow 做物化视图;它事实上实现了一个”逻辑单调核心 + 外围协调”的分层。
- Anna KV(Wu 2018/2019)把 lattice 作为键值存储的第一等公民,读写路径完全 coordination-free;但它放弃了范围查询与事务。
把这几条线并在一起看,一个趋势很清楚:单调性正在从”一个性质”变成”一个架构接口”。
4.5 一个最小心智模型
读 Keep CALM and CRDT On 时很容易被术语淹没。给一个最小心智模型帮助定位:
- 底层:一个 Z-set / lattice 表示(DBSP 或 CRDT 半格);
- 中层:一段声明式描述(SQL / Bloom / Hydroflow);
- 上层:一个静态检查器(CALM 分析 → 标记哪里需要协调);
- 最外层:一个自动降级路径(把非单调段替换成 Raft / escrow / 延迟协调)。
这个四层结构可以解释所有现代 coordination-free 研究工作的定位。Anna 强调最底层,Bloom/Hydro 强调中层与上层的衔接,VLDB 2025 这篇把四层串起来。Materialize / RisingWave / Feldera 则是 DBSP 版本的类似四层架构(见 下一篇)。
五、Yjs、Automerge、Riak DT 的工程现状
5.1 Yjs 13:协作编辑器事实标准
Yjs 使用 YATA 算法做序列 CRDT,加上
Y.Map、Y.Array、Y.Text、Y.XmlFragment
等顶层类型,是今天协作编辑器(如 Notion 早期、JupyterLab
RTC、TipTap、BlockSuite)的事实标准。工程亮点:
- 二进制编码紧凑:update 编码走 variable-length 整数 + struct store,比 JSON diff 小一个数量级;
- awareness 协议:把”光标位置、选中范围、在线状态”这类非持久、短生命周期的信息从 CRDT 状态里剥离,走一个独立的最终一致通道;
- provider
体系:WebSocket、WebRTC、IndexedDB 等 provider
可以任意组合,应用只认
Y.Doc。
踩坑点: - 历史压缩。Yjs 默认不主动丢弃
tombstone,长期协作文档会让状态单调增长。社区方案
y-redis、y-leveldb 各有自己的
snapshot 策略,但都不保证所有客户端能无损裁剪。 -
字符串合并的”意外顺序”。两个客户端同时在相邻位置插入时,YATA
决定的最终顺序在代数上是良定义的,但对用户而言未必是最”像预期的”的顺序——这是
CRDT 领域无法绕开的问题:代数正确 ≠ 用户心理模型正确。 -
并发光标跳动。editor 内部使用 position
anchors 追踪光标位置;远端 update 到达时 anchors
需要重新解算,用户经常看到”光标短暂跳到别处”。现代
editor(Lexical、BlockSuite)都有专门的 anchor 校准层。
5.2 Automerge 2:朝着”通用 JSON CRDT”
Automerge(Kleppmann, Ink & Switch)和 Yjs 的目标重叠但定位不同:Automerge 更像”一个通用的 JSON CRDT”——支持任意嵌套的 map/list/counter/text,并强调 local-first software 的完整栈(Ink & Switch 的 Peritext、Upwelling、Patchwork 都是在它上面建的)。
Automerge 2 最重要的工程变化是从”TypeScript/JS 实现”切换到 Rust 核心 + 各语言 binding(automerge-rs)。这意味着:
- 性能显著改善(大文档初始化从秒级降到百毫秒级);
- WASM 可以在浏览器跑;
- 但 API 也有一次 breaking change。
Automerge 的代数更接近 Kleppmann 2017 A Conflict-Free Replicated JSON Datatype 的”tombstone + version vector”模型,对并发的 map 键冲突、list 并发插入都有明确的合并语义(但仍然有”user-surprising”的边界情形)。
Ink & Switch 在 Peritext(2022)里提出了一个针对富文本格式(bold、italic、link)的扩展 CRDT——格式本身也要能并发合并。这类”格式 CRDT” 问题比字符序列复杂:一段文本的并发 bold 与并发 italic 要能共存,而并发”删除并格式化”又要有明确结果。Peritext 给出了一个当前最成熟的方案,但它仍然是研究课题。
5.3 Riak DT 与商用 KV 的 CRDT 功能
Basho 的 Riak 2013 开始在 KV 层提供内置 CRDT(G-Counter、PN-Counter、OR-Set、Map、Register),论文基础是 INRIA 的工作。Riak 项目 2017 年后逐步归档,但它的 CRDT 设计是后续许多系统的参照:
- Redis Enterprise(CRDB):跨区域多写基于 CRDT;
- Azure Cosmos DB 的 multi-master 模式:内置 LWW 与 PN-Counter;
- Cassandra / ScyllaDB:counter 列本质是 PN-Counter;
- CockroachDB、YugabyteDB:没走 CRDT 路线,坚持 Paxos/Raft 串行化——这本身是一个”有意识地选择协调”的典型案例。
5.4 工程口径的统一对照
| 维度 | Yjs | Automerge | Riak DT |
|---|---|---|---|
| 语言内核 | TS/JS(部分 WASM 化) | Rust(binding 多语言) | Erlang |
| 主场景 | 实时协作编辑 | local-first 应用 + 协作 | 分布式 KV |
| 序列算法 | YATA | RGA 变体 | 无(KV 不管序列) |
| 历史 GC | 不完整,provider 侧处理 | 部分(actor 压缩) | 不适用 |
| 网络模型 | CmRDT + 因果 | CmRDT + 因果 | CvRDT |
| 打包尺寸 | 极小(<50KB JS) | 中等(WASM ~MB) | 服务端组件 |
5.5 性能边界:把 CRDT 推到极限会发生什么
生产里把 CRDT 库推到边界的典型场景:
- 一篇有 500 万字符的文档 + 20 个并发编辑者。Yjs 13 在桌面端可以做到,但在低端移动设备上 update apply 会引入可感知卡顿;
- 一个 10 万键的
Y.Map。map 本身单次查找是 O(log n),但整文档序列化与网络 update 的内存峰值会让浏览器 tab 崩溃; - 离线十天重新上线。长 offline 会积累大量 delta,再上线时合并可能耗时数秒到十几秒,UI 冻结。
这些边界不是 CRDT 算法本身的缺陷,而是”把代数正确的结构推到工程尺度”时的实际代价。设计评审时要明确文档规模上限 与离线时长上限,超过就要切回”文档拆分”或”回到协调”的策略。
六、业务侧如何判断”能不能无协调”
6.1 一张判定清单
在真实设计评审时,我们按下面这张清单走一遍,比直接争论 “用不用 Raft” 更有效:
- 写语义是否单调?也就是说,“新加一条记录”是否只会扩大结果集?如果出现”减、删除、覆盖、互斥占用”,则非单调。
- 读语义是否非单调?哪怕写是单调的(如 OR-Set),读出”精确计数”或”是否存在且唯一”都会让整体非单调。
- 非单调段是否落在单一分区?如果是同一账户、同一实体上的非单调操作,协调只需要在这个分区内做(例如 per-key Raft group),其余依然可以并行。
- 业务能否接受 escrow 或预留?库存扣减的经典技巧:把 1000 件库存切成 10 份各 100 件 escrow 到不同分区,每份内部无协调地扣,份之间偶尔再平衡。
- 读是否能接受”最终收敛 + 有界 staleness”?如果业务读侧可以接受有限过时(如仪表盘、排行榜、feed),CRDT / CALM 的代价最低。
- 是否存在”全局判定”(如唯一用户名、转账原子性)?这一类几乎肯定要协调,区别只在于协调是落在事务协议、leader 抢占、还是外部锁服务。
6.2 三个常见案例
购物车。加商品是单调(add);减商品在同一购物车内可以做
OR-Set 语义;结算时的”是否库存足够”
是非单调,放在结算事务里单独做。加购物车的写路径完全可以
CRDT 化(Riak、DynamoDB multi-region 风格),结算走 OCC/SI
事务。
协作编辑。文本 insert/delete 的合并是 RGA/YATA;但”文档是否被导出、版本号是否单调递增、是否被锁定只读”这些元操作是非单调的,需要一个小 leader(通常是文档服务)来做序列化。Yjs 的生产部署中,正文走 coordination-free,元数据走传统数据库事务。
库存扣减。纯 CRDT 不适合(“不能低于 0” 是非单调约束)。工业界标准解法是 escrow:把库存切成多个 reservation 块分布到多个分区,每个块内部的扣减单调,块之间偶发协调(rebalance / refill)。只有在所有块都见底时退化成全局协调。详见仓库的 分布式事务专题 相关文章。
6.3 何时必须回到 Paxos/Raft
三种情况下”无协调”几乎一定不成立:
- 严格单调递增的外部 ID(如数据库事务 ID、支付流水号的严格序);
- 全局互斥资源(主备切换、领导者选举、分布式锁);
- 基于”当前值”判断的副作用(“余额足够则扣款并发短信”——副作用的 at-most-once 需要协调)。
这些场景下,与其勉强做 CRDT,不如直接走 Raft/Paxos。仓库里 Raft 深入、Paxos、共识工程实践 这三篇有具体工程讨论。
6.4 评审时的两个反模式
- “我们用最终一致就行”。 这句话在工程上几乎没有信息量——最终一致只约束了时间上的收敛,没说明收敛到什么。一个不小心会收敛成 LWW 丢写。
- “CRDT = 无冲突”。更严谨的说法是”合并算法确定性地选了某一个值,不需要人介入”。这和”用户期望的那个值”之间隔着业务语义。所有 CRDT 生产系统都要在 UI 层做”显式冲突提示”或”多版本并排展示”的兜底。
6.5 一个完整案例:多区域购物车
把前面的判定清单落到一个具体案例上。假设我们做一个跨区域(US / EU / APAC)的电商购物车服务,目标是:“用户在任一区域的任一设备上都能看到自己所有改动,最终一致;不接受丢购物车项。”
步骤 1:拆出可单调的子问题。“加商品”和”删商品”用 OR-Set:每次 add 给商品 ID 附一个 unique tag;每次 remove 记录要删的 tag 集合。合并在半格上做,完全 coordination-free。
步骤 2:识别非单调读。“购物车当前总金额”是对 OR-Set 的非单调聚合。但我们不需要它全局一致——总金额在 UI 上允许 “实时更新但短暂跳动”,实现上在每个区域本地聚合即可。
步骤 3:结算时的协调点。下单时需要”确认总金额 + 锁库存 + 扣支付”。这一刻必须回到协调——走 OCC/SI 事务,就近选一个区域做结算 leader,其他区域把该购物车临时”冻结”(通过一个 LWW 状态 flag;冲突概率低,用 LWW 可接受)。
步骤 4:失败与回退。如果结算失败,把 flag 清掉,CRDT 状态继续允许修改。整个流程里只有结算一小段协调窗口,其余时间完全无协调。
为什么有效。这正是 §2.5 的 “sharded serial zone” 模式:非单调只发生在单一购物车 ID 上,协调局部化;其他购物车完全并行。
七、把 CRDT 嵌进已有服务的三种姿势
7.1 姿势一:把 CRDT 放在应用层,数据库只当 blob 存储
这是 Yjs、Automerge
在多数生产部署下被使用的方式:业务服务持有
Y.Doc 或 Automerge.Doc
实例,把编码后的二进制 update 写到任意一个
KV(PostgreSQL、Redis、对象存储)。数据库只负责”拿得到、拿得全”,不解析、不参与合并。
- 优点:数据库选型完全自由;业务层的状态机清晰、可单元测试;
- 缺点:服务端无法对 CRDT 内容做查询(除非解码整份文档)。这逼着团队把”CRDT 状态”和”可查询投影”分两张表存;后者每次 merge 之后异步更新,只能最终一致。
这是工业界最常见的模式。它背后隐含的判断是:“我不信任数据库内置的 CRDT;我只信任我 SDK 版本里的 CRDT 实现。”
7.2 姿势二:数据库内置 CRDT 类型
Redis Enterprise CRDB、Riak DT、Azure Cosmos DB 的 multi-master 属于这一类。数据库引擎知道某张表/某个 key 是一个 PN-Counter / OR-Set / LWW-Register,合并在引擎内部完成。
- 优点:跨区域写直接走,应用代码几乎不动;
- 缺点:CRDT 类型是数据库约定的那几种,复杂数据(嵌套 JSON CRDT、协作文本)基本无法表达;一旦数据库内置的语义与业务期望偏差半点,修起来就得改数据库配置甚至迁移。
实践建议:这种姿势适合有明确 CRDT 类型、且跨区域多写流量确实大的场景(全球化计数、排行榜、购物车增减)。其他场景不如姿势一。
7.3 姿势三:数据流中间件承担 CRDT 合并
这是 Hydro / Anna / Bloom 这条学术路线想推进的方向:把 CRDT 合并下沉到数据流/KV 中间件,应用端只写一个数据流程序(可能是 SQL、Bloom、Hydroflow)。Materialize、Noria、Anna 都在这条谱系里。
- 优点:开发者基本不用手写合并函数;编译器 / runtime 负责选择 CRDT;
- 缺点:工程生态尚未成熟;诊断和运维难度比前两种高。
7.4 选姿势的三个判断
- 我的 CRDT 会用在几种客户端?多客户端(移动 + Web + 服务端)→ 姿势一(共享 SDK)更可控;
- 跨区域多写的流量比例?占大头 → 姿势二(数据库内置)省最多;
- 业务逻辑的 CRDT 复杂度?纯计数/集合 → 姿势二够用;嵌套/富文本 → 只有姿势一能撑住。
八、一个常见的误用:LWW 作为万能默认
8.1 LWW 看起来省事,实际上是”静默丢写”
LWW(Last-Write-Wins)寄存器只要求有一个全序时间戳,合并就是”取最大时间戳那个值”。工程上它是最便宜的
CRDT:Cassandra 的 cell timestamp、DynamoDB 的
last-writer-wins、Riak 的
allow_mult=false,本质都是 LWW。
它的代价容易被低估:并发写里,只有一个幸存;其他”被丢了”的写没有任何告警。一个被 LWW “吃掉”的订单修改,在日志里和网络抖动无法区分。
8.2 什么时候 LWW 可以接受
- 写的是”幂等覆盖”语义(用户偏好、感知器最新读数);
- 业务上”新的一定对”;
- 不存在需要合并的并发语义(如果两个用户确实都按了 “我要这个”,你不在乎只记下一个)。
8.3 什么时候必须换成更强的 CRDT
- 购物车、好友关系、标签:用 OR-Set 而非 Set-with-LWW,否则”并发加/删” 会丢;
- 计数:用 PN-Counter 而非 number-with-LWW,否则并发 +1 只留下一个;
- 富文本、结构化文档:用序列 CRDT 或 JSON CRDT;LWW 整体覆盖等同于”最后一个保存的人把所有人的改动覆盖掉”;
- 配置开关:如果是”任何一个节点开了就算开”,用 G-Set 表达单调开启更合适。
8.4 兜底:让冲突变得可见
即便选了强 CRDT,“合并结果不符合用户预期”的情形仍会出现(§5.1 提到的 YATA 例子)。两个务必提供的工程兜底:
- 版本历史:保留足够的历史让用户回退;
- UI 级冲突标记:对”看起来被替换”的字段给出视觉提示(比如多人同时编辑同一段时展示另一版的气泡)。
这是”代数正确”与”用户正确”之间不可绕开的那层胶水。
九、因果一致性、会话保证与 CRDT 的关系
CRDT 经常和因果一致性(Causal Consistency, CC)被放在一起讲,但它们回答的问题不同:
- CC 是一个一致性模型:它约束”可观察的事件顺序”;
- CRDT 是一组数据类型:它让合并在任意顺序下都收敛。
CC 的常见实现用版本向量(Version Vector)或dotted version vector 来追因果;CRDT 可以直接把因果信息编进类型内部(比如 OR-Set 里元素的 tag)。工程上两者是互补的:
- 服务端用 CC(session guarantees:monotonic reads、read-your-writes、writes-follow-reads 等)提供可预期的读视图;
- 数据结构层用 CRDT 保证并发写无协调收敛。
仓库里的 会话一致性保证、一致性模型全家谱 有对 CC 的独立讨论;本文不在此展开。
9.1 与 CALM 的闭环
一个常被忽略的观察:CC + 单调性 = coordination-free。如果你的系统只走因果可见、且所有操作单调,那么不需要任何全局协调,你就拿到了一个正确的 coordination-free 系统。Bloom 的类型系统、Anna 的 lattice 合并、Hydro 的 subgraph 分析本质上都在帮你维持这个闭环。反过来,只要有一个非单调点(比如”一次 exactly-once 通知”),闭环就破了,要么协调、要么降级。
十、开放问题与小结
10.1 开放问题
- CRDT 的历史压缩:tombstone / 版本向量的长期膨胀还没有普适方案,协作文档、长期在线 KV 都要定制。
- 跨 CRDT 的组合:把两个 CRDT 组合(例如 Map of OR-Set of LWW-Register)在代数上是良定义的,但工程上的”组合语义是否符合业务预期”常常不是。Automerge 的 JSON CRDT 在这方面持续踩坑。
- CALM 判定在命令式语言上的可工程化:Bloom 和 Hydro 是用声明式/数据流语言来绕开这件事;Java/Go 服务里如何自动判定某段逻辑是单调的,仍无令人满意的答案。
- 拜占庭环境下的 CRDT:经典 CRDT 假设副本都是正确的;有恶意副本时如何维持最终一致,研究仍在早期(Merkle-CRDT、可验证 delta)。
- CRDT 与端侧机器学习的融合:本地模型参数在设备间合并是否可以看作一种 CRDT?在联邦学习、端侧推荐等场景下这是一个未被充分回答的问题。
10.2 一段话总结
CALM 定理告诉你什么时候可以不协调;CRDT 告诉你如果决定不协调,状态和合并该如何设计;Hydro 与 Keep CALM and CRDT On 把这两者焊进同一个工具链;Yjs、Automerge、Riak DT 则给出了十五年的工程化证据,证明”coordination-free 可落地、但永远有一个非单调的外层需要回到协调”。
下一个十年值得观察的是:当数据库、消息系统、前端应用都开始把”半格类型”当成公共接口时,“协调”是否会从”系统默认”退化成”偶尔的显式选择”。
10.3 一张可贴在墙上的速查
| 问题 | 快速答案 |
|---|---|
| “这个操作要协调吗?” | 单调 → 不必;非单调 → 必须(或降级) |
| “我要挑哪种 CRDT?” | 纯增长 → G-Set/G-Counter;有删 → OR-Set/PN-Counter;有序 → RGA/YATA;嵌套 → JSON CRDT |
| “LWW 能不能用?” | 只在”新写一定对”的语义下;否则换 OR-Set/PN-Counter |
| “CRDT 换 Raft 的临界在哪?” | 出现”唯一性、互斥、精确计数、exactly-once 副作用” 就回到 Raft |
| “评审里该问什么?” | 写单调吗?读单调吗?非单调能否单分区隔离?能否 escrow?能否接受有限 staleness? |
参考文献
- Hellerstein J. M. The Declarative Imperative: Experiences and Conjectures in Distributed Logic. CIDR 2010. https://www.cidrdb.org/cidr2011/Papers/CIDR11_Paper1.pdf
- Hellerstein J. M., Alvaro P. Keeping CALM: When Distributed Consistency is Easy. CACM, February 2020. https://arxiv.org/abs/1901.01930
- Shapiro M., Preguiça N., Baquero C., Zawirski M. Conflict-Free Replicated Data Types. SSS 2011 / INRIA RR-7687. https://hal.inria.fr/inria-00609399
- Shapiro M. et al. A Comprehensive Study of Convergent and Commutative Replicated Data Types. INRIA RR-7506, 2011.
- Almeida P. S., Shoker A., Baquero C. Delta State Replicated Data Types. Journal of Parallel and Distributed Computing, 2018.
- Alvaro P., Conway N., Hellerstein J. M., Marczak W. R. Consistency Analysis in Bloom: a CALM and Collected Approach. CIDR 2011.
- Conway N., Marczak W. R., Alvaro P., Hellerstein J. M., Maier D. Logic and Lattices for Distributed Programming. SoCC 2012. (BloomL)
- Ameloot T. J., Neven F., Van den Bussche J. Relational Transducers for Declarative Networking. JACM 2013. (CALM 证明)
- Laddad S., Power C., Milano M., Cheung A., Crooks N., Hellerstein J. M. Keep CALM and CRDT On. VLDB 2025. (作者与题目以 VLDB 2025 program 为准;部分实现细节待核实)
- Wu C., Faleiro J. M., Lin Y., Hellerstein J. M. Anna: A KVS For Any Scale. ICDE 2018 / SIGMOD 2019.
- Kleppmann M., Beresford A. R. A Conflict-Free Replicated JSON Datatype. IEEE TPDS 2017. https://arxiv.org/abs/1608.03960
- Nicolaescu P., Jahns K., Derntl M., Klamma R. YATA: Yet Another Transformation Approach for Real-time Collaboration. ICWE 2016. (Yjs 基础算法)
- Roh H.-G., Jeon M., Kim J.-S., Lee J. Replicated Abstract Data Types: Building Blocks for Collaborative Applications. JPDC 2011. (RGA)
- Weiss S., Urso P., Molli P. Logoot-Undo: Distributed Collaborative Editing System on P2P Networks. IEEE TPDS 2010.
- Baquero C., Almeida P. S., Shoker A. Making Operation-Based CRDTs Operation-Based. DAIS 2014. (pure op-based)
- Yjs 官方文档. https://docs.yjs.dev/
- Automerge 官方文档. https://automerge.org/docs/
- Hydro 项目主页. https://hydro.run/
- Abadi D. Consistency Tradeoffs in Modern Distributed Database System Design: CAP is Only Part of the Story. IEEE Computer, 2012. (PACELC)
上一篇:【数据库研究前沿】加密数据库:FHE、PIR 与可搜索加密的工程边界
下一篇:【数据库研究前沿】流批一体与增量视图:Materialize、RisingWave、Feldera 的 DBSP 理论
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【数据库研究前沿】系列导论:从 System R 到 AI-Native 的 2026 研究地图
以 System R、Postgres、Bigtable、Spanner、Snowflake 等关键节点串起 50 年数据库史,勾勒 2026 年 AI-Native、向量检索、HTAP 云原生、新硬件、隐私计算、新范式、方法论七条主线,并给出 25 篇系列文章的完整阅读地图。
【数据库研究前沿】如何读数据库顶会论文:SIGMOD/VLDB/CIDR 阅读路线
从顶会定位、检索渠道、三遍读法到工业与学术论文的辨别方法,给出 2023–2025 年数据库领域可信必读二十篇,并配套 CMU 15-721、Stanford CS 245 等公开课清单。
【数据库研究前沿】学习型查询优化器:Neo、Bao、Balsa 到 LLM-CBO
系统梳理 Neo、Bao、Balsa 以及新兴 LLM-assisted 查询优化的核心思想,结合 PostgreSQL pg_hint_plan 给出一条可落地的 learned QO 工程路径
【数据库研究前沿】学习型索引再审视:RMI、ALEX、PGM
从 Kraska 2018 RMI 到 ALEX、PGM-Index、RadixSpline,系统梳理学习型索引的数学骨架、更新代价与落地边界,并给出一个最小 RMI 的 Python 实现