多端同时操作元素的同步锁方案设计

多端同时操作元素的同步锁方案设计

1. 背景

产品需要做一个多人同步实时操作元素的功能,但需要确保同一个元素每次只能由一个用户获得控制权,各端同步这个用户的元素状态。

2. 实现思路

考虑使用一个同步锁机制,元素通过一个 加锁状态(lock)以及 加锁时间戳(lockTime)实现。只有获得锁的用户才能拥有对元素状态的控制权,控制权的优先级由时间戳决定。

3. 代码分析

简单实现了一个代码demo。

3.1 元素架构设计

元素需要实现三个基础功能接口

  1. 设置本地操作监听函数
  2. 远端同步状态函数
  3. 接收来自远端状态的函数
class DemoElement {
  // 2099-12-31 23:59:59
  static readonly LOCK_RELEASE_TIMESTAMP = 4102415999000;

  public lock: boolean
  public lockTime: number

  public container: HTMLDivElement

  constructor() {
    this.lock = false;
    this.lockTime = DemoElement.LOCK_RELEASE_TIMESTAMP;
    this.container = document.createElement('div');
    this.bindEvent();
  }
  
  // 绑定本地操作监听
  bindEvent() {}

  // 向远端同步状态
  sendRealtimeData() {}

  // 接收来自远端的状态
  syncData({ lock, lockTime }) {}
}

3.2 bindEvent 函数实现

这里以 pointerdown – pointermove – pointerup 处理流程为例,展示本地操作的加锁、释放锁的流程

bindEvent() {
  this.container.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    e.stopPropagation();
    const time = new Date().valueOf();
    // 加锁时间小于当前时间,说明已被加锁,本地不可占用
    if (time > this.lockTime) {
      return;
    }
    // 加锁
    this.lock = true;
    this.lockTime = time;

    const onMove = (e) => {
      // 没有获得锁,不能占用
      if (!this.lock) {
        removeListener();
        return;
      }
    }

    const onMoveEnd = (e) => {
      // 没有获得锁,不能占用
      if (!this.lock) {
        removeListener();
        return;
      }
      // 释放锁
      this.lock = false;
      this.lockTime = DemoElement.LOCK_RELEASE_TIMESTAMP;
      removeListener();
    }

    const removeListener = () => {
      this.container.removeEventListener('pointermove', onMove, false);
      this.container.removeEventListener('pointerup', onMoveEnd, false);
    }

    this.container.addEventListener('pointermove', onMove, false);
    this.container.addEventListener('pointerup', onMoveEnd, false);
  });
}

3.3 sendRealtimeData 函数实现

实时同步元素状态时,除了元素的属性,还需要把锁的状态同步出去

sendRealtimeData() {
  return {
    lock: this.lock,
    lockTime: this.lockTime
  }
}

3.4 syncData 函数实现

接收来自远端的元素状态时,根据 lock 和 lockTime 判断远端的加锁状态以及本地能否获得锁权限

syncData({ lock, lockTime }) {
  if (lock) {
    // 传入lockTime小于等于本地lockTime,已经被远端加锁,则本地解除资源占用
    if (lockTime <= this.lockTime) {
      // 释放锁
      this.lock = false;
      this.lockTime = lockTime;
    } else {
      // 本地先加锁,不接受外部同步信息
    }
  } else {
    // 远端解除占用
    if (this.lock) {
      // 本地先加锁,不接受外部同步信息
    } else {
      // 释放锁
      this.lock = false;
      this.lockTime = lockTime;
    }
  }
}