同一时间只允许一个客户端操作前端界面

业务场景

# 设计

后端使用Guava cache存储一个key,来作为锁,每个用户拥有一个锁,如果10分钟没有对cache进行写操作,则过期失效
用户操作前端界面时,会请求后端获取锁(如果cache的size < 1,说明此时没人正在操作,系统给用户分配锁,否则返回null),如果用户拿到锁,则可以操作前端界面,否则无法操作
用户在前端操作完后需要后需要请求后端,释放自己分配到的锁
防止用户长时间占用界面导致其它用户无法使用,用户在前端操作时,启用定时器2分钟后禁用操作,并请求后端释放自己分配到的锁
1
2
3
4

# 后端

@Service
@Slf4j
public class CacheService {
  /**
   * 缓存,限制人数为1,设置10分钟没有写操作则过期
   */
  private final Cache<String, Boolean> cache =
      CacheBuilder.newBuilder().maximumSize(1).expireAfterWrite(10, TimeUnit.MINUTES).build();

  /**
   * 释放锁
   * @param key
   */
  public void releaseLock(String key) {
    if (key != null) {
      log.debug("==========释放前 cache 大小 ={} ==========", cache.size());
      log.debug("========== cache key ={} ==========", cache.asMap().keySet());
      log.debug("========== key ={} ==========", key);
      cache.invalidate(key);
      log.debug("==========释放后 cache 大小 ={} ==========", cache.size());
    }
  }

  /**
   * 获取锁
   * @return
   */
  public String getLock() {
    log.debug("========== cache 大小 ={} ==========", cache.size());
    if (cache.size() < 1) {
      // 没被占用,生成id,锁住,并返回

      String key = UUIDGenerateUtil.getNextId();
      cache.put(key, true);
      log.debug("========== 生成 key ={} ==========", key);
      log.debug("========== 返回 key ={} ==========", key);
      return key;
    } else {
      log.debug("========== cache key ={} ==========", cache.asMap().keySet());
      log.debug("========== 返回 key ={} ==========", "null");
      return null;
    }
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Service
public class XService {
  @Autowired
  private CacheService cacheService;

  /**
   *如果某些业务操作会导致前端的操作结束,需要将对应锁释放
   */
  @Transactional
  public void operate(String key) {
    ...
    cacheService.releaseLock(key);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/X")
public class XController {
  @Autowired
  private CacheService cacheService;

  @Autowired
  private XService xService;

  @GetMapping("/edit/flag")
  @ApiOperation(value="查询是否可以操作")
  public ResponseDTO<String> getEditFlag(){
    return ResponseDTO.ok(cacheService.getLock());
  }

  @PostMapping("/edit/flag")
  @ApiOperation(value="操作完成后处理")
  public ResponseDTO<Integer> removeEditFlag(@ApiParam(value="操作时分配的key") String key){
    cacheService.releaseLock(key);
    return ResponseDTO.ok();
  }

  @PutMapping
  @ApiOperation(value="业务操作")
  public ResponseDTO<Integer> operate(String key){
    xService.operate(key);
    return ResponseDTO.ok();
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 前端

/**
 * ts
 */
export default class XComponent extends Vue {
  key = null as any;
  delay = 120000;
  timer = null as any;

  initEvent(f: any) {
    const w = window as any;
    w.onload = f;
    w.onscroll = f;
    w.onresize = f;
    w.onclick = f;
    w.onmouseup = f;
    w.onmousemove = f;
    w.onmousedown = f;
    w.onkeydown = f;
    w.onkeypress = f;
    w.onkeyup = f;
  }

  isActive(e: any) {
    //console.log('活跃');
    if (this.timer) {
      clearTimeout(this.timer);
    }
    if (this.key) {
      this.timer = setTimeout((t: any) => {
        //过期需要把key置空并向后台发请求释放锁
        //console.log('超过2分钟没操作');
        //console.log('释放key %o', this.key);
        this.closeForm();
      }, this.delay);
    }
  }

  closeForm() {
    //console.log('关闭操作窗口释放key %o', this.key);
    clearTimeout(this.timer);
    this.initEvent(null);
    this.$http.post(`.../X/edit/flag?key=${this.key ? this.key : ''}`).then(res => {
      if (res.data.code === '10000000') {
        this.key = null;
        //console.log('释放完key %o', this.key);
      }
    });
  }

  // 生命构造时
  created() {
    this.initEvent(this.isActive);
    //console.log('开始操作');
    this.$http.get(`.../X/edit/flag`).then(res => {
      if (res.data.code === '10000000') {
        this.key = res.data.data;
        //console.log('获取key %o', this.key);
      }
    });
  }

  // 生命销毁时
  destroyed() {
    this.closeForm();
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * html
 */
<div>
  ...
  <q-btn label="提交"
         type="submit"
         :disable="key == null"
         color="primary" />
</div>
1
2
3
4
5
6
7
8
9
10