Skip to content

Commit

Permalink
25/01/06
Browse files Browse the repository at this point in the history
  • Loading branch information
WindRunnerMax committed Jan 6, 2025
1 parent 40a7217 commit cf97301
Show file tree
Hide file tree
Showing 14 changed files with 96 additions and 77 deletions.
2 changes: 1 addition & 1 deletion .scripts/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export const docs: Record<string, string[]> = {
"RichText/初探富文本之划词评论能力",
"RichText/初探富文本之文档虚拟滚动",
"RichText/初探富文本之搜索替换算法",
"RichText/基于Slate构建文档编辑器",
"RichText/基于slate构建文档编辑器",
"RichText/WrapNode数据结构与操作变换",
],
Patterns: [
Expand Down
2 changes: 1 addition & 1 deletion Backup/Slate文档编辑器-Decorator装饰器.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,5 @@ for (let i = 0; i < leaves.length; i++) {
## 参考

- <https://docs.slatejs.org/>
- <https://github.com/ianstormtaylor/slate>
- <https://github.com/WindRunnerMax/DocEditor>
- <https://github.com/ianstormtaylor/slate/blob/25be3b/>
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,5 @@ export const WithContext: FC<{ editor: EditorKit }> = props => {
## 参考

- <https://docs.slatejs.org/>
- <https://github.com/ianstormtaylor/slate>
- <https://github.com/WindRunnerMax/DocEditor>
- <https://github.com/ianstormtaylor/slate/blob/25be3b/>
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,5 @@ export const isTextBlock = (editor: Editor, node: Node): node is TextBlockElemen
## 参考
- <https://docs.slatejs.org/>
- <https://github.com/ianstormtaylor/slate>
- <https://github.com/WindRunnerMax/DocEditor>
- <https://github.com/ianstormtaylor/slate/blob/25be3b/>
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
如果觉得还不错,点个`star`吧 😁

<!-- Summary Start -->
版本库中共有`491`篇文章,总计`92453`行,`1089067`字,`3036140`字符。
版本库中共有`491`篇文章,总计`92472`行,`1089168`字,`3036208`字符。
<!-- Summary End -->

这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字`EveryDay`就是希望激励我能够每天学习,下面的文章就是从`2020.02.25`开始积累的文章,都是参考众多文章归纳整理学习而写的,文章包括了`HTML`基础、`CSS`基础、`JavaScript`基础与拓展、`Browser`浏览器相关、`Vue`使用与分析、`React`使用与分析、`Plugin`插件相关、`RichText`富文本、`Patterns`设计模式、`Linux`命令、`LeetCode`题解等类别,内容都是比较基础的,毕竟我也还是个小。此外基本上每个示例都是本着能够即时运行为目标的,新建一个`HTML`文件复制之后即可在浏览器运行或者直接可以在`console`中运行。
Expand Down Expand Up @@ -303,7 +303,7 @@
* [初探富文本之划词评论能力](RichText/初探富文本之划词评论能力.md)
* [初探富文本之文档虚拟滚动](RichText/初探富文本之文档虚拟滚动.md)
* [初探富文本之搜索替换算法](RichText/初探富文本之搜索替换算法.md)
* [基于Slate构建文档编辑器](RichText/基于Slate构建文档编辑器.md)
* [基于slate构建文档编辑器](RichText/基于slate构建文档编辑器.md)
* [WrapNode数据结构与操作变换](RichText/WrapNode数据结构与操作变换.md)

## Patterns
Expand Down
2 changes: 1 addition & 1 deletion RichText/WrapNode数据结构与操作变换.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,5 @@ const editor = el[key].child.memoizedProps.node;
## 参考

- <https://docs.slatejs.org/>
- <https://github.com/ianstormtaylor/slate>
- <https://github.com/WindRunnerMax/DocEditor>
- <https://github.com/ianstormtaylor/slate/blob/25be3b/>
12 changes: 6 additions & 6 deletions RichText/初探富文本之CRDT协同算法.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
协同编辑,可以支持多个用户同时编辑文档,不会因为用户并发修改导致冲突,而导致结果不一致甚至数据丢失的问题。协同编辑重点在于协同算法,主要有`Operational Transformation(OT)``Conflict-free Replicated DATA Type(CRDT)`两种协同算法。协同算法不需要是正确的,其只需要保持一致,并且需要努力保持你的意图,就是说协同算法最主要的目的是在尽可能保持用户的意图的情况下提供最终的一致性,重点在于提供最终一致性而不是保持用户的意图。当前石墨文档、腾讯文档、飞书文档、`Google Docs`都是基于`OT`协同算法的,`Atom`编辑器使用的是`CRDT`协同算法。

## CRDT协同算法
`Conflict-free Replicated DATA Type(CRDT)`协同算法的核心思想不在于解决冲突,而在于构造一种数据结构来避免冲突,避免了冲突就可以直接进行合并,最终得到文档内容。`CRDT`协同算法的目的是在尽可能保持用户意图的情况下,保持文档的最终一致性,举个例子,当`A`和`B`同时在文档的`L`处插入了不同的字符,那么谁插入的字符在前协同算法并不关心,其只需要尽可能地根据一定策略例如时间戳来判断究竟是谁的字符在前,但是最终计算出的结果即究竟谁的字符在前并不影响协同算法其关心的重点在于经过协同算法将用户产生的`Op`调度之后,在每个人面前呈现的文档内容是绝对一致的,这就是保持文档的最终一致性。从功能的角度上说,协同算法保证的是在多人同时在线编辑的情况下,由于每个人提交的内容是不一样的,就需要通过协同算法的调度,使得每个用户最终都能看到一样的内容。实际上在线文档本身就是一个数据一致性要求很强的项目,所以无论是使用`CRDT`算法还是`OT`算法来实现协同,保证最终一致性就是必须要考虑的基本内容。
`Conflict-free Replicated DATA Type(CRDT)`协同算法的核心思想不在于解决冲突,而在于构造一种数据结构来避免冲突,避免了冲突就可以直接进行合并,最终得到文档内容。`CRDT`协同算法的目的是在尽可能保持用户意图的情况下,保持文档的最终一致性,举个例子,当`A`和`B`同时在文档的`L`处插入了不同的字符,那么谁插入的字符在前协同算法并不关心,其只需要尽可能地根据一定策略例如时间戳来判断究竟是谁的字符在前,但是最终计算出的结果即究竟谁的字符在前并不影响协同算法其关心的重点在于经过协同算法将用户产生的`Op`调度之后,在每个人面前呈现的文档内容是绝对一致的,这就是保持文档的最终一致性。从功能的角度上说,协同算法保证的是在多人同时在线编辑的情况下,由于每个人提交的内容是不一样的,就需要通过协同算法的调度,使得每个用户最终都能看到一样的内容。实际上在线文档本身就是一个数据一致性要求很强的项目,所以无论是使用`CRDT`算法还是`OT`算法来实现协同,保证最终一致性就是必须要考虑的基本内容。

由于`CRDT`设计上可以完成对于各个副本的合并与更新而不会产生冲突,那么经由`CRDT`实现的算法就可以直接在客户端之间互相传递,相互同步至最终一致性的状态,也就是各个用户之间可以直接`P2P`进行数据合并而不需要中央服务器进行调度,由此`CRDT`可以很好地支持去中心化的应用,即使没有中心化服务器各端之间也能完成同步。

Expand Down Expand Up @@ -82,7 +82,7 @@

那么`CRDT`究竟代表了什么,其是如何做到合并时不会出现冲突的。在分布式系统中复制有两种实现途径,一种是在主节点和从节点之间复制全量状态,还有一种是就是复制操作本身。简单解释一下,全量复制状态类似于我们把当前正在编写的整个文档都复制出来同步到其他客户端,而复制操作本身类似于我们只把当前编辑时造成的`Op`复制出来同步到其他客户端。那么如果是复制状态,也就是全量复制,就需要有一些状态收敛规则,因此我们就可以创建`Convergent Replicated Data Types`,也就是`CvRDT`基于状态的收敛复制数据类型,也被称为`State-based CRDT`。而如果是复制操作,那么这个操作就需要被设计为`Commutative`可交换的,而并不需要进行操作变换,由此可以创建`Commutative Replicated Data Types`,也就是`CmRDT`可交换复制数据类型,也被称为`Op-based CRDT`。在这两种情况下,目标都是通过确保操作彼此不会冲突独立发生从而为了避免协调,所以可以总称为`Conflict-free Replicated Data Type`,也就是`CRDT`。

#### State-based CRDT
#### State-Based CRDT
基于状态的`CRDT`,名字听着很吓人,实际上突然理解起来也挺吓人,但是拆开看就没有那么难以理解了。`State-based CRDT`的结点和结点传输的是状态,存在一个算子`Merge: (SA, SB) => SC`, 收到传输的状态就和自己存储的状态`Merge`,这个算子必须满足交换律、结合律和幂等律,所以如果用抽象代数的角度形容地话,其使整个状态系统形成了一个半格。所以最终只要能接受到其他所有节点的新状态,那么永远能保证系统最终会收敛,由此并不会发生冲突。这样也去除了对于`CmRDT`的底层通讯机制依赖,但是因为传递的是状态,所以最后传输可能有点大,当然也是存在优化策略的。

上边这段话可能有些难以理解,我们慢慢拆开来研究一下,首先我们看下算子`Merge: (SA, SB) => SC`,这也就是说我们同步状态的内容是需要合并的,这也就是我们做`State-based CRDT`的目的,即收到传输的状态就和自己存储的状态`Merge`。接下来就是我们这个算子需要保证的条件了,先来回顾一下交换律、结合律和幂等律:
Expand Down Expand Up @@ -124,7 +124,7 @@

在经历了半格、偏序集之后,似乎我们最终又回到了交换律、结合律和幂等律,实际上我们在半格中也提到了,只要我们能够定义一个有意义的最小上界`LUB`函数,我们就能创建`CRDT`。如果上边的`Max(x, y)`不是很直观的话,我们在本文数据结构一节中会有更多的数据结构示例来解释这个问题。或许在富文本的数据结构中并没有`Max(x, y)`这么简单地能够满足半格条件的实现方法,但是别忘了我们可以为数据附加额外的信息,在我们前边提到了`CRDT`是通过数据结构保证了编辑的无冲突,从而增加了空间复杂度,而数据结构的设计很重要的一点就是携带其他的信息,例如时间戳、逻辑时间戳、用户唯一`id`等等,通过附带元信息的方式是让其更新保持单调,从而能够构建一个带有最小上界偏序关系的半格。

#### Op-based CRDT
#### Op-Based CRDT
基于操作的`CRDT`,实际上类似于上边的`State-based CRDT`,只不过我们操作的对象变成了操作`Op`的复制与同步。那么同样对于操作,我们也需要为其设计为符合交换律、结合律与幂等律的,因为即使传输的对象从状态转换到了操作,也是需要经过网络进行传输的,那么就不可避免地出现了上述分布式系统必须要解决的问题,所以传输的操作也是需要经过设计的,而使用`Op-based CRDT`能够获得的明显提升是同步过程中需要传输的数据显著降低。

那么当前客户端的更新操作本身满足以上三律,那么复制过后的操作同步到其他客户端的时候仅需要对传输过来的操作进行回放即可,最简单的例子是集合求并集。如果本地的客户端操作无法满足上述的条件,则可以考虑将元信息附加到操作`Op`上从而满足三律条件。同样的,如果我们能够保证`Exactly once`的语义,则幂等律条件是可以放宽的。如果依旧无法满足的话,则可以考虑同步副本数据即`State-based CRDT`,同时附带额外元信息,通过元信息让本地更新与其他客户端合并操作具备以上三律。有趣的是,使用基于操作的方式总是能够模拟基于状态的方式。
Expand All @@ -147,12 +147,12 @@
### 数据结构
下面是一些典型的`CRDT`数据结构的例子,可以帮助我们理解`CRDT`的设计思想,要注意的是我们在此处的例子是没有中央服务器调度以及数据存储的的,数据都是在各个客户端之间进行同步。

#### Op-based Counter
#### Op-Based Counter
`Op-based Counter`表示了一个整数计数器,假设此时我们有`A``B`两个客户端共同维护一个计数器,也就是说我们两个客户端都存储了一个整数,假设当前值为`10`,那么此时`A`进行了`increment`操作,`B`进行了`decrement`操作,那么我们就可以暂时地得到`A`的值为`11``B`的值为`9`,之后便要进行数据同步,以便使得`A``B`得到最终一致性的`10`这个值,那么我们就可以在网络中同步`A``increment`操作,`B``decrement`操作,这样就能够使得`A``B`的值都变为`10`

看起来这个操作是很容易实现的,那便是因为加法天然满足交换律和结合律,减法也同样可以看作是加法,只不过是加一个负值,那么在这个例子中需要注意的点是加法不幂等,所以同步过程中需要保证不丢不重。

#### Grow-only Counter
#### Grow-Only Counter
`State-based Counter`在组织形式并非那么的显而易见,在这一小节我们先从`Grow-only Counter`看起,也就是只增的计数器。首先明确的概念是我们此时是将数据全量同步的`CRDT`实现,由于同步的是全量,如果每个副本单独进行累加,在进行合并的时候无法知道每个副本具体累加了多少,也不能简单的取一个`max`作为最终结果,例如此时我们有`A``B`两个客户端共同维护一个计数器,初始时两个客户端数据都是`0`,此时`A`增加了`1``B`增加了`2`,那么全量同步数据时如果`max(1, 2) = 2`是不行的,因为我们实际上想得到的数据是`3`。以上,虽然我们通过`max`设计的数据结构是符合了交换律结合律和幂等律,但是这个设计与我们的目的是相悖的,我们想得到的是一个计数器,而不是获得客户端的最大值。

那么一种可行的方案是在每个副本上都使用一个数组保留其它所有副本的值,本地更新时只操作当前副本在数组中对应项,合并只能修改数组中除了当前副本的其他项目,并且对数组每一项求`max`进行合并,在查询时将本地所有副本的值求和,即为我们想要的计数器`Counter`。此时我们有`A``B`两个客户端共同维护一个计数器,初始时两个客户端数据都是`0`,那么`A`维护的数据是`[0, 0]``B`维护的数据是`[0, 0]`,此时`A`增加了`1``B`增加了`2`,那么`A`就变成了`[1, 0]``B`变成了`[0, 2]`,当数据同步之后,`A`即为`[1, 2]``B``[1, 2]`,这样两个客户端的数据就是一致的了,最终的计数为`3`
Expand All @@ -164,7 +164,7 @@

由此带有`Decrement``State-based CRDT`也并非像`G-Counter`那样显而易见,带有减法之后,不能满足更新时时单调的偏序关系,所以在这里一种可行的方式是构造两个`G-Counter`,一个存放`Increment`的累加值,一个存放`Decrement`的累加值,通过两组带有偏序关系的`CRDT`来达到我们维持三律的目的。

#### Grow-only Set
#### Grow-Only Set
`Grow-only Set`表示的是一个仅增的集合,`Set``Add`操作本质上是求并,天然满足交换律、结合律和幂等律, 可以很简单地使用`Op-based CRDT`,此时也不需要与上述的`Op-based Counter`一样需要保证不丢不重,因为集合求并集是幂等的,当然也可以使用`State-based CRDT`进行全量的数据求并集,此时每个客户端的副本也只需要保存一份集合即可。

#### Two-Phase Set
Expand Down
Loading

0 comments on commit cf97301

Please sign in to comment.