关注「索引目录」公众号,获取更多干货。
一直以来都习惯使用实时工具,很少去想它们背后的工作原理。比如 Google Docs、Figma、Replit 多人协作、VSCode Live Share……你输入什么,对方就能立刻看到,一切正常,不会出现任何冲突,也不会覆盖别人的内容。它……就是这么好用。
不知为何,我一直很好奇这背后的运作机制。一个人的改动怎么会神秘地出现在所有地方?冲突是如何处理的?如果两个人同时改动了同样的东西会发生什么?
我不想只得到“高屋建瓴”的答案,我想亲身感受这个系统。就像构建Veridian帮助我理解 Git一样,我也想通过自己构建一个实时同步系统来理解它们。
所以我开发了Conflux,一个用 Rust 编写的小型实时协作引擎。这并不是因为世界需要另一个后端,而是因为我需要了解这些实时系统究竟是如何在多个用户之间同步状态而不崩溃的。
事实证明,这并非什么神秘的技术,而只是一系列简单理念的有序堆砌。
我为什么想建造这个
每当我看到有人同时编辑同一个东西时,我的脑子里就会冒出这样的想法:“好吧,肯定是某个神秘的库在做一些复杂的操作。” 但后来我了解了CRDT,一切就豁然开朗了。
无冲突复制数据类型 (CRDT)本质上是一种永远不会发生冲突的数据结构。每次更新都是可合并的。任何人都可以自由编辑而无需锁定。最终,所有数据都会收敛到相同的状态。
意识到这一点后,我决定亲眼看看:
-
在实际应用中,基于 CRDT 的协作系统究竟是什么样子?
背后的“真相”:什么是 CRDT?
好的,我们来分析一下。“无冲突”这一点才是关键。
“正常”方式(问题所在):
想象一下你我正在编辑一个文本文件。
-
我们都下载了该文件。文件内容是: "Hello"。 -
我把副本改成: "Hello world"。 - 与此同时
,你将你的副本更改为: "Hello there"。 -
我上传了我的版本。服务器现在有 "Hello world"。 -
您上传了您的版本。服务器现在有 "Hello there"。
我的修改没了,永远没了。你覆盖了我的修改。这是个冲突。这就是像 Git 这样的版本控制系统花费大量时间试图通过“合并冲突”来管理的问题。
“CRDT”方法(解决方案):
CRDT 的工作方式并非如此。它们不会来回发送整个文件,而是发送指令。
-
我们俩都拥有以下状态: "Hello"。 -
我做了一些更改。我的本地 CRDT 没有显示“新文件是‘Hello world’”。它生成了一条指令: (At position 5, add: " world")。 -
与此同时,你进行了一项更改。你的 CRDT 生成了一条指令: (At position 5, add: " there")。 -
我向服务器发送指令。 -
您将指令发送到服务器。
服务器以及最终所有客户端都会收到这两条指令。CRDT 的“魅力”在于它拥有一套数学规则来合并这些指令,从而使所有人最终都达到完全相同的状态。
最终文本可能是"Hello world there"或"Hello there world"。重要的是没有数据丢失,我们最终都看到了相同的内容,而没有出现“合并冲突”错误。
就是这样。CRDT 只是一种数据结构,它拥有一个非常优秀的“合并”算法,因此永远不会发生冲突。
Conflux 的实际作用
现在我们知道了什么是 CRDT,整个系统就更有意义了。
去掉多余的细节,Conflux 实际上只做三件事:
-
它维护着各种房间,例如“文档”或“会话”。 -
每个房间都包含一个CRDT 文档——该文档存储共享状态。 -
客户端发送指令(更新),服务器合并这些指令,然后将它们广播给其他所有人。
这是同样的循环,但现在它的意义就显而易见了:
你输入内容 → 你的本地 CRDT 生成一条指令→ 你将该指令(“更新”)发送到服务器 → 服务器将其合并到自己的 CRDT 中 → 服务器将该指令广播给所有其他客户端 → 它们的本地 CRDT 将其合并 → 每个人的 UI 都更新。
就是这样。这就是整个循环。没有复杂的算法,没有奇怪的变换,也没有分支时间线。
只需:更新→合并→广播。
由于 CRDT 的设计初衷就是为了实现无缝合并,因此不会发生任何冲突。
架构(简易版)
为了便于理解,我画了出来:
图中每个方框都只负责一项任务。没有哪个方框会同时执行五项任务。正是这种简洁性让整个系统显得平易近人。
服务器的设计方式
Conflux 服务器由四个主要部分组成,每个部分都只负责一项工作。
1. 客房经理
这是用来跟踪所有活跃房间的部分。
如果房间不存在,它会创建它。如果房间长时间闲置,它会清理它。
没什么特别的——就是生命周期管理。
2. 房间(演员循环)
房间本身基本上就是一个小型服务器。
它运行一个循环,监听类似这样的命令:
ApplyUpdate(这是我们的CRDT指令!) SetAwarenessChatJoinLeave
并且它会将这些更新应用到它自己的 YDoc 中。
这种“演员”设计意味着每个房间都是独立的,从而避免了通常的共享状态混乱。
3. WebSocket 服务器
这是入口。
它:
-
使用 JWT 对用户进行身份验证。 -
从URL中提取房间ID。 -
将连接升级为 WebSocket。 -
将收到的消息转发到正确的房间。 -
将房间发出的消息转发回客户端。
它接受两种类型的消息:
- 纯文本:
这将变成聊天消息。 - JSON:
这将变成结构化的 CRDT 或感知更新。
这样一来,调试就变得很容易了,因为你可以直接在终端输入聊天信息。
4. JWT 身份验证
sid每次登录都会在令牌中创建一个新的会话 ID ( )。
这样就解决了“所有人使用同一个令牌登录”的问题。
当客户端连接时,服务器就知道:
-
哪个用户 -
哪个会议 -
哪个房间
它虽然简单,但却提供了一个清晰的身份模型。
同步流程(简明解释)
假设两个人正在编辑一份共享文档。
- 用户 A 编辑了某些内容。
其本地 CRDT 立即应用更改并生成指令。 - A 向服务器发送指令(更新)。
这只是一小段二进制数据。 - 服务器将该指令应用于自身的 CRDT。
这样可以确保服务器上的文档副本具有权威性。 - 服务器广播指令。
房间内的 所有其他客户端都会收到相同的更新。 - 每个客户端都会应用此指令。
他们本地的 CRDT 会将此指令合并到他们已有的系统中。 - 所有人都保持同步。
即使许多人同时更改同一内容,CRDT 也能顺利处理合并。
听起来很复杂,但看到实际效果就觉得很简单了。
为什么意识和聊天很重要
实时系统不仅仅关乎文档内容。
用户需要了解:
-
还有谁在线? -
他们的光标在哪里 -
谁在打字 -
谁加入了或离开了
所以我添加了“感知事件”。这些只是简短的 JSON 消息,可以立即传播,显示谁是谁以及他们在哪里。
聊天功能很简单——如果客户端发送的是普通文本而不是 JSON,服务器会将其视为聊天消息并进行广播。
这是一个很小的功能,但它让系统感觉更加生动。
仪表盘
我还构建了一个小型仪表盘接口:
GET /dashboard
返回结果:
-
房间号 -
已连接客户端数量 -
文档更新次数 -
宣传活动数量
虽然没什么特别之处,但看到系统运行起来却非常有用。
我从建造这个项目中学到了什么
就像 Veridian 让我真正理解了 Git 一样,Conflux 让我真正理解了实时协作。
以下是几个突出的要点:
-
实时同步主要涉及消息排序和广播。 -
CRDT 可以消除大约 90%的“冲突”问题。 -
演员式房间使得并发性出奇地好。 -
WebSocket比我想象的要容易使用得多。 -
尽早建立完善的身份识别系统可以避免日后出现很多麻烦。 -
大多数“复杂系统”只不过是一系列简单步骤的集合。
在构建 Conflux 之前,这些系统感觉就像黑盒子一样。
建成之后,我觉得它们是我可以理解的东西,甚至可以加以改进。
结语
Conflux 并不完美,它可能缺少一些功能,也绝对还达不到生产就绪的程度。但它完全实现了我当初构建它的目的,并且帮助我理解了实时协作的底层工作原理。
如果你对这些东西感兴趣,不妨自己尝试搭建一个小型版本。
你不需要重新创建一个 Google Docs。即使是一个基于 CRDT 的小型共享计数器也能让你学到很多东西。
您可以在这里查看 Conflux:
https://github.com/Kayleexx/conflux
关注「索引目录」公众号,获取更多干货。

