Notion 的数据协同原理

3,065 阅读7分钟

Notion 是一款十分强大的软件,它是一个将笔记,知识库和任务管理无缝衔接整合的协作平台。Notion 使用起来十分方便,它打破了传统的笔记软件对于内容的组合方式,将文档的内容分成一个个 Block,并且能够以乐高式的方式拖动组合这些 Block,这让它使用起来十分灵活。本篇文章来聊聊 Notion 是如何处理数据协同的。

文档结构

在讲解 Notion 的数据协同之前,需要先了解一下 Notion 的文档结构。Notion 的文档结构是由若干个 Block 组成,Block 之间可以进行嵌套,形成一个树状结构。Block 有各种各样的类型,比如:页面,文本,图片,代码块等等。Block 的操作方式也极其简单,直接使用鼠标进行拖拽,就可以随意重新排列他们的位置。 下面是一个简单的 Notion 文档的数据结构,content 属性是子 Block 的 id 列表,properties.title 中是 Block 的内容。

{
    "3c4b5dc4-2ced-4346-b7c7-d7bcabbfff34":{
        "value":{
            "role":"read_and_write",
            "value":{
                "id":"3c4b5dc4-2ced-4346-b7c7-d7bcabbfff34",
                "version":481,
                "type":"page",
                "content":[
                    "9b6281c8-e546-4929-9242-0be3e8b61908",
                    "29e945d6-3e2c-4c7a-bd1f-9d6dbdd0059b",
                    "369797d8-8bf6-4794-9502-4ff1d4b9523d"
                ],
                "format":{
                    "page_icon":"🎀"
                },
                "permissions":[
                    {
                        "role":"editor",
                        "type":"user_permission",
                        "user_id":"115ca767-ba0d-48f7-a5ae-af1f91bcf44f"
                    },
                    {
                        "role":"read_and_write",
                        "type":"public_permission",
                        "added_timestamp":1644578023971
                    }
                ],
                "created_time":1639709100000,
                "last_edited_time":1646054580000,
                "parent_id":"83fcf30c-43b6-40b3-b0a5-964380955b82",
                "parent_table":"space",
                "alive":true,
                "created_by_table":"notion_user",
                "created_by_id":"115ca767-ba0d-48f7-a5ae-af1f91bcf44f",
                "last_edited_by_table":"notion_user",
                "last_edited_by_id":"070b853c-1e8d-4a4d-95f2-7f130d94e5f0",
                "space_id":"83fcf30c-43b6-40b3-b0a5-964380955b82"
            }
        }
    },
    "9b6281c8-e546-4929-9242-0be3e8b61908":{
        "value":{
            "role":"read_and_write",
            "value":{
                "id":"9b6281c8-e546-4929-9242-0be3e8b61908",
                "version":125,
                "type":"text",
                "properties":{
                    "title":[
                        [
                            "1"
                        ]
                    ]
                },
                "created_time":1644822360000,
                "last_edited_time":1646054520000,
                "parent_id":"3c4b5dc4-2ced-4346-b7c7-d7bcabbfff34",
                "parent_table":"block",
                "alive":true,
                "created_by_table":"notion_user",
                "created_by_id":"070b853c-1e8d-4a4d-95f2-7f130d94e5f0",
                "last_edited_by_table":"notion_user",
                "last_edited_by_id":"070b853c-1e8d-4a4d-95f2-7f130d94e5f0",
                "space_id":"83fcf30c-43b6-40b3-b0a5-964380955b82"
            }
        }
    },
    "29e945d6-3e2c-4c7a-bd1f-9d6dbdd0059b":{
        "value":{
            "role":"read_and_write",
            "value":{
                "id":"29e945d6-3e2c-4c7a-bd1f-9d6dbdd0059b",
                "version":8,
                "type":"text",
                "properties":{
                    "title":[
                        [
                            "2"
                        ]
                    ]
                },
                "created_time":1644924840000,
                "last_edited_time":1646054580000,
                "parent_id":"3c4b5dc4-2ced-4346-b7c7-d7bcabbfff34",
                "parent_table":"block",
                "alive":true,
                "created_by_table":"notion_user",
                "created_by_id":"070b853c-1e8d-4a4d-95f2-7f130d94e5f0",
                "last_edited_by_table":"notion_user",
                "last_edited_by_id":"070b853c-1e8d-4a4d-95f2-7f130d94e5f0",
                "space_id":"83fcf30c-43b6-40b3-b0a5-964380955b82"
            }
        }
    }
}

数据协同

Notion 的数据协同是 Block 级别的协同,如果多个用户同时修改的是同一篇文档中不同的 Block,则不需要做冲突处理。如果多个用户同时修改的是同一个 Block,才需要做冲突处理。 Block 的协同是 Block 级别的全量更新,即后写入的数据会直接全量覆盖之前的数据。因此,相较于 OT 协同,Block 协同不要求版本号连续,只要版本号越大,Block 的状态就越新,业务层只需要去获取最新的 Block 数据即可。

版本号的作用

在 Block 协同中,版本号的作用相对 OT 协同中较弱,版本号的递增完全由后台控制,后台每收到一次前端修改 Block 的操作,将该操作应用之后,就会将 Block 的版本号加1。版本号在 Block 协同中的作用主要是:

  1. 用于服务端以乐观锁的方式写数据;
  2. 用于前端判断某个 Block 是否为最新版本,如果不是最新版本,就立即从服务端拉取最新数据;

数据操作

Notion 中编辑 Block 内容时一共只有五种原子操作,即上述后台收到的前端修改 Block 的操作,如下:

  • set: 设置某个 Block 的一组属性,如果 Block 或者属性不存在,就创建它。通常用于新建或者覆盖式更新一个 Block;
  • update: 更新某个 Block 的部分属性。通常用于更新 Block 的创建修改时间,版本,样式等;
  • listRemove: 将某一条数据,从数组属性中移除。通常用于将某 Block 从其父级 Block 的子节点列表中移除,也用于改变子节点顺序时,先 移除插入移除 操作。
  • listAfter: 将某一条数据,插入到数组属性值中的某个元素的后面。通常用于拖拽元素时,先 移除插入插入 操作。
  • listBefore: 将某一条数据,插入到数组属性值中的某个元素的前面。Notion 中拖拽行为默认使用 listAfter 操作,listBefore 通常用于 listAfter 无法表示的情况。 用户编辑任何内容的操作都是上述几种原子操作的组合,前端不会直接发送原子操作到后台,而是发送这些原子操作的组合,Notion中称之为 Transaction。比如拖拽 Block 中的子元素,就是 listRemove 和 listAfter 的组合。

全量协同的操作方式

Notion 中,用户对 Block 的修改主要有两类操作:改变 Block 的属性(文本内容,样式等)和改变 Block 子节点(改变子节点顺序及增删子节点)。 改变 Block 的内容时,会直接用新值替换旧值,所以,多人同时编辑同一 Block 内容时,就会出现覆盖式更新的现象,后面人写的内容会覆盖前面人写的内容。 改变 Block 子节点的顺序,就是对 Block.content 数组进行删除和插入操作,该操作是依据指定子节点的 Block id 来进行,和 OT 协同中依据 index 进行插入和删除不同。所以,不同的人移动同一 Block 下的子节点时,也可以无冲突直接写入。 由于移动某一 Block 时,一般都是插入到某个已存在的 Block 的前面或者后面。在多人同时编辑的时候,可能会出现该指定的 Block 或者需要移动的 Block 被删除的情况。如果指定的 Block 被删除了,那么会将需要移动的 Block 移到父元素子节点列表的最后面,如果需要移动的 Block 被删除了,则该移动操作无效。 在后台,所有的操作都并行处理,不同 Block 间的操作互不影响,相同 Block 的操作用乐观锁防止数据更新丢失。 以上就是 notion 中基于 Block 的全量协同。

数据协同时序图

前端收到协同消息时,会先判断协同消息中 Block 的版本号是否大于本地相应 Block 的版本号,如果大于,则去服务端拉取该 Block 最新的数据。下面是几种不同编辑操作情况下,前端处理协同数据的时序图。

当只有用户 A 编辑某一 Block 时: 企业微信截图_00663a5c-9b46-4698-9c19-8ee49b7bb983.png 当用户 A 和 用户 B 同时编辑同一 Block 时: 企业微信截图_117ca4a0-01b7-4538-b5e3-b0fb96f2059c.png 当用户 A 删除某 Block,用户 B 移动该 Block 时: 企业微信截图_24191479-30ed-4c3a-b22f-b02080daa253.png 当用户 B 移动 Block1 到 Block2 后面时,后台会返回失败,当收到编辑失败时,前端会直接去拉取最新的数据覆盖本地数据。

总结

Notion 基于 Block 编辑排版的设计十分精妙,打破了固有的文档设计思维,Notion 的数据协同设计也相较于传统的 OT 协同要简单很多。但是这种简单的数据协同处理会在用户体验上有一定的影响。