system desing 系统设计(十四): 文档协同编辑collaboration editing&springCloud微服务架构实现的抽象流程

   在线文旦都用过吧? 别人给个链接,用浏览器打开后,只要有相应的权限就能读写文档内容了!核心功能如下:

   

     1、在线文档一般用的都是浏览器打开链接,而不是自己单独出个app,目的就是利用浏览器存量用户多的优势,降低用户触达和使用的门槛。既然是用浏览器,有两个问题需要解决:

  •  url该怎么设计了?既要方便传播,又要安全!
  •    既然是collaboration协同编辑,怎么在不同的client做到编辑的内容real-time updata了?

  (1)大家常用的http或https协议底层都是基于tcp/ip协议,是无状态的,一般都是client给server发请求,然后server返回相应的数据。server都是被动相应的,不会主动给client发数据。那么问题来了:clientA改了文档内容,因为基于http/https协议的server不会主动push,clientB怎么real-time updata文档内容了?如果让clientB通过poll的方式轮询server,效率未免太低了吧!并且每个client都去poll后台服务器,服务器不得亚历山大啊!

  早期的http 1.0不支持长链接long connection,每次tcp 3次handshake后传完数据就要关闭connection,下次传输前又要重新再来3次handshake,效率低下。同时还有线头阻塞HeadOfLineBlocking问题,所以后来推出的http 1.1支持long connection,支持一个TCP链接可以传送多个http请求(Request)和响应(Response),也同时支持http管道pipeline,让开发人员可以把 FIFO 队列从客户端(请求队列 Request Queue)迁移到服务器(响应队列Response Queue),即客户端可以并行(parallel),服务端串行(Serial)。http1.1允许客户端不用等待上一次请求结果返回,就可以发出下一次请求【同步syncronize改成了异步asyncronize】,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果;基于以上特性,websocket协议问世了,最大的优势就是可以双工通信,做到数据实时更新,比如用浏览器炒股看盘时,用的就是webscoket协议,保证股价等敏感数据能实时更新!用了websocket后文档的实施更新就简单多了,流程如下:

        

  (2)接着说url的设计问题!文档都是存放在服务器的,如果对外分享文档时把文档的存储路径和名字都带上(比如这种:http://xxx.com/学习资料/苍老师.doc),感觉很不安全啊,泄露隐私了都!为了安全,需要做个转换,比如把关键路径和文件名hash一下,做成这种形式:http://xxx.com/wwuSA1cs4qtS,这样分享出去就安全多了!比如腾讯文档的格式就是这样的:    https://doc.weixin.qq.com/sheet/e3_m_WvGXAYVhULM?scode=VTSQIQarAAghD63LJzAQIAfgaDACc&tab=mr4sum 咋样?单从url肯定看不是啥吧!

  2、文档编辑,最重要的肯定是文档啦!大家想想在本地存储文档的时候,两类数据最重要:第一就是metadata,也就是文档的元数据,诸如文档名称、文档作者、创建时间、存放目录、最近修改时间、文档大小、文档类型等;第二就是文档本身了!文档的metadata都是结构化的数据,用mysql存就行了!文档本身了?当然是File system啦,诸如HDFS之类的是最合适不过的了!所以当创建文档时,整个过程如下:

  

    3、按照上述流程创建好了文档,又该怎么修改了?

 (1)既然是协同,如果只是同时读还好,但如果有多人同时编辑改写怎么办了?大伙第一个想到的肯定是上锁了!这就涉及到一个颗粒度: 是锁整个文档了?还是锁一行了?还是锁某个字?很明显,锁行的颗粒度是最合适的!如果是锁文档,一人编辑,其他人都干等? 如果是锁字符,后台压力大不大啊?所以综合考虑,还是锁行呗!既然是锁行,文档应该怎么在内存以什么样的数据结构存放才能便于锁行了?如下:

   

  最核心的数据结构莫过于双向链表了,文档结构转成双向链表格式说明如下:
  • uuid是每个client 生产的,怎么保证全局唯一了?client A第一个读取文档后,前端生产的uuid=11111会上传到服务器保存;clientB读取文档后,服务端会把前面的uuid=11111发给clientB;如果clientB改了文档,前端生产uuid=22222上传server;clientB自己要去重
  • uuid本质就是索引,核心是把各个行合并成完成的文件;【为啥不直接用指针了?uuid这种序列化的数字人容易看懂,便于后期的运维和错误排查嘛!直接给个内存地址,能弄清啥了?
  • value之间用的就是双向链表串联起来的,方便锁行和快速在链表的节点间删改链表

  (2)client修改某行后,肯定是要第一时间通知server的,通知可以以json的形式发送给server,注明修改人、修改内容、修改方式等

        

   server收到了某个client的这些json通知,说明其修改了文档内容,所以server肯定是要第一时间把这些json转发到其他正在使用该文档client的,方便别人更新内容。既然某个client更新了文档,后台的server肯定也是要更新文档的,这个该怎么操作了?这里分析一下文档更新(包括增删改)的特点:

  • 部分文档更新可能会很频繁,所以上述json类的request会很多
  • 还有可能是很多人同时更新,不止1个人在更新,不同client更新的内容很有可能是冲突的:比如clientA在第一行第一列增加了A字母,结果clientB在同样的位置增加了B字母.....

  综上所述,如果server后台一旦收到更新的请求立即更改HDFS,这效率得多低啊!从磁盘读写数据本来就慢,还遇到这么频繁的读写,服务器不得又感觉亚历山大?所以修改的细节就需要斟酌了,如下:

  

    client做的所有修改,都会先被保存在redis服务器!然后有个scheduled task,定时从redis取出所有的updata来合并处理,然后把所有updata合并后的结果存在file server,最后写入HDFS,这就极大减少了file server读写disk的压力!用异步的方式减轻下游的流量压力【只不过没用kafka】;那么redis又怎么存这个更改了?

        

   直接这样简单粗暴:根据file key+row key做检索,value保存更改后的content,preID和nextID;细心的朋友可能已经发现了:卧槽,为啥没timestamp了?连时间都没有,怎么确定更改的先后顺序了?还记得uuid字段么?都是各个client自己生成的,用这个就能确定修改的先后顺序了!比如新增一行:

  

   4、(1)在编辑文档是,还想要知道另一个也在编辑文档的人,这个功能简单,直接把同一个文档在线用户存放在redis,用户放在list或set就行了!

   

   (2)显实正在编辑某行的人: 同样存在redis中,由server通过websocket同步给其他client

        

   5、上述的方式都要基于锁。任何数据,一旦上锁,效率就会成倍降低,有没有不上锁、又能同时让多人协同编辑的方式了?google docs用了一个叫做OT(Operational Transform)的算法,图示如下:

  

   左边的client要在第三个位置插入X,但是右边的client要在第一个位置插入O,server收到了这两条消息,于是乎就整个了这两个请求,得到最后的结果字符串就是OABCXD,然后根据最终的字符串,分别告诉两个client该怎么改自己现有的字符串!咋样,这个想法是不是很完美了?利用同样的算法来处理一个请求,假设原始的字符串是:“我要买个瓜”;clientA像买西瓜,所以其指令是insert("西",4)。但好巧不巧,clientB此时也想吃瓜,不过clientB想吃的是哈密瓜,所以clientB的指令是insert("哈密",4);此时的server同时收到了这两个inset指令,并且都是在同一个位置,怎么办?只有两种办法:

  • 根据时间戳取数据。至于用先到的请求,还是后到的请求,就要业务来顶了
  • 两个都保留,最终的结果是“我要买个哈密西瓜”.......

 

总结:系统设计思路要点,有两个最核心的要素:

  •  数据存哪?以sql还是nosql的形式存? 数据模型怎么设计?
  • 数据怎么触达用户?直接从数据库读取?还是用redis做cache?还是通过MessageQueue把同步改成异步来削峰?

具体到实现层面,推荐spring cloud的方式,如下:

  

  •  client直接通过rest接口访问application服务器
    • rest接口走http/https协议,head还可以包含各种自定义的字段用于传递大量信息(诸如accept、referer、其他自定义字段),功能比较多
  •  application通过dubbo访问具体提供各种crud逻辑处理服务的instance
    • 相比于rest的http,dubbo走的是rpc协议,序列化是针对二进制协议(0/1)来做序列化和反序列化,性能高比http的json形式高多了!
    • rpc走long connection长链接,比http 1.0的short connection效率高多了!
  • application和各种service都要registed in nacos
  • application可以用ribbon做client侧的loadBalance(nginx是server侧的loadBalance),几行代码就搞定了:
       //指定服务名,根据服务名找对应的实例ip;这个服务已经在nacos注册了
       String serviceId = "nacos-restful-provider";
      //通过负载均衡发现地址,流程是从服务发现中心拿nacos-restful-provider服务的列表,通过负载均衡算法获取一个地址
       @Autowired
       LoadBalancerClient loadBalancerClient;
        //远程调用
        RestTemplate restTemplate = new RestTemplate();
        //发现一个地址:根据服务名称找服务实例
        ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId);
        //获取一个http://开头的地址,包括ip和端口
        URI uri = serviceInstance.getUri();
        String result = restTemplate.getForObject(uri + "/service", String.class);  
  •  service做crud可以通过mybatis读写数据库,配置简单快捷、方便

  

参考:

1、https://svn.apache.org/repos/asf/incubator/wave/whitepapers/operationaltransform/operational-transform.html  G-Suite协同引擎的协议白皮书

2、https://dl.acm.org/doi/10.1145/274444.274447 GOT算法及一维数据操作变换算法论文
3、https://www.tiny.cloud/blog/real-time-collaboration-ot-vs-crdt/  To OT or CRDT, that is the question
4、https://github.com/share/sharedb real-time database backend based on OT of JSON documents
5、https://www.bilibili.com/video/BV1Za411Y7rz?p=64&vd_source=241a5bcb1c13e6828e519dd1f78f35b2   文档协同编辑
6、https://www.zhihu.com/question/20215561    https://www.ruanyifeng.com/blog/2017/05/websocket.html    websocket协议介绍

posted @ 2022-08-28 12:47  第七子007  阅读(644)  评论(0编辑  收藏  举报