如何实现在线Excel多人协作

2022-08-25 13:58:32 浏览数 (1)

引言:结合工作实践和自己的一些思考,今天和大家分享在线Excel的协作方案。

如果你对在线文档的主题感兴趣还可以看这两篇文章:如何实现多人协作的在线文档,在线Excel存储方案

场景

多个用户同时操作一个Excel文件。 场景中的实体有:用户、Excel。其中用户又分为「拥有者」「阅读者」「协作者」 拥有者:创建Excel的用户 阅读者:可以查看Excel的用户 协作者:可以编辑Excel内容的用户

创建领域模型

  1. 一个Excel只有一个拥有者,但是可以有多个阅读者和协作者
  2. 一个Excel可以被多个阅读者或协作者同时访问
  3. 一个Excel可以被多个协作者同时编辑
  4. 一个Excel可以被拥有者删除

过程分析

协作的关键过程有: 「用户打开Excel」 「用户编辑Excel」 「用户退出Excel」 「用户删除Excel」 在所有的关键过程中,既需要客户端往服务端发送消息,也需要服务端往其他客户端广播消息。而且当用户频繁修改Excel内容时,为了保证每个人修改的内容实时同步到其他客户端,会有频繁的网络传输。这很像一个聊天室。在这种场景下长链接是比较合适的方案,「WebSocket」是实现长链接的常用方案之一。

和聊天室不同的是,聊天室更倾向于AP模型;在线Excel更倾向于CP模型,因为消息丢失或顺序不对,会导致文件内容错误,后果很严重。

以上这些关键过程的实现都需要知道一个Excel文件有多少人正在阅读、编辑。记录当前Excel的在线用户,才能在Excel内容变化时把变化的内容广播给他们。

Excel在线用户

当前有「多少人在协作」是实时变化的数据,而且需要频繁、高效的访问,使用redis存储比较合适。我们可以使用redis的Hash类型存放,Excel的唯一ID作为Key,把在线用户、打开文件时间等信息存储起来。

代码语言:javascript复制
hset excel_id user_id "打开时间"

其他的存储类型,或redis的其他存储方式都是可以的。

状态广播

WebSocket连接建立之后,客户端会和服务端的某一个副本「保持」长链接。用户打开Excel或者修改Excel内容,都需要根据当前excel_id,去redis中查找「在线用户」,然后发送「广播消息」,把状态变化同步到所有客户端。此场景下广播消息的发送有三种实现方案:

方案一:exce_id路由

excel服务的所有请求,根据exce_id路由,这样同一个exce_id上的所有长链接都会在同一个副本上。需要发送广播消息时,当前exce_id的所有长链接都在此副本上,代码层面不用做任何特殊处理。

优点:实现简单,不侵入业务代码 缺点:

  1. 无法动态扩容,即使增加了副本,某个exce_id的请求还是打在原来副本上
  2. 负载均衡不友好,如果在某个副本上exce_id的用户数都偏多,会导致单个副本链接数过多,其他副本可能会比较空闲
  3. 如果一个Excel协作人数特别多,可能会导致副本cpu或内存被打满;换句话说,一个副本的上限决定了Excel能支持的同时在线人数
  4. 无法抽离单独的WebSocket网关
  5. 长链接本来就是有状态的,把服务的状态和副本绑定了,相当于把状态放大了
方案二:事件广播

需要发送广播消息时,Excel所有副本都根据exce_id从redis中获取在线用户,对比当前副本持有链接的Sessions中是否存在此用户信息。如果存在则向此链接发送广播消息,如果不存在就忽略不做处理。

有广播消息时对其他所有副本发送通知,可以采用消息队列来实现。让所有副本订阅某频道,有广播消息时,通过消息队列通知到其他副本。 除了消息队列还可以根据应用ID调用云平台的接口返回所有pod的VIP,然后根据VIP给所有副本发送请求。

建议采取消息队列的方案,减少对云平台的依赖。

优点:

  1. 可以动态扩容
  2. 解耦Excel和副本
  3. 不影响负载均衡
  4. 可以有单独的网关层

缺点:

  1. 需要引入消息队列,增加了系统的复杂性
  2. 侵入业务逻辑,副本需要自己判断广播是否由自己发送
  3. 导致很多对redis的无效请求,广播频繁发送会给线上环境带来较大的压力(因为一次广播并不一定牵扯到所有副本)
方案三:注册中心,统一管理,指定发送

由注册中心管理excel、用户以及副本的长链接关系,需要发送广播时,根据excel_id获取所有需要广播副本的vip/Host,调用其服务给客户端推送广播消息。

优点:

  1. 可以动态扩容
  2. 解耦excel和副本
  3. 不影响负载均衡
  4. 可以有单独的网关层
  5. 基本不侵入业务逻辑

缺点: 需要引入注册中心,增加了系统的复杂性,增加了运维成本

关键逻辑

用户打开Excel

当某用户打开Excel时,需要同步此用户的信息到所有正在阅读或协作此文档的客户端。这时的交互流程如下。

  1. 用户在浏览器中打开Excel文件,并发送请求到服务端
  2. 根据excel_id,在redis中查找所有在线用户
  3. 如果没有找到数据,说明当前没有人打开此Excel,把自己插入redis中,执行完毕
  4. 如果查找到数据,把自己添加到当前记录中
  5. 给所有除自己外打开此文档的「链接」推送消息
  6. 其他客户端接收到服务端的消息后,在页面上显示登录用户头像
  7. 执行完毕

用户操作Excel

用户对Excel的操作类型特别多,比如修改单元格内容、修改行宽、增加列、合并单元格等等。我们把用户对Excel的所有操作归为两类:1.「修改单元格内容」 2.「其他操作」

修改单元格内容

对于修改单元格内容的操作我们采用互斥逻辑。互斥逻辑分为锁定、取消锁定、发送内容三部分。

锁定逻辑
  1. 当用户选中某个单元格时,前端把选中信息发送到服务端
  2. 服务端根据「excel_id和当前单元格坐标」取锁,取锁成功进行下一步;如果取锁失败,给当前用户返回此单元格正在被A用户编辑
  3. 服务端根据excel_id获取当前在线用户,发起事件广播
  4. 其他客户端收到广播消息后,在单元格右侧标识操作人的用户信息,同时禁止当前用户操作此单元格
  5. 执行完毕
取消锁定
  1. 当单元格失去焦点时,客户端向服务端发送消息,服务端根据「excel_id和当前单元格坐标」释放锁
  2. 服务端根据excel_id获取当前在线用户,发起事件广播
  3. 客户端收到广播消息后,在单元格右侧移除操作人的用户信息,允许其他用户操作此单元格
  4. 执行完毕
内容修改
  1. 当用户修改完单元格内容时,发送请求到服务端
  2. 服务端根据「excel_id和当前单元格坐标」取锁,取锁成功进行下一步;如果取锁失败,给当前用户返回此单元格正在被A用户编辑
  3. 服务端根据excel_id获取当前在线用户,发起事件广播
  4. 其他客户端收到广播消息后,根据广播内容和当前表格内容重新渲染表格
  5. 执行完毕
补充

如何判断取锁成功? 「excel_id和当前单元格坐标」不存在时说明没有用户操作此单元格,取锁成功。 「excel_id和当前单元格坐标」存在时,可以把用户ID当作锁的Value值,比较Value是否为当前用户,如果是也认为取锁成功,可以修改单元格内容。

加锁时设置默认超时时间,防止单元格内容被永远冻结。

此外还存在间隙问题:用户在客户端选中一个单元格后,“请求到服务端加锁,然后发送广播到其他客户端“ 的间隙时间较长,这中间如果有用户快速修改了同一个单元格的内容,会存在内容被覆盖 或者 修改失败两种风险。我们可以根据自己使用Excel的业务场景,决定允许当前状况发生,或者通过优化取锁逻辑来处理。

其他修改

对于其他修改采用覆盖逻辑,时间靠后的操作,覆盖靠前的操作。

  1. 当用户选中某个单元格时,前端把选中信息发送到服务端
  2. 服务端根据excel_id获取当前在线用户,发起事件广播
  3. 客户端收到广播消息后,根据广播内容和当前表格内容重新渲染表格
  4. 执行完毕

采用覆盖逻辑的原因:用户的很多操作无法做合并。比如:A用户把单元格第一行高度由30px调整为50px;B用户把第一行高度由30px调整为40px。此时程序无法按照预期设置第一行单元格的高度

用户退出Excel

当一个用户退出Excel时,需要同步这个人的信息到所有正在阅读或协作此文档的客户端。用户主动退出操作包含:点击页面左上角的回退按钮、浏览器的回退按钮、关闭浏览器等。还有可能因为异常的网络中断导致用户退出,所有的退出操作对应到服务端,就是WebSocket链接断开。可以采用WebSocket服务端的close事件当作用户退出的标识。交互流程如下:

  1. 服务端WebScoket断开,触发close事件
  2. 服务端根据excel_id获取当前在线用户,如果没有找到数据,说明当前没有人打开此文档,删除redis中的在线用户记录,执行完毕;如果查找到数据,把自己从「在线用户列表」中删除,执行下一步
  3. 给所有除自己外打开此文档的链接推送消息
  4. 客户端接收到服务端的消息后,在页面上「在线用户显示列表」中,删除此用户或者标记为下线状态
  5. 执行完毕

用户删除Excel

  1. 客户端发起删除请求
  2. 服务端验证删除权限是否通过,通过继续执行,不通过返回没有权限
  3. 根据excel_id,在redis中查找所有在线用户。
  4. 如果没有找到数据,说明当前没有人打开此文档,删除redis中的记录,执行完毕
  5. 如果查找到数据,给所有除自己外打开此文档的链接推送消息,
  6. 客户端根据消息给用户弹框提示,excel已被删除
  7. 执行完毕

存在的问题

此方案并没有解决协作中的所有问题,除了上文中已经提出的注意事项外,还有很多地方要注意。比如:遇到合并函数操作时,如何解决多个人操作的冲突?有人在修改一个单元格时,别的用户有合并单元格操作时如何处理?多个人同时修改一个单元格的逻辑能否优化? 消息传输层的问题尤其重要,需要单独说一下:

  1. 因为WebSocket消息是无序的,所以,以上场景依赖消息顺序时,都需要额外的保障机制
  2. WebSocket发送消息有可能失败,在服务端和客户端通信时,是否需要ACK机制?
  3. 如果建立了ACK机制,握手的另一方正好下线了如何处理?
  4. 链接异常断开又重新建立时,如何保证当前用户数据更新到最新状态?

总结

今天详细和大家介绍了,在线Excel协作的一些实现方案和关键流程,希望能起到抛砖引玉的作用。喜欢在线协作的同学可以一起来交流讨论。

0 人点赞