记一次线上并发bug

123 阅读2分钟

记一次线上并发bug

原文请移步公号

前言

最近在写一个视频会议系统,我这边主要的一部分代码是RTC回调,还有一部分是客户端回调。在处理回调的时候都是按照同步的方式去进行的,本质上对于我来说不能有并发问题的,但是它确实发生了,那么我们一起来看下。

展示问题代码

下面这段代码主要含义就是当客户端开启共享屏幕或者视频的时候需要客户端回调给服务端RTC状态,服务端需要根据RTC状态计算最新视频画面排序规则。

   // 1+6 1:主窗口一个 6 右侧小窗口数量
    public List<Integer> getTop7(String roomId, Integer mainN, Integer rightN) {
        // 【redis:zset】获取top7排序
        List<Integer> topList = getTopN(roomId, mainN, rightN);
        log.info("Top7返回的排序列表:"+topList);
        // 【redis:list】主要针对右侧小窗口缓存 减少相对调换次数
        List<Integer> lastList = getLastScreenSortList(roomId, "7");
        log.info("Top7最后一次排序:"+lastList);
        List<Integer> tmpLastList = new ArrayList<>(lastList);
        List<Integer> tmpTopList = new ArrayList<>(topList);
        
        // 纯算法计算稳定性排序 不涉及中间件存储和读取
        List<Integer> newList = updateLastScreenSortList(tmpLastList, tmpTopList);
        log.info("Top7当前最新排序:"+newList);
  
        // 【redis:list】删除最后一次排序key
        deleteLastScreenSortList(roomId);
        if (newList.size() > 0) {
            // 【redis:list】存储最新排序为最后一次排序key
            saveLastScreenSortList(roomId, newList, "7");
        }
        log.info("Top7当前最新排序:"+newList);
        return newList;
    }

大家可以看下,并发问题主要问题会出现在代码块哪里???

问题就是出现在如下代码块中:

    // ---并发代码开始---
    deleteLastScreenSortList(roomId);
    if (newList.size() > 0) {
        // 【redis】存储最新排序为最后一次排序key
        saveLastScreenSortList(roomId, newList, "7");
    }
    // ---并发代码结束---
        
    // 保存最后一次排序列表
    public void saveLastScreenSortList(String roomId, List<Integer> userIds, String topN) {
        redisTemplate.opsForList().rightPushAll(LAST_SCREEN_SORT_KEY.setParamValue(topN, roomId), userIds);
    }

为什么会出现并发问题呢?就是当出现多个RTC回调同时走到并发代码段开始的时候,就会出现一个现象,这个现象就是:删删增增。也就说这两个请求会同时执行删除操作,然后同时走到存储部分。而存储部分是调用redis的list结构的rightPushAll方法,因此会push两份数据,那如果有大量回调同时进行呢?那就会导致数据在短时间内急剧膨胀,导致redis内存告警,当前服务和其他服务不可用。

这是非常严重的一个并发操作不当导致的线上问题,因此需要大家特别留心观察自己的代码,尤其是共享资源部分。

解决方案

  1. 可以用并发锁解决
  2. 可以用lua 脚本
  3. 可以用redis事务

这几种方案都可以,就看自己的场景适合哪一种,我们选择第二种方式,保证删除和新增都是原子操作。当然第三种也可以,只要保证删和增命令没问题就可以。至于第一种锁的粒度需要掌控好,同时也要保证锁的正确释放。