事情的起因是这样的:近日项目用户量暴增加上最近一次项目优化把很多东西都放进了redis中,导致redis的开销和key数量急剧上升,由原来不到几千key增长到了三到五万,然后悲剧就此发生

问题排查

代码本地debug启动,前端代码本地启动联调,进行登录操作并对后端代码进行逐行排查,发现走到这一行后直接卡死

// 卡顿代码
cacheUtil.clearCache(xxx);

代码逻辑

然后看一下内部逻辑

// 内部逻辑
public void clearCache(String keyPattern) {
        List<String> scan = redisUtils.scan(keyPattern + "*");
        redisUtils.removeKeys(scan);
    }

// scan方法
public List<String> scan(String pattern) {
        ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection rc = Objects.requireNonNull(factory).getConnection();
        Cursor<byte[]> cursor = rc.scan(options);
        List<String> result = new ArrayList<>();
        while (cursor.hasNext()) {
            result.add(new String(cursor.next()));
        }
        try {
            RedisConnectionUtils.releaseConnection(rc, factory);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            cursor.close();
        }
        return result;
    }

原理及相关资料分析

整个工具类是项目初期创建的,一直使用到用户量过万没出现问题,所以最初没考虑到是这里的问题,在经过对scan的原理探究后发现,scan是一个基于游标的迭代器,它的时间复杂度为O(n)相比于Keys命令来说有一个优点是不会阻塞redis,但在某条语句中如果不是异步调用的话会阻塞我们的程序执行,相当于分页查找redis中的所有key每拿出一页才会再次去拿下一页,spring内置的jedis默认页面大小是10,假设有一万key,最坏的情况就要进行1000次通讯,当然受网速和机器与机器是否在同一地域等等的影响通讯时长不固定,但key的数量上来之后无疑这里会增加很多不必要的开销;查阅了相关的资料后得到一个count值与搜索效率的曲线,大概是在count值为10000时会达到一个平衡点,再大的话对效率影响微乎其微。

解决方案

scan方法中对于ScanOptions对象的build添加上count属性的设置,由于目前项目体量一般,key数量在三到五万,所以count先给定了1000,经过测试方法调用,scan方法从70多秒优化到了2秒+,count设置为10000的话能再缩短一般,但由于考虑到可能会影响到并发性,所以暂时未设置为1w

ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();