ElectricSQL 与 Rich-CRDTs:为本地优先应用引入数据库级不变量保障
2026/9/16 11:52:26 网站建设 项目流程

ElectricSQL 与 Rich-CRDTs:为本地优先应用引入数据库级不变量保障

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

Rich-CRDTs(富 CRDT)是在经典 CRDT(无冲突复制数据类型)基础上扩展出"数据库级保障"的复制数据类型,用于在并发写入下维持约束、引用完整性等不变量。本文以 ElectricSQL(构建于同步之上的 agent 平台)团队的研究视角,系统讲解 Rich-CRDTs 的三大核心技术——组合(Composition)、补偿(Compensations)与预留(Reservations),并给出可直接落地的数据建模思路,读完你将对"如何在不引入全局协调的前提下保证分布式数据的一致性约束"建立清晰、可操作的认识。

先复习:CRDT 与强最终一致性

CRDT 是一类特殊的复制数据类型,其核心价值在于提供无冲突的并发(conflict-free concurrency)。借助 CRDT,多人可以同时更新同一份数据,所有更新会被无冲突地合并,最终所有节点收敛到相同的值——这被称为强最终一致性(Strong Eventual Consistency):一旦全部更新完成复制,所有参与者看到的数据完全一致。CRDT 因此成为现代低延迟地理分布式数据库与多人协作系统共同的底层关键组件。

CRDT 将冲突解决策略编码进类型本身:当分布式系统中的两个节点试图同时更新同一个 CRDT 时,CRDT 调用内置的合并方法来处理并发操作。该合并方法必须满足三条数学性质:

  • 交换律(Commutativity):合并顺序不影响结果;
  • 结合律(Associativity):分组合并的结果一致;
  • 幂等性(Idempotency):同一操作重复应用不改变结果。

常见的 CRDT 类型包括:

  • Counter(计数器):合并方法将所有操作的增量值累加;
  • Last-Writer-Wins Register(LWW 寄存器):合并方法直接以时间戳最新的操作值覆盖寄存器。

CRDT 可以相互组合以构造更复杂的对象。但问题在于:把合并策略与一致性语义做对非常困难。某些操作在并发合并时无法不破坏一致性,而且大量常见的数据库不变量根本无法由单个数据类型的合并策略来保证。实际构建现代应用时,几乎必然遇到这类"可组合性"与"不变量安全"问题,而解决它们至今仍是 ad hoc(特设)过程——开发者每用 CRDT 构建一个新应用,都要从头重新推理这些问题并做决策。这种缺乏标准化的事实,使 CRDT 编程极易出错。

Rich-CRDTs 的目标,正是为开发者提供内置检查与合理默认值的高级抽象,让不变量保障"开箱即用"。

Rich-CRDTs 是什么

Rich-CRDTs 在基础 CRDT 之上提供额外的不变量安全与数据库保障,手段是以下三类技术:

  1. 组合(Composition)
  2. 补偿(Compensations)
  3. 预留(Reservations)

其中前两类——组合与补偿——是无冲突的,它们不引入任何额外协调机制;第三类预留则尽可能避免运行时协调,但在必要时以协调兜底,以保证必须的不变量。单个 Rich-CRDT 可以同时使用其中一种或多种技术,设计时的总体思路是:先用无冲突技术保住不变量,最后才在必要时审慎使用预留。

原文档给出了一张清晰的决策流程,用于指导 Rich-CRDT 的建模选择:

┌────────────────────────┐ │ │ │ Can I use basic CRDTs? ├──► Done │ │ └──┬─────────────────────┘ │ ▼ ┌────────────────────────┐ │ │ │ Can I use composition? ├──► Done │ │ └──┬─────────────────────┘ │ ▼ ┌──────────────────────────┐ │ │ │ Can I use compensations? ├──► Done │ │ └──┬───────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ │ │ Can I use escrow reservations? ├──► Done │ │ └──┬─────────────────────────────┘ │ ▼ Use lock based reservations

即:优先尝试基础 CRDT → 组合 → 补偿 → 托管预留(escrow)→ 最后才退到基于锁的预留。这一阶梯式取舍贯穿整篇技术方案的设计哲学。

组合(Composition):嵌套出高阶数据结构

组合指的是把多个 CRDT 合并或嵌套起来,构造更丰富的高阶无冲突数据结构

最简单的组合示例是正负计数器(PN Counter):它由两个计数器组合而成,一个记录正向变化、一个记录负向变化,两者结合得到当前净值。PN Counter 由此突破了单个 Counter 无法表示"减少"的局限。

更复杂的组合示例是JSON CRDT。JSON 本质上是由四种数据类型构成的 map:string、number、object、array。在 map CRDT 中,每个 key 可以持有不同的 CRDT 对象作为其值,JSON CRDT 为 JSON 提供的每种可能值类型给出合理的默认 CRDT 策略。例如:

{ "name": "Valter", "score": 234, "attributes": { "location": "Lisbon" }, "history": ["bought-fish", "grilled-fish"] }

上面这个 JSON CRDT 中,每个 key 映射到不同的基础 CRDT 类型,且各类型可以选用不同的冲突解决策略:例如name用 Last-Writer-Wins 寄存器,score用 Counter,history用数组类型(如Replicated Growable Array,可复制增长数组)。map 层还可以额外提供"当并发操作把不同类型值关联到同一 key 时"的冲突解决策略。

组合的价值在于:由已验证正确的基础 CRDT 拼装出的高阶结构,其合并语义可以分层推导,从而降低自定义合并逻辑出错的风险。

补偿(Compensations):由数据库主动执行的额外操作

补偿指的是数据库在用户指定操作之外额外执行的操作,其目的是确保某项不变量始终成立。

数据库里经常存在多张由键关联的表——关联表中的外键指向父表的主键。引用完整性(Referential Integrity)就是要保证这些关联在任何时刻都有效:例如删除了主表中的第 15 行,就不允许任何关联表中还残留值为 15 的外键。

引用完整性在并发场景下极具挑战,因为一个操作可能在删除某行,而另一个并发操作正在向引用它的关联表添加新行。经典例子是:某个玩家报名参加锦标赛(enrol),恰好与锦标赛被删除(delete)并发发生

图示:玩家-锦标赛的引用完整性违反示意。

补偿可以提供解法。例如在"报名玩家"操作中附加一个touch 操作:把额外的 touch 写进报名玩家的事务里,那么即使它与一个删除锦标赛的并发事务合并,只要锦标赛集合遵循add-win 语义(添加操作胜过删除操作),就能保证锦标赛依然存在,从而维持引用完整性。

图示:玩家-锦标赛的补偿操作,通过确保锦标赛存在来维持引用完整性。

有两个要点需要特别注意:

:::note 在没有冲突的情况下,这些额外补偿操作对外不可观测(不影响用户可见的结果)。 :::

:::note 该例的可交互演示可见原文档引用的 "Introduction -> Active-active replication" 页面。 :::

预留(Reservations):为并发操作预留资源

预留是指为每个客户端"预留"一定量的资源,从而允许对单一数据类型执行并发操作。资源预留有三种方式:

  1. 托管预留(Escrow reservations)
  2. 托管预留加算法(Escrow reservations plus an algorithm)
  3. 锁(Locking / 互斥)

托管预留(Escrow)

托管预留是一类这样的预留:为某个操作获取的资源会在整个分布式集群内被分摊出去,使各个节点/集群各自持有一部分"执行特定操作的权限"。

有界计数器(Bounded Counter)是使用预留技术的 Rich-CRDT 典型例子,它把一部分资源预分配给每个节点。假设你是 Stubhub,有 1000 张 Justin Bieber 演唱会的票要卖,你要避免"两个人在两个不同节点上同时买到最后一张票"的场景。做法是:给 10 个节点每个分配 100 张票的预留额度。当某个节点的配额耗尽时,它再与其它 peer 协调获取更多预留。这样,绝大多数操作无需每次都协调即可校验

托管预留的关键优化之一是主动分配与再平衡(proactive rebalancing):让预留由真正需要的节点/集群持有。如果 Justin Bieber 演唱会在旧金山、所有票都在 US-West 集群被买走,那么 Rich-CRDT 系统可以注意到(或预测到)这一点,主动把更多预留划给 US-West 集群。在运转良好时,主动再平衡可以让绝大多数更新都免于协调

托管预留加算法(Escrow plus algorithm)

这一类预留中,为操作获取的资源由算法给出

例如对树形结构的操作必须小心执行以维持树不变量——两个节点可能被并发地移动到彼此之下,从而引入环。一种朴素做法是预留整棵树的根来安全执行 move 操作,这会阻止树上任何其他并发操作;而一个预留算法可以更高效:只要并发操作触及树的不同部分,就是安全的。move 操作的参数(源节点与目标节点)可以用来计算"需要预留的最近公共父节点",从而既保证节点安全移动,又允许其他子树上的并发操作继续执行。

锁(Locking)

锁是指只有持有锁的节点才能访问某资源的技术,锁分共享锁与排他锁。

典型例子是全局顺序标识符(global sequential identifiers):必须一个接一个按序生成,即全局已知最后生成的值,才能保证顺序属性且无空洞。为此,开发者可以使用锁 CRDT:当某节点持有锁时,它能 1) 看到上一个锁持有者产生的变更,从而知道序列中下一个标识符应是什么;2) 访问生成全局标识符的那段代码。否则,它必须与其它 peer 交换权限——这与上述托管预留机制类似。

Next steps:Rich-CRDTs 在 ElectricSQL 中的落地

Rich-CRDTs 并非纯理论。从仓库证据看,它直接构成了 ElectricSQL 同步引擎的一致性与约束设计的理论基础:

  • 团队背景:Rich-CRDTs 与 Just Right Consistency 的研究作者 Valter Balegas 正是本项目的联合创始人兼 CTO(见 website/data/team/team.yaml 中short_bio: 'Rich-CRDTs and Just Right Consistency. ...');
  • 工程落地:在 Introducing ElectricSQL v0.6 一文的路线图说明中明确写道,ElectricSQL 的不变量支持目前聚焦于"使用补偿机制实现引用完整性"(invariant support is limited to referential integrity using compensations),并计划逐步支持跨复制边界的引用完整性、唯一约束(unique constraints)与检查约束(check constraints)——"In this we're building on research we authored and are implementing as Rich-CRDTs";
  • 研究脉络:项目维护了完整的论文清单(见 website/docs/sync/reference/literature.md 与 website/data/literature/papers.yaml),其中与本文直接相关的论文包括:Extending Eventually Consistent Cloud Databases for Enforcing Numeric Invariants(2015,Balegas 等人)、IPA: invariant-preserving applications for weakly consistent replicated databases(2018)、Just-Right Consistency: reconciling availability and safety(2018)、Conflict-free Replicated Data Types (CRDTs)(2011,Preguiça / Baquero / Shapiro)等。如需深入了解底层算法,可从该清单按图索骥。

因此,把本文的决策流程与三类技术应用到本地优先(local-first)应用上,正是 ElectricSQL 让"带完整约束的 SQL"在弱一致复制环境中仍然成立的关键路径:能组合就用组合,组合不了就用补偿保住引用完整性,最后才在必要时引入预留。这也是从"写 CRDT 容易出错"走向"开箱即用"的核心抽象思路。

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询