非常简单地学习一下shareDB的原理

shareDB概述

在在线文档编辑器和多玩家游戏等场景中,都需要协同机制去解决数据一致性问题。在协同实现上,目前市面上主要有以下两种常用算法:

  1. OT算法 :用户在客户端对data(数据)的每一个操作都会当作op 发送到服务器,服务器会对op 进行变换(transform)后再广播到其他用户的客户端中,这些客户端会应用这个op 到其本地数据上,从而实现数据一致。目前shareDB就是使用这个算法
  2. CRDT算法 :用户本地的data(数据) 是一种自带了冲突解决机制的数据,当用户对本地数据进行操作后,客户端把数据发送到别的用户的客户端,然后这些客户端会将收到的数据与本地数据进行合并。目前yjs就是使用这个算法

总结来说,OT算法是改操作,让操作能合在一起 ;CRDT算法是改数据结构,让数据自己能合在一起

因为本文核心主题是shareDB,而shareDB又使用了OT算法 实现协同,因此下面我们会着重说一下OT算法

OT算法概述

OT算法中主要提供以下两个方法:

  1. apply : 用于把op 应用到data 上,得到新的data' 。例如我们在字符串"13"中间输入"2",那就会产生一个op ,这个op 可以表示为{value:'2', action:'insert', index: 1},这个op 就可以通过apply 操作应用到"13"上,得到"123"

    伪代码如下:

    js 复制代码
    console.log(doc.data) // 输出: '13'
    ot.apply(op, doc)
    console.log(doc.data) // 输出: '123'

    举一个场景:客户端A和客户端B访问同一份文档编辑,客户端A对文档做了变动,该变动同步到客户端B中。则流程图如下:

    但如果客户端A和客户端B对文档的同一位置做了变动,则会产生冲突,则会导致两个客户端的文档内容不一致,如下流程图:

    这种情况下,就需要用到OT算法 中另外一个逻辑transform来解决冲突了

  2. transform : 用于把op 转换成成适配当前内容的无冲突的op'。还是上面冲突的例子,如下伪代码所示:

    js 复制代码
    console.log(op) // 输出: {value:'4', action:'insert', index: 2}
    op1 = ot.transform(op, doc) // {value:'4', action:'insert', index: 3}
    ot.apply(op, doc)
    console.log(doc.data) // 输出: '1234'

    流程图如下:


但上述场景这里会存在一个问题:客户端A在获取到从客户端B同步过来的op 时,怎么知道这个op 需要transform 后才apply

这里就需要用到时钟变量 来判断同步过来的op是否落后于当前版本,如下伪代码:

js 复制代码
/**
 * 输出:
 * {
 *    // data代表操作的具体内容
 *    data: {value:'4', action:'insert', index: 2},
 *    // version代表客户端B在产生但还没apply该op时的版本号
 *    version: 10
 * }
 */
console.log(op) 
// 判断op.version是否小于当前文档版本号,如果小于则代表有冲突
if(op.version < doc.version) {
    op = ot.transform(op, doc)
}
ot.apply(op, doc)

上述代码中加了版本号作为时钟变量 去判断。而在目前行业中,围绕这个时钟变量的设计,OT算法衍生出了多个分支,其中主要存在以下两个分支:

  1. 中央服务器 + 单一标量 : 用一个中央服务器去维护作为时钟变量 的文档版本,同时每个客户端也维护各自的文档版本。所有客户端的op 都需要先同步到中央服务器,中央服务器通过对比op 的版本和本地快照的版本来决定是否需要transform,然后再由中央服务器分发到各个客户端。如下流程图所示:

    这也是shareDB采用的方案

  2. 去中心化网络 + 状态向量 : 每个客户端维护一个向量作为时钟变量 。向量的维度等同于当前协同人数。中央服务器只负责转发op 。客户端收到op 后会比较向量的大小来决定是否需要对其进行transform。如下流程图所示:

    其实整个流程中有中央服务器做参与,而非p2p的同步。但因为中央服务器只做转发。所以上述流程图我直接忽略中央服务器的角色了

shareDB概述

上一章节也说了shareDB的协同机制是基于OT算法中央服务器 + 单一标量 方案实现的。但其实shareDB内部的OT算法 是第三方库实现的。当我们在后端初始化shareDB时,可以注册新的OT类型:

js 复制代码
var ShareDB = require('sharedb');
var json1 = require('ot-json1');

// 注册json1类型。shareDB默认已有json0类型
ShareDB.types.register(json1.type);
var backend = new ShareDB();
var connection = backend.connect();
var doc = connection.get('examples', 'counter');
// 创建文档的时候,在第二参数里指定使用json1类型。缺省时默认使用json0
doc.create({numClicks: 0}, json1.type.uri);

ot-json1这类第三方的OT 库会为我们提供applytransform 的具体实现逻辑。至于shareDB本身专注于op 同步、订阅分发、快照与op 的存储、时钟变量的维护等等这些框架层的事

例如我们来看看客户端初始化shareDB逻辑时的示例代码:

js 复制代码
var ReconnectingWebSocket = require('reconnecting-websocket');
var sharedb = require('sharedb/lib/client');

// Open WebSocket connection to ShareDB server
var socket = new ReconnectingWebSocket('ws://' + window.location.host, [], {
  // ShareDB handles dropped messages, and buffering them while the socket
  // is closed has undefined behavior
  maxEnqueuedMessages: 0
});
var connection = new sharedb.Connection(socket);

// 创建名为'counter'的文档。放在名为'examples'的数据集里
var doc = connection.get('examples', 'counter');

// 与后端建立订阅关系
doc.subscribe(showNumbers);
// 监听op事件,当op事件触发时,会调用showNumbers函数
// 注意这里注册的回调函数会在op(无论本地还是远端的op)apply了后才触发
doc.on('op', showNumbers);

function showNumbers() {
  document.querySelector('#num-clicks').textContent = doc.data.numClicks;
};

function increment() {
  // 关于json0的op数据结构可看https://github.com/ottypes/json0 
  doc.submitOp([{p: ['numClicks'], na: 1}]);
}

// Expose to index.html
globalThis.increment = increment;

总结来说,就是

  1. 调用doc.subscribe建立订阅关系
  2. 通过doc.on('op', callback)监听op事件的回调函数,使其会在每次本地或远端op apply后执行从而实现页面响应式变化
  3. 通过doc.submitOp提交本地op

调用doc.subscribe建立订阅关系时,其执行逻辑的流程图如下所示:

而当客户端A执行doc.submitOp后触发客户端B的doc.on('op', callback)的执行逻辑的流程图如下所示:

中间件可以用于权限校验和新增元属性,详细可看Middleware actions

相关推荐
认真的薛薛1 小时前
阿里云: A记录 & CNAME
服务器·前端·阿里云
2301_815645381 小时前
css基础
前端·css
Hilaku1 小时前
求求你们🙏 ,别再换打包工具了?
前端·javascript·程序员
用户新1 小时前
V8引擎 精品漫游指南--Ignition篇(下 二) JavaScript 栈帧详解
前端·javascript
账号已注销free1 小时前
box-shadow完整用法
前端
得闲喝茶1 小时前
JavaScript在数据处理的应用
开发语言·前端·javascript·经验分享·笔记
前端那点事2 小时前
Vue3 script setup 语法糖最全教程!零基础吃透+项目落地+面试满分
前端·vue.js
ConardLi2 小时前
Harness 实践:让 Agent 全自动制作知识讲解视频
前端·人工智能·后端
费曼学习法2 小时前
React Hooks 源码级揭秘:为什么必须按顺序调用?
javascript·react.js