想象一下,两个用户在没有互联网的情况下在不同的平面上编辑同一文档。每个都更改相同的字段。当他们着陆并且设备同步时,需要有人决定哪个版本获胜。传统的答案既简单又残酷:最后保存的内容会覆盖前一个内容。另一个人丢了工作,但自己却没有意识到。
这是任何本地优先应用程序的核心问题。数据首先存在于设备上,编辑离线进行,然后进行协调。问题不在于是否会发生冲突,而在于如何在不丢失信息的情况下合并两个不同的事实,并且在理想情况下,不依赖中央服务器充当法官。 CRDT 是我们对此最优雅的数学答案。
什么是 CRDT,无需神秘主义
CRDT 代表无冲突复制数据类型。这个名字比这个概念更令人恐惧。实际上,它是一种数据结构,旨在使多个副本可以独立编辑,并且当它们相遇时,它们会自动收敛到相同的最终状态,而无需事先协调。
关键词是融合。更改到达的顺序、应用相同更改的次数以及它通过网络经过的跳数都无关紧要。如果两个设备看到相同的操作集,它们最终会是相同的。这种保证是数学上的,而不是代码善意的承诺。
为了实现这一点,CRDT 的操作必须具有三个属性。它们是可交换的,因此顺序不会改变结果。它们是关联的,因此分组并不重要。而且它们是幂等的,因此两次应用相同的东西不会造成损害。遵守这些规则的结构形成了数学上所说的半格,这就是收敛保证的来源。
为什么这解决了本地优先问题
如果没有 CRDT,离线同步需要仲裁器。通常,接收所有版本、应用任何规则(通常是臭名昭著的最后写入规则)的服务器会获胜,并返回官方真相。这有两个成本。数据在覆盖中丢失,并且依赖始终在线的中心点来解决任何分歧。
CRDT 消除了这种依赖性。由于合并是确定性的并且内置于结构本身中,因此任何设备都可以与任何拓扑中的任何其他设备合并。两部手机可以通过蓝牙直接同步,三个副本可以放在一个网格中,结果与协调一切的服务器相同。服务器一旦存在,就只是一个方便的中继,而不是权威。
这改变了应用程序的性质。用户无需等待网络响应即可看到其编辑已应用,因为本地事实已经有效。同步在后台进行,并且永远不会返回“您的工作已被丢弃”。这使得现代协作编辑器的应用程序体验如此流畅,也是任何严肃的本地优先架构的技术基础。
您会发现的 CRDT 风格
两种主要风格占主导地位。基于状态的 CRDT 在副本之间交换整个结构,并使用合并功能进行组合。它们的推理很简单,但当数据增长时,它们会对网络造成沉重负担。基于操作,或基于操作,仅传播单个更改,这更经济,但需要可靠的操作交付层。
在这些样式之上有一个现成类型的目录。从多个副本添加增量而不丢失任何增量的计数器。知道如何以一致的方式混合添加和删除的集合,例如 OR-Set。最令人垂涎的情况是顺序文本,其中每个字符都接收一个唯一且可排序的标识符,以便竞争插入不会相互重叠。像 Yjs 和 Automerge 这样的算法将所有这些打包到您使用的库中,而无需重新实现理论。
对于架构决策者来说,好消息是您很少从头开始编写 CRDT。您选择库,根据它提供的类型对数据进行建模,并获得融合作为礼物。智力工作是将领域映射到这些结构,而不是证明定理。
CRDT 真正发挥作用的地方
当合并规则真正中立时,即保留两个编辑始终是正确的行为时,它们是无与伦比的。协作文本就是一个完美的例子:如果两个人输入不同的段落,那么您需要两个段落。列表、看板、笔记、矢量图和大多数协作生产力工具都属于这一类。
它们在连接性较差或间歇性的场景中也表现出色。现场应用、偏远地区的数据收集、需要离线数小时的设备。在[离线优先1]应用程序中,CRDT 使您可以完全放心地工作,在下一次同步中不会丢失任何内容。自动收敛正是这些上下文所需要的保证。
当您想要从关键路径中消除服务器时,它们就会发挥作用。点对点架构、本地网格、属于同一用户的设备之间的同步,无需通过云。所有这一切都是可行的,因为合并智能存在于数据中,而不是基础设施中。
它们不是正确答案的地方
这是经常被掩盖的部分。 CRTD 收敛到有效状态,但不一定收敛到您的业务认为正确的状态。融合并不意味着满足业务规则。如果两个人预订了离线航班的最后一个座位,CRDT 会很乐意合并两个预订,并且您将获得超额预订的数学保证。
需要语义决策的冲突不属于 CRDT。不能为负的平衡、一个领域的独特性、使另一个领域无效的认可、任何需要“不,这不可能发生”的不变量都需要一个真正的仲裁者。在这些情况下,业务规则需要决定冲突,并且尝试将其推送到数据层会产生微妙且昂贵的错误。
还有内存和存储的成本。为了确保收敛,许多 CRDT 存储随着编辑历史记录而增长的元数据。删除了项目墓碑、每个字符标识符、版本向量。如果没有压缩策略,看似很小的结构可能会膨胀到令人惊讶的程度。并不是每个问题都可以廉价地变成 CRDT,也不是每个问题都应该如此。
作为 CTO,我的建议是务实的。使用 CRDT,其中合并是自然累加的并且经验增益是真实的。当正确性取决于业务不变量时,接受仲裁器,无论是服务器、命令队列还是呈现给用户的显式解析流。最常见的错误是将 CRDT 视为灵丹妙药,并发现生产中出现超额预订。
如果您现在正在设计本地优先产品的同步层,那么在选择工具之前,有必要将可自动合并的内容与需要人工或服务器决策的内容明确分开。这种诚实的划分将节省数月的返工时间。
另请阅读
- [同步引擎:使本地优先可行的工具2
- [本地优先:首先在您的设备上运行的软件3
- [现实生活中位置优先:政府、医疗保健和物流4
- [Next.js App Router:默认思考服务器指南5
- [离线优先:为没有互联网时进行设计6
- [使用 Zod 进行数据验证:为什么 TypeScript 类型还不够7
