点击下方“JavaEdge”,选择“设为星标”
第一时间关注技术干货!
免责声明~ 任何文章不要过度深思! 万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案」; 不要急着评判文章列出的观点,只需代入其中,适度审视一番自己即可,能「跳脱出来从外人的角度看看现在的自己处在什么样的阶段」才不为俗人。 怎么想、怎么做,全在乎自己「不断实践中寻找适合自己的大道」
1 协作文档编辑服务的设计方式
1.1 C/S架构的集中式设施
为所有用户提供文档编辑服务。所有用户都连接到一个中心服务器,该服务器负责存储和处理文档数据,用户通过连接到该服务器来协作编辑文档。提供更好的安全性和可控性,但有单点故障问题
1.2 点对点技术设计
以便在单个文档上协作。将文档数据分散存储在多个用户设备,每个用户都可直接编辑文档并将更改同步到其他用户设备。提供更好灵活性和可扩展性,但可能会有数据同步不及时或数据冲突问题
大多数商业方案侧重C/S架构,以实现更精细控制。因此,本文也使用C/S架构设计服务。
2 需求
2.1 功能性
① 文档协作
多用户能同时编辑文档。大量用户应能查看文档。
② 冲突解决
系统应将一个用户做的编辑推送给所有其他协作者。若他们正在编辑文档同一部分,系统还应解析用户之间的冲突。
③ 建议
- 在文档中完成常用单词、短语和关键词的建议
- 修复语法错误的建议
④ 查看计数
文档的编辑应能看到该文档的查看计数。
⑤ 历史
用户能查看文档的协作历史。
2.2 非功能性
① 延迟
不同的用户可连接起来协作同一份文档。为来自不同区域的用户维护低延迟访问。
② 一致性
系统应能解析用户并发编辑文档时之间的冲突,从而实现文档的一致视图。不同区域的用户应看到文档的更新状态。
对连接到同一区域和不同区域的用户来说,保持一致性都很重要。
③ 可用性
该服务应一直可用,并展示对故障的鲁棒性。
④ 可扩展性
大量用户应能同时使用该服务。他们可查看相同的文档,也可创建新文档。
3 组件
3.1 数据存储
- 关系数据库,用于保存用户信息和文档相关信息以施加特权限制
- NOSQL,用于存储用户评论以获得更快的访问速度
- 时间序列,用于保存文档的编辑历史记录
- Blob 存储,用于存储文档中的视频和图像
- 缓存,分布式缓存如 Redis 和 CDN,为最终用户提供良好的性能。使用 Redis存储不同数据结构,包括用户会话、类型预期服务的功能、频繁访问的文档。CDN存储频繁访问的文档和重量级对象如图像和视频。
3.2 处理队列
针对每次微小字符更改使用 HTTP 调用是低效的。因此使用 WebSockets 减少开销,并通过不同用户实时观察文档的更改。
3.3 其他
会话服务器,维护用户的会话信息。通过会话服务器管理文档访问权限。还要有配置、监控、发布-订阅和日志记录服务来处理监控任务,如在服务器失败时监控和选举领导者,排队用户通知等任务及记录调试信息。
协作文档编辑服务的详细设计:
4 工作流程
4.1 协作编辑和冲突解决
每个请求都会转发到操作队列。这是解析同一文档的不同协作者之间冲突的地方。如果没有冲突,则通过会话服务器将数据批量存储在时间序列数据库中。像视频和图像这样的数据会被压缩以优化存储,而字符会被立即处理。历史:借助时间序列数据库,可以恢复文档的不同版本。可以使用 DIFF 操作来比较版本并标识差异以恢复同一文档的旧版本。
4.2 异步操作
通知、电子邮件、查看次数和评论都是可以通过像 Kafka 这样的发布-订阅组件排队的异步操作。API 网关生成这些请求并将它们转发到发布-订阅模块。
4.3 建议
建议以类型提前服务(typeahead service)的形式出现,该服务提供通常使用的单词和短语的自动完成功能。类型提前服务还可以从文档中提取属性和关键词并向用户提供建议。由于单词数量可能很高,我们将为此目的使用 NoSQL 数据库。此外,最常用的单词和短语将存储在像 Redis 这样的缓存系统中。
4.4 导入和导出文档
应用程序服务器执行许多重要任务,包括导入和导出文档。应用程序服务器还将文档从一种格式转换为另一种格式。例如,.doc 或 .docx 文档可以转换为 .pdf 或反之亦然。应用程序服务器也负责为类型预期服务提取特征。
5 详细设计
5.1 文档编辑器
文档由以特定顺序排列的字符组成。每个字符都有一个值和一个位置索引。字符可以是字母、数字、回车()或空格。索引表示字符在有序字符列表中的位置。
文本或文档编辑器的作用是在文档中的字符上执行插入()、删除()、编辑()等操作。下面是文档的描绘以及编辑器将如何执行这些操作。
文档编辑器如何执行各种操作
5.2 并发性
不同用户对同一文档的协作可能导致并发问题。若多个用户编辑文档的同一部分,可能出现冲突。由于用户在本地有文档的副本,服务器上的最终文档状态可能与用户在他们端看到的不同。在服务器推送更新版本后,用户会发现意外结果。
① 在同一位置索引处添加字符
两个用户修改同一字符可能导致并发问题:
② 删除同一字符
删除同一字符,可能导致意外更改:
第二个例子表明,不同用户应用相同的操作不会是幂等的。因此,在多个协作者同时编辑文档同一部分时,需冲突解决。
协作编辑中并发问题的解决方案应遵循规则:
- 交换律:应用操作的顺序不应影响最终结果
- 幂等性:重复的相似操作只应用一次
6 冲突解决技术
6.1 操作转换(OT)
广泛用于协作编辑中的冲突解决的技术,一种【无锁】、【非阻塞】的冲突解决方法。若协作者之间的操作冲突,OT会解析冲突并将正确的汇聚状态推给最终用户。因此,OT为用户提供一致性。
OT 使用位置索引方法执行操作来解析上面讨论的那些冲突。通过保持交换律、幂等性来解决上述问题。
OT示例:
基于 OT 的协作编辑器在满足以下两个属性时一致:
- 因果关系保持:如果操作 a 发生在操作 b 前,那先执行操作 a,然后执行操作 b
- 收敛:不同客户端上的所有文档副本最终相同
上述属性是 CC 一致性模型的一部分,CC 一致性模型是协作编辑中一致性维护的模型。
OT的缺点
对字符的每个操作都可能需要更改位置索引。这意味着操作之间存在顺序依赖关系。它的开发和实现具有挑战性。
OT是一组复杂算法,其正确实现在实际应用中已被证明有挑战性。Google Wave 团队花两年时间实现OT。
6.2 无冲突复制数据类型 (CRDT)
是为了改进 OT。CRDT 具有:
- 复杂的数据结构
- 但简化的算法
CRDT 通过为每个字符分配两个关键属性来满足交换律和幂等性:
- 为每个字符赋予全局唯一标识
- 全局订购每个字符
每个操作现在都有一个更新后的数据结构:CRDT 简化的数据结构
SiteID 唯一标识请求操作的用户站点,带有一个值和一个 PositionalIndex。PositionalIndex值可以是分数:
- 某些操作不会改变其他字符的 PositionalIndex
- 避免不同用户操作之间的顺序依赖性
示例描绘来自站点 ID 123e4567-e89b-12d3 的用户在 PositionalIndex 为 1.5 的位置插入一个值为 A 的字符。尽管添加了新字符,但使用小数索引保留了现有字符的位置索引。因此,避免了操作之间的顺序依赖性。如下所示,在 O 和 T 之间插入()并没有影响 T 的位置。
防止操作之间的顺序依赖性:
CRDT 确保用户之间的强一致性。即使一些用户处于离线状态,当他们重新联机时,最终用户处的本地副本也将汇聚。
尽管众所周知的在线编辑平台如 Google 文档、Etherpad 和 Firepad 使用 OT,但 CRDT 使协作文档编辑中的并发和一致性变得容易。事实上,使用 CRDT,可以实现无服务器点对点协作文档编辑服务。
6.3 注意
OT 和 CRDT 是协作编辑中冲突解决的良好解决方案,但我们使用 WebSockets 可高亮协作者的光标。其他用户也能预期该协作者的下一个操作的位置,并自然和自觉地避免冲突。
7 总结
7.1 一致性
操作转换(OT)和冲突不定决议数据类型(CRDT)在文档中实现冲突解决的强一致性。
时间序列数据库能保留事件的顺序。一旦 OT 或 CRDT 解析了任何冲突,最终结果就保存在数据库。这有助我们在单个操作方面实现一致性。
在IDC内的不同服务器之间保持文档状态的一致性。要在同一IDC内同时复制更新后的文档状态,可使用 Gossip 协议这样的点对点协议。这不仅提高一致性,还会提高可用性。
7.2 可用性
我们的设计通过使用副本以及使用监控服务监控主副本服务器来确保可用性。操作队列和数据存储等关键组件在内部管理自己的复制。
由于使用 WebSockets,WebSocket 服务器可将用户连接到会话维护服务器,这些服务器将确定用户是否正在主动查看或协作文档。
因此,保留多个 WebSocket 服务器将增加设计的可用性。最后,采用缓存服务和 CDN 改进可用性。
7.2 可扩展性
由于使用微服务,若操作队列的请求数量超过其容量,可轻松单独扩展每个组件。可使用多个操作队列。此时,每个操作队列将负责单个文档。可将不同用户请求的与单个文档相关的操作转发到特定队列。生成的队列数量将等于活动文档的数量。因此可实现水平扩展性。
参考:
- 编程严选网