想象你们班有四本《班级日志》,四个值日小组各拿一本,每天在上面记录班级大事。周五要把四本合订成一本“总日志”。可问题来了:甲组写了“周三篮球赛获奖”,乙组也写了同一件事,合订时重复了;丙组把某条记录删掉了,丁组却又把它加上去——合订时这条到底该不该存在?这就是分布式系统每天都在面对的“合订难题”:数据有多份副本,副本各自更新,最后必须拼成一份统一的结果,还不能吵架。 最朴素的解法是“合订时大家拉个群商量”——先锁死、再排序、后合并。但一旦网络断开或有人离线,“商量”就进行不下去。聪明的做法是换一种记账方式:让每本日志天生就不会产生分歧。比如规定“每组的记录只能写在自己专属的页码上”“每条记录都贴上唯一编号”“删除某条不是涂掉,而是登记它已失效”。只要四个小组都用这套记账规则,合订时就只剩机械性的“归拢”,不需要仲裁——这就是无冲突复制数据类型(CRDT)的思想。 这篇文章带你从“班级日志”出发,理解 CRDT 的数学根基(半格)、三种最常用类型(只增计数器 G-Counter、可删除集合 OR-Set、后写覆盖寄存器 LWW-Register),最后落地到协同编辑与离线优先应用的工程实战。

岗位关联

对 AI 从业者而言,掌握 CRDT 意味着能在大模型服务、RAG 知识库同步、多机缓存副本等分布式场景中,用数学确定性消灭“数据打架”,把最终一致性从玄学变成可证明的工程能力。

目录
  1. 可视化演示
  2. 初识CRDT:合订日志的困局
  3. 数学基础:半格与合并
  4. G-Counter:只增计数器
  5. OR-Set:带墓碑的集合
  6. LWW-Register:时间戳说了算
  7. 工程落地:协同编辑与离线优先
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示CRDT副本通过数学结构自动合并的无冲突收敛,展示G-Counter、OR-Set与LWW-Register等核心类型的合并语义,并应用于协同编辑与离线优先场景。

本节配有交互动效(进入视口后自动加载,约 3KB)。

CRDT 无冲突收敛 副本合并:自动收敛 G-Counter:计数累计 OR-Set:集合运算 merge×N 保证最终一致

初识CRDT:合订日志的困局

四本《班级日志》各记各的,周五合订时最怕什么?最怕“同一件事记了两遍”、“删除的记录又回来了”。其实只要提前约定记账规则,合订就不会有争议。分布式系统里的多副本就是那四本日志:用户在任意副本上写数据(离线也能写),之后通过网络把更新传播给其他副本。关键在于——传播的顺序、时机、路径都不可控,如何保证最终结果一致?

先看背景:CAP 定理告诉我们,一致性(C)、可用性(A)、分区容忍性(P)三者不可兼得。网络分区是常态,所以多数互联网系统选择 AP 路线,放弃“任何时候强一致”,改用“最终一致性”:允许副本暂时不一致,但一旦停止写入并完成同步,所有副本必须收敛到同一状态。传统做法是用操作转换(OT)或中心化版本号仲裁,复杂度高;CRDT 则换了个思路——设计数据结构本身,让任意合并天然无冲突。

CRDT 的合并函数 $m$ 只要满足三条律,就能保证“无论怎么合、合多少次,都不吵架”:

回到班级日志:有了这三条律,值日组长再也不用在周五晚上开“仲裁会”。他们只需要机械地执行同一条合订工序,无论先合哪两本、中间同步多少次,最终四组手上拿到的总日志一定是同一份。CRDT 的价值就在这里——把“冲突要不要解决”变成“冲突根本不存在”。

∀ a,b,c∈S:m(a,a)=a,m(a,b)=m(b,a),m(m(a,b),c)=m(a,m(b,c))\displaystyle \forall\,a,b,c\in S:\quad m(a,a)=a,\quad m(a,b)=m(b,a),\quad m(m(a,b),c)=m(a,m(b,c))
三条律分别是幂等、交换、结合。幂等保证“重复合并无害”,交换保证“谁先合谁都一样”,结合保证“分步合并与一次合并等价”。三者合起来,就是“任意同步路径最终收敛”。
  1. 幂等律 $m(a,a)=a$同一个副本把自己的状态拿来回合并两遍,结果不变。相当于合订时不小心把同一本日志抄了两遍,也不会多出内容。
    m(a,a)=am(a,a)=a
  2. 交换律 $m(a,b)=m(b,a)$先合甲再合乙,与先合乙再合甲,结果完全一样。网络消息先到后到都不影响。
    m(a,b)=m(b,a)m(a,b)=m(b,a)
  3. 结合律 $m(m(a,b),c)=m(a,m(b,c))$三本日志可以两两先合,再与第三本合,中间怎么分组都行。这给了工程上极大的同步自由度。
    m(m(a,b),c)=m(a,m(b,c))m(m(a,b),c)=m(a,m(b,c))

数学基础:半格与合并

为什么三条律就能保证不乱?想想值日表的“完整程度”:乙表把甲表没写的都补上了,我们就说乙表“更完整”。合并两本日志,本质是生成“比两者都完整,但又最克制”的一本新日志。日语中这种比较关系,数学上叫做偏序关系,记作 $\sqsubseteq$:$a \sqsubseteq b$ 表示 $a$ 的信息量不超过 $b$。

如果任意两个状态 $a, b$ 都存在一个“刚好覆盖两者、又最小”的状态,称为最小上界 $a \sqcup b$(读作 a join b),那么整个状态空间就构成一个 join 半格。CRDT 的合并操作 $merge(a,b)$ 直接定义为 $a \sqcup b$——只需证明“状态空间是半格”,就自动获得了幂等、交换、结合三条律。而且合并后状态只会更完整,不会回退,这就是单调性:

来看 G-Counter 这类具体例子时,半格会变得非常直观。此刻你只需记住:CRDT 设计者的工作,就是为某种业务数据类型找出一个“比较谁更完整”的偏序,以及对应的“求最小上界”的合并方法。班级日志的“完整程度”可以是“谁覆盖了谁的记录”,也可以是“谁的时间戳更新”——不同的偏序,会导出不同的 CRDT 类型,这正是接下来三节的故事。

a⊑a⊔b,a⊔b=b⊔a,a⊔a=a,(a⊔b)⊔c=a⊔(b⊔c)\displaystyle a \sqsubseteq a\sqcup b,\qquad a\sqcup b=b\sqcup a,\qquad a\sqcup a=a,\qquad (a\sqcup b)\sqcup c=a\sqcup(b\sqcup c)
这组式子是半格的完整定义:第一项说合并至少吸收自身(单调前进),后三项正是上一节的三条律。合并就是求最小上界。
  1. 单调上界 $a \sqsubseteq a\sqcup b$合并后的状态一定包含 $a$ 的全部信息,不会丢失已有记录——日志只会越合越厚,不会越合越薄。
    a⊑a⊔ba \sqsubseteq a\sqcup b
  2. 交换律 $a\sqcup b=b\sqcup a$最小上界的概念本身不分先后,天然满足交换。
    a⊔b=b⊔aa\sqcup b=b\sqcup a
  3. 幂等律 $a\sqcup a=a$自己与自己求上界还是自己,合订不会把内容翻倍。
    a⊔a=aa\sqcup a=a
  4. 结合律 $(a\sqcup b)\sqcup c=a\sqcup(b\sqcup c)$多状态合并可以任意分批,网络怎么乱序都无所谓。
    (a⊔b)⊔c=a⊔(b⊔c)(a\sqcup b)\sqcup c=a\sqcup(b\sqcup c)

G-Counter:只增计数器

班级要统计每个值日小组这学期做了几次卫生。最稳妥的记法是什么?每组固定写在自己专属的页码上:“一组 12 次、二组 9 次……”这样无论合订多少次,每组都只认自己那一页的数字,别人碰不到。最后总数 = 各页数字相加。这就是 G-Counter(Grow-only Counter,只增计数器)。

G-Counter 的每个副本维护一个 $n$ 维向量($n$ 为节点数),节点 $i$ 只能自增自己的分量 $c_i$,合并时逐分量取最大值。读总数时对全部分量求和。因为“自己只写自己的页码”,并发自增永不丢失;因为“合订时每页取更大的数字”,重复同步也不会把计数翻倍:

G-Counter 只解决“只增不减”的需求。如果业务需要减(比如库存、余额),就用 PN-Counter:把“增加量”和“减少量”分别做成两个 G-Counter,总数等于两者之差。这就像班级想把“文明加分”和“违纪扣分”分开记两页,互不干扰。合订时各页取 max,最终差值依然收敛——这就是“给 CRDT 做组合”的典型思路。

cj(new)=max⁡(cj(A), cj(B)),total=∑j=0n−1cj(new)\displaystyle c_j^{(new)} = \max\left(c_j^{(A)},\,c_j^{(B)}\right),\qquad total = \sum_{j=0}^{n-1} c_j^{(new)}
公式上半部分描述合并规则:新向量的每个分量等于两个副本该分量的较大者;下半部分描述读取规则:总值是各分量的和。
  1. 分量约束节点 $j$ 的分量 $c_j$ 只能由节点 $j$ 自己增加,别人只能读取。这保证了并发自增不会互相覆盖。
    cj 仅由节点 j 自增c_j \text{ 仅由节点 } j \text{ 自增}
  2. 逐分量取 max合并时,每个分量的新值 = 两副本该分量的最大值,即“每页取最新的数字”。
    cj(new)=max⁡(cj(A), cj(B))c_j^{(new)} = \max(c_j^{(A)},\,c_j^{(B)})
  3. 总和求和读取计数器时把 $n$ 个分量全部相加,得到全局总次数。
    total=∑j=0n−1cj(new)total = \sum_{j=0}^{n-1} c_j^{(new)}

OR-Set:带墓碑的集合

图书角有两本台账,管理员分别记录“《三体》在架”。突然一本标注“借出”,另一本没标。合订时到底在不在架?问题出在“同一本书”的身份上——如果每次入库的书都贴一张唯一编号贴纸,事情就清楚了:入库 = 贴编号;借出 = 在编号旁记一笔“已出”。不同管理员贴的编号不同,合订时只需把编号并在一起,冲突自然消失。

OR-Set(Observed-Removed Set,观察移除集合)正是这个思路。集合状态由两部分组成:添加集 $A_{add}$(所有曾添加的“元素-标签”对)和删除集 $A_{rem}$(已被删除的标签,即“墓碑”)。每次 add(e) 都生成一个全新标签 $t$;remove(e) 把元素当前所有标签送入删除集。查询时,只要存在一枚标签既在添加集中、又不在删除集中,元素就算“在集合中”:

墓碑机制牺牲了一点存储(删除记录不会消失),换来的是并发添加与并发删除永远可判定。回到图书角:管理员 A 贴了编号 001 的书,管理员 B 在另一本台账上把 001 标“借出”,合订时 001 在删除集中,书显示“借出”;如果 B 标的是自己贴的 002,那么 001 仍在架。谁贴的标谁负责删,永远不会吵起来。

lookup⁡(e)  ⟺  ∃ t: (e,t)∈Aadd ∧ t∉Arem\displaystyle \operatorname{lookup}(e) \iff \exists\,t:\ (e,t)\in A_{add}\ \land\ t\notin A_{rem}
Aadd′=Aadd(1)∪Aadd(2),Arem′=Arem(1)∪Arem(2)A_{add}' = A_{add}^{(1)}\cup A_{add}^{(2)},\qquad A_{rem}' = A_{rem}^{(1)}\cup A_{rem}^{(2)}
第一个式子说明元素的可见条件;第二个式子说明合并规则:添加集和删除集各自做并集合并。因为合并就是取并集,天然满足交换、结合、幂等。
  1. 唯一标签 $t$每次添加都生成新标签,即使两个副本同时添加同一个元素,也被视为两个独立事实。
    (e,t)∈Aadd(e,t) \in A_{add}
  2. 墓碑删除 $A_{rem}$删除不是从集合中抹掉元素,而是把该元素所有标签记入删除集——就像在台账上记“编号 X 已还”,而不是涂掉书名。
    e 的所有标签 →Areme \text{ 的所有标签 } \to A_{rem}
  3. 可见条件 $\exists\,t$只要有一枚标签没进墓碑,元素就被看到。删掉一个副本的添加,不影响另一个副本的同名元素。
    ∃ t: (e,t)∈Aadd∧t∉Arem\exists\,t:\ (e,t)\in A_{add}\land t\notin A_{rem}
  4. 并集合并 $A'=A_1\cup A_2$两副本合订 = 两份台账的入库记录、退库记录全部归拢,无先后依赖。
    Aadd′=Aadd(1)∪Aadd(2),Arem′=Arem(1)∪Arem(2)A_{add}'=A_{add}^{(1)}\cup A_{add}^{(2)},\quad A_{rem}'=A_{rem}^{(1)}\cup A_{rem}^{(2)}

LWW-Register:时间戳说了算

教室前的值日安排板经常被反复改写:小明写“周三:小红”,小红路过改成“周三:小绿”。多块副板同步时,以哪块为准?最直接的办法是看板上最后落笔的时间——谁最后写,谁说了算。这就是 LWW-Register(Last-Writer-Wins Register,后写覆盖寄存器)。

LWW-Register 把每次写入变成一个二元组 $(v, t)$:$v$ 是业务值,$t$ 是时间戳。本地写入时,若新时间戳比当前记录更大,就覆盖;合并时同样比较两副本的时间戳,取大者。这个类型本身极其简单,复杂度和风险全在时间戳的质量上:

LWW-Register 的诱惑在于“无脑简单”,陷阱在于时间戳。如果各节点用本地物理时钟,时钟漂移或 NTP 校正导致时间倒退时,看似更新的写入反而可能被旧写入覆盖。生产上的补救措施是使用 Lamport 时钟、向量时钟等逻辑时钟来捕捉因果顺序,或者在全局同步时钟(如 Google TrueTime)之上构建。简单说:落笔时间不可靠,LWW 就会变成“乱写覆盖认真写”的捣蛋鬼。

(wa,ta)⊔(wb,tb)={(wa,ta),ta>tb(wb,tb),otherwise\displaystyle (w_a,t_a)\sqcup(w_b,t_b)=\begin{cases}(w_a,t_a), & t_a > t_b\\(w_b,t_b), & \text{otherwise}\end{cases}
合并规则一句话:保留时间戳更大的那个状态。若时间戳相等,需要额外规则(如比较节点 ID)来保证确定性,避免两端各自保留各自的值导致永不收敛。
  1. 写入选 $(v,t)$每次写入不是孤立的“值”,而是“值+时间戳”的组合,落笔即带时间。
    (wa,ta)(w_a,t_a)
  2. 比较时间戳 $t_a>t_b$合订时比“谁更新”,而不是比“谁的逻辑更复杂”。
    ta>tbt_a > t_b
  3. 取大者覆盖时间戳大的状态胜出,旧笔迹被新笔迹覆盖。
    (wa,ta) 胜出(w_a,t_a)\ \text{胜出}
  4. 平局兜底时间戳相等时按节点 ID 等确定性规则裁决,保证单调前进。
    otherwise⇒(wb,tb)\text{otherwise} \Rightarrow (w_b,t_b)

工程落地:协同编辑与离线优先

回想四本日志合订的故事——当我们把“固定页码”“唯一编号”“落笔时间”这些约定落实成代码,就得到了协同编辑软件的基石。你用的在线文档、设计稿协作、笔记软件,本质上都是“每个用户持有一本日志,离线时记自己的,联网后自动合订”。

工程实现上有两条主流路线。一是专业协同编辑库:Yjs(基于 YATA 算法,被 Notion、Linear 等使用)和 Automerge(基于多值寄存器)。它们把文档建模为一串“带唯一 ID 的字符序列”,每个字符类似 OR-Set 的元素,删除只是给它盖墓碑;光标、选区等元数据则用 LWW-Register 的思路。二是通用数据同步框架:MongoDB Realm、Triplit 等将 CRDT 用于离线优先应用的字段级同步。选型经验如下:

落地时的关键决策包括:计数用 G-Counter/PN-Counter;集合增删用 OR-Set;“最新值覆盖”用 LWW-Register 但必须配合逻辑时钟;富文本/文档直接挑选 Yjs 这类成熟的 CRDT 引擎,不要自己造轮子。还有一个常被忽视的坑:字符级 CRDT 在超长文档高并发输入时可能退化为 $O(n^2)$ 合并,需要用区块化(block)策略优化。CRDT 不是银弹,但它是“合订日志”这类问题的数学最优解——把冲突在设计期消解,而不是在运行时仲裁。

merge(Si,Sj)=Si⊔Sj,合并完成后 Si=Sj\displaystyle merge(S_i,S_j)=S_i\sqcup S_j,\qquad \text{合并完成后}\ S_i=S_j
无论用哪个库,核心保证都是这条收敛式:任意两个副本合并后状态相等。工程上所有复杂度(网络重传、乱序、离线补同步)都是在为“最终能执行这次 merge”服务。
  1. 任意副本对 $S_i, S_j$网络里任何两个节点两两同步,不需要全局协调者。
    ∀ i,j\forall\,i,j
  2. 合并 $= S_i\sqcup S_j$执行的就是半格上的最小上界运算,等价于把两份日志归拢。
    merge(Si,Sj)=Si⊔Sjmerge(S_i,S_j)=S_i\sqcup S_j
  3. 收敛 $S_i=S_j$合并完成后双方状态一致,这就是最终一致性的数学保证。
    Si=SjS_i=S_j

AI 应用场景

场景一:大模型知识库的多副本同步

阶段:数据工程 / 向量库索引同步

解决的问题:RAG 应用通常把文档切片后写入向量数据库,并在多区域部署副本。传统主从同步在分区时要么阻塞写入(牺牲可用性),要么用 LWW 覆盖导致“旧切片覆盖新切片”。CRDT 让每个区域先落本地,再异步合并,文档的增删改以 OR-Set 语义收敛,不丢更新。

具体案例:某智能客服系统在美东、美西两地域各部署一套知识库,运营人员离线批量更新 FAQ。上线 CRDT 同步层后,两地合并时重复文档自动去重,已删除文档不会因旧副本回传而“复活”。

场景二:AI 数据标注的多人协同

阶段:数据工程 / 标注质量保障

解决的问题:多标注员并发修改同一批训练数据的标签,A 改“猫”B 改“狗”时传统方案要么锁标注(降低效率),要么静默覆盖(丢标签)。LWW-Register 保证“最新标注生效”,OR-Set 追踪标签的增删历史,审计可追溯。

具体案例:自动驾驶数据标注平台中,同一张点云图由多人在弱网环境下框选目标。接入 CRDT 后,各客户端离线框选、联网自动合并,重复框自动去重,冲突框按时间戳裁决,标注返工率下降约 30%。

场景三:离线优先的移动端 AI 应用

阶段:边缘计算 / 端云数据同步

解决的问题:AI 质检 App 在工厂地库、矿井等无信号环境采集数据,传统方案要么等待网络(阻塞生产),要么本地覆盖(丢数据)。CRDT 让移动端先写本地日志,联网后与云端自动合订,保证“每个端上采集的样本都不丢、不重、不乱”。

具体案例:某工业视觉质检系统,巡检员在天车、地坑等弱网区域拍照并离线标注缺陷。基于 CRDT 的表单同步层让 20 台终端离线记录 2000+ 条样本,网络恢复后在 3 秒内完成收敛合并。

Python 演示

python55 行
class GCounter:    def __init__(self, node_id, node_count):        self.node_id = node_id        self.values = [0] * node_count     def inc(self, delta=1):        self.values[self.node_id] += delta     def merge(self, other):        for i in range(len(self.values)):            self.values[i] = max(self.values[i], other.values[i])     def total(self):        return sum(self.values)  class ORSet:    def __init__(self):        self.added = {}        self.removed = set()     def add(self, e, tag):        self.added.setdefault(e, set()).add(tag)     def remove(self, e):        for tag in self.added.get(e, set()):            self.removed.add(tag)     def lookup(self, e):        return any(t not in self.removed for t in self.added.get(e, set()))     def merge(self, other):        for e, tags in other.added.items():            self.added.setdefault(e, set()).update(tags)        self.removed.update(other.removed)  def demo():    a = GCounter(0, 2)    b = GCounter(1, 2)    a.inc(5); b.inc(3)    a.merge(b); b.merge(a)    print("G-Counter 收敛:", a.values, b.values, "总数", a.total())     x = ORSet()    y = ORSet()    x.add("apple", "x1")    y.add("apple", "y1")    y.remove("apple")    x.merge(y); y.merge(x)    print("OR-Set 合并后 apple 可见:", x.lookup("apple"), "| 墓碑集合:", sorted(x.removed))  if __name__ == "__main__":    demo()
目的:用不到 60 行实现 G-Counter 与 OR-Set 两个核心 CRDT,演示双副本并发写入后通过 merge 自动收敛。
  1. self.values[self.node_id] += delta只改自己的分量。这是 G-Counter 并发不冲突的根源:我的页码只有我能写。
  2. self.values[i] = max(self.values[i], other.values[i])逐分量取 max 合并,重复同步也不会翻倍计数。
  3. self.added.setdefault(e, set()).add(tag)每次 add 都生成独立标签,不覆盖别人对同一元素的添加。
  4. for tag in self.added.get(e, set()): self.removed.add(tag)删除是把该元素当前所有标签送入墓碑,而不是物理删除数据。
  5. return any(t not in self.removed for t in ...)只要有一枚标签没进墓碑,元素就仍“在集合中”。
  6. a.merge(b); b.merge(a)双向同步后两边状态一致,这就是最终收敛的直观体现。

常见误区

误区很多同学以为用了 MongoDB/Redis 之类的数据库就自动获得 CRDT 能力,数据合并冲突由存储引擎解决,业务代码不用关心。

正解CRDT 是“数据结构+合并规则”的设计,不是数据库的一个开关。数据库或框架只是载体,你必须为每个业务数据类型选定或实现正确的 CRDT 类型,并确保合并操作满足幂等、交换、结合三律。就像班级日志本——“合订不吵架”靠的是记账规则,不是靠柜子(数据库)本身。

为何常见:因为 CRDT 常以库的形式(如 Yjs)被集成,使用者只看到“自动同步”,误以为底层机制与己无关。

误区常常有人认为只要每次写入带 Unix 毫秒时间戳,合并时取大者,就能保证“最新覆盖旧数据”。

正解物理时钟会漂移、会因 NTP 校正而倒退。节点 A 的时钟快 5 秒,它写了一条“过期逻辑”,却可能覆盖节点 B 真正的新数据。必须改用 Lamport 时钟、向量时钟等逻辑时钟,或依赖 TrueTime 这类全局同步时钟,才能保证时间戳反映真实的“先后”。

为何常见:直觉上“时间戳大 = 时间晚 = 更正确”,人们忽略了分布式系统没有全局时钟这一基本事实。

误区初学者认为在任意副本上 `remove(e)`,合并后所有副本都应该看不到 `e`。

正解OR-Set 的删除只针对“当前副本已看到的标签”。如果另一个副本在删除发起后又并发添加了新的标签,合并后该元素可能仍然可见。这并非 bug,而是“并发添加与删除都算数”的正确语义——想要删除“绝对权威”,需要配合 LWW 时间戳仲裁。

为何常见:把单机“删除就是抹掉”的直觉带入了多副本并发场景。

误区有人以为用 CRDT 后,任意时刻读任意副本都能拿到最新写入。

正解CRDT 只保证“停止写入并合并完成后”收敛一致(最终一致性),不保证某次读取是最新的(强一致)。从 A 副本读到的可能是几秒前的状态。读多写少、要求实时一致的场景,仍需配合版本号或租约机制。

为何常见:把“最终收敛”与“实时一致”两个强度级别的承诺混为一谈。

误区有人直接对 G-Counter 调用 `decrement`,期望合订后正确反映增减净值。

正解G-Counter 是 Grow-only(只增)的,减操作在数学上会破坏单调性,导致合并不收敛。要支持减法必须用 PN-Counter——用两个 G-Counter 分别记录“增加总量”和“减少总量”,最终值等于两者之差。凡是“允许回退”的业务,都要用组合式 CRDT 设计。

为何常见:真实业务中“减”很常见,而 G-Counter 的名字容易让人忽略其“只增”的限制。

面试题

Q1. 以下哪组性质是 CRDT 合并操作必须满足的?

单选题 · easy

  • A.结合律、交换律、幂等律← 正确答案
  • B.分配律、吸收律、幂等律
  • C.交换律、分配律、单调律
  • D.结合律、分配律、互补律

答案:A。CRDT 合并函数必须满足幂等(合并两次等于一次)、交换(谁先谁后无所谓)、结合(任意分组都等价)。这正是把“半格上的最小上界”作为合并规则带来的三个性质。分配律、吸收律、互补律是格论中与 join 合并无关的性质,不构成 CRDT 的收敛保证。

Q2. 根据 CAP 定理,多数互联网系统在分布式存储上选择哪条路线?

单选题 · easy

  • A.CP,放弃可用性
  • B.AP,选择最终一致性← 正确答案
  • C.CA,放弃分区容忍性
  • D.CAP 三者同时满足

答案:B。CAP 定理指出一致性、可用性、分区容忍性三者不可兼得。由于网络分区无法避免,系统必须保留分区容忍性(P),只能在 C 和 A 之间取舍。CRDT 属于 AP 路线的典型代表:保留可用性(各副本随时可写),用最终一致性替代强一致。

Q3. G-Counter 合并时各分量采取什么操作?

单选题 · mid

  • A.各分量直接相加
  • B.各分量取最大值← 正确答案
  • C.各分量取最小值
  • D.各分量与本地异或

答案:B。G-Counter 合并的规则是“逐分量取 max”:每个节点只自增自己的分量,合并时从两份向量中挑出每个分量的较大者。如果相加会导致重复同步时计数翻倍;取最小值则会让已同步的计数回退,破坏单调性。

Q4. 某三节点 G-Counter 中,副本 A 的向量为 [3,0,0],副本 B 的向量为 [0,2,4],两者合并后总数为?

单选题 · mid

  • A.合并后总数为 9← 正确答案
  • B.合并后总数为 5
  • C.合并后总数为 7
  • D.合并后总数为 6

答案:A。合并后各分量取 max,得到 [3,2,4],对分量求和得 3+2+4=9。选项 B 是只算了非零分量个数,选项 C/D 是把某些分量相加但漏了 4,均不符合“逐分量取 max 后求和”的规则。

Q5. OR-Set 中删除一个元素时,真正被记录进删除集的是什么?

单选题 · mid

  • A.删除元素本身
  • B.元素的所有标签← 正确答案
  • C.什么都不用记录
  • D.仅记录当前时间戳

答案:B。OR-Set 的删除是把元素当前所有标签(tag)加入删除集(墓碑)。直接删除元素会导致并发添加的标签复活;只记时间戳则无法区分“删除哪一次添加”。墓碑机制保证“删除只针对已观察到的添加”。

Q6. OR-Set 能保证合并收敛的原因包括?

多选题 · hard

  • A.每次添加都有唯一标签← 正确答案
  • B.删除以墓碑形式记录← 正确答案
  • C.合并时添加集删除集取并集← 正确答案
  • D.依赖全局时钟来排序操作

答案:ABC。唯一标签让“同名元素的多次添加”互不干扰;墓碑让删除不依赖物理删除;添加集与删除集各取并集的合并天然满足幂等、交换、结合。OR-Set 完全不依赖全局时钟,因此 D 错误。

Q7. LWW-Register 合并时两个状态时间戳相等,通常如何处理?

单选题 · mid

  • A.两个值都保留即可
  • B.按节点ID确定性裁决← 正确答案
  • C.随机挑选其中一个
  • D.抛出异常结束合并

答案:B。时间戳相等时要求合并结果确定且单调,必须用可比较的规则(如节点 ID、操作序号)打破平局。随机选择会导致同一对副本反复合并结果不一致;保留两个值则偏离了 LWW“单值覆盖”的定义。

Q8. 物理时钟不适合直接作为 LWW-Register 的时间戳,原因包括?

多选题 · hard

  • A.时钟漂移导致误判先后← 正确答案
  • B.消息乱序影响时间排序
  • C.NTP校正可能使时钟倒退← 正确答案
  • D.时钟精度制约合并效率

答案:AC。时钟漂移使不同节点对“谁更晚”判断不一致;NTP 校正确实可能让本地时钟回拨,导致新写入带上更小的时间戳。消息乱序是网络问题,与时钟本身无关;精度也不直接影响合并正确性,而 LWW 的正确性恰恰取决于时间的单调与可比。

Q9. 实现一个“点赞数只增不减”的统计功能,最适合使用哪种 CRDT?

单选题 · easy

  • A.使用 OR-Set 集合类型
  • B.使用 G-Counter 计数器← 正确答案
  • C.使用 LWW-Register 寄存器
  • D.使用普通 JSON 文件记录

答案:B。点赞数只增不减正是 G-Counter 的典型场景:各节点自增自己的分量,合并时逐分量取 max,保证并发点赞不丢、重复同步不翻倍。OR-Set 适合集合增删,LWW 适合“最新值覆盖”,普通 JSON 无并发合并能力。

Q10. 与 OT(操作转换)相比,CRDT 在协同编辑场景的优势包括?

多选题 · hard

  • A.无需中心服务器排序← 正确答案
  • B.天然支持离线优先← 正确答案
  • C.数学保证合并收敛← 正确答案
  • D.传输带宽一定更小

答案:ABC。CRDT 合并满足交换律,不需要中心服务器统一操作顺序;副本本地可写,离线后同步即可收敛;收敛性由半格数学结构保证。但 CRDT 常携带标签、墓碑等元数据,带宽不必然更小,因此 D 错误。