将服务端端下发的所有 update 节点,在架构树中查询,若找到了,则将备份数据转为正式数据。若找不到,则为新增节点,atv,需要拉取具体信息并保存在架构树中。 当完整架构同步结束后,在 db 中找到并删除掉所有备份节点,清除掉缓存和同步状态。 若服务端下发了全量节点,客户端的处理时序图为: 服务端下发版本号回退标记 从时序图中可以看出,服务端下发的版本号回退标记是很重要的信号。 而版本号回退这个标记,仅仅在同步的首次会随着新的版本号而下发。在完整架构同步期间,客户端需要将该标记缓存,并且跟着版本号一起存在数据库中。在完整架构同步结束后,需要根据是否版本号回退来决定删除掉数据库中的待删除节点。 备份架构树方案 架构树备份最直接的方案是将 db 中数据 copy 一份,并存在新表里。如果在数据量很小的情况下,这样做是完全没有问题的,但是架构树的节点往往很多,采取这样简单粗暴的方案在移动端是完全不可取的,在几十万人的企业里,这样做会造成极大的性能问题。 经过考虑后,企业微信采取的方案是: 若同步架构时,后台下发了需要版本号回退的 flag,客户端将缓存和 db 中的所有节点标为待删除(时序图中 8,9 步)。 针对服务端下发的更新节点,在架构树中清除掉节点的待删除标记(时序图中 10,11 步)。 在完整架构同步结束后,在 db 中找到并删除掉所有标为待删除的节点(时序图中 13 步),并且清除掉所有缓存数据。 而且,在增量同步过程中,不应该影响正常的架构树展示。所以在架构同步过程中,若有上层来请求 db 中的数据,则需要过滤掉有待删除标记的节点。 缓存架构树 方案决定客户端避免不了全量节点对比,将重要的信息缓存到内存中会大大加快处理速度。内存中的架构树节点体定义为: 此处我们用 std::map 来缓存架构树,用 std::pair 作为 key。我们在比较节点的时候,会涉及到很多查询操作,使用 map 查询的时间复杂度仅为 O(logn)。 增量同步方案关键点 本节单独将优化同步方案中关键点拿出来写,这些关键点不仅仅适用于本文架构同步,也适用于大多数同步逻辑。 保证数据处理完成后,再储存版本号 在几乎所有的同步中,版本号都是重中之重,一旦版本号乱掉,后果非常严重。 在架构同步中,最最重要的一点是: 保证数据处理完成后,再储存版本号。 在组织架构同步的场景下,为什么不能先存版本号,再存数据呢? 这涉及到组织架构同步数据的一个重要特征:架构节点数据是可重复拉取并覆盖的。 考虑下实际操作中遇到的真实场景: 若客户端已经向服务端请求了新增节点信息,客户端此时刚刚插入了新增节点,还未储存版本号,客户端应用中止了。 此时客户端重新启动,又会用相同版本号拉下刚刚已经处理过的节点,而这些节点跟本地数据对比后,会发现节点的 seq 并未更新而不会再去拉节点信息,也不会造成节点重复。 若一旦先存版本号再存具体数据,一定会有概率丢失架构更新数据。 同步的原子性 正常情况下,一次同步的逻辑可以简化为: 在企业微信的组织架构同步中存在异步操作,若进行同步的过程不保证原子性,直播,极大可能出现下图所示的情况: 该图中,同步的途中插入了另外一次同步,很容易造成问题: 输出结果不稳定。若两次同步几乎同时开始,但因为存在网络波动等情况,返回结果可能不同,给调试造成极大的困扰。 中间状态错乱。若同步中处理服务端返回的结果会依赖于请求同步时的某个中间状态,而新的同步发起时又会重置这个状态,很可能会引起匪夷所思的异常。 时序错乱。整个同步流程应该是原子的,若中间插入了其他同步的流程会造成整个同步流程时序混乱,引发异常。 怎样保证同步的原子性呢? 我们可以在开始同步的时候记一个 flag 表示正在同步,在结束同步时,清除掉该 flag。若另外一次同步到来时,发现正在同步,则可以直接舍弃掉本次同步,或者等本次同步成功后再进行一次同步。 此外也可将同步串行化,保证同步的时序,多次同步的时序应该是 FIFO 的。 缓存数据一致性 移动端同步过程中的缓存多分为两种: 内存缓存。加入内存缓存的目的是减少文件 IO 操作,加快程序处理速度。 磁盘缓存。加入磁盘缓存是为了防止程序中止时丢失掉同步状态。 内存缓存多缓存同步时的数据以及同步的中间状态,磁盘缓存用于缓存同步的中间状态防止缓存状态丢失。 (责任编辑:本港台直播) |