Redis的BigKey和HotKey

3,794 阅读5分钟

1.什么是BigKey和HotKey

1.1.Big Key

Redis big key problem,实际上不是大Key问题,而是Key对应的value过大,因此严格来说是Big Value问题,Redis value is too large (key value is too large)。

到底多大的value会导致big key问题,并没有统一的标准。

例如,对于String类型的value,有时候超过5M属于big key,有时候稳妥起见,超过10K就可以算作Bigey。

Big Key会导致哪些问题呢?

1、由于value值很大,序列化和反序列化时间过长,网络时延也长,从而导致操作Big Key的时候耗时很长,降低了Redis的性能。

2、在集群模式下无法做到负载均衡,导致负载倾斜到某个实例上,单实例的QPS会比较高,内存占用比较多。

3、由于Redis是单线程,如果要对这个大Key进行删除操作,被操作的实例可能会被block住,从而导致无法响应请求。

Big Key是如何产生的呢?

一般是程序设计者对于数据的规模预料不当,或设计考虑遗漏导致的Big Key的产生。

在某些业务场景下,很容易产生Big Key,例如KOL或者流量明星的粉丝列表、投票的统计信息、大批量数据的缓存,等等。

1.2.Hot Key

Hot Key,也叫Hotspot Key,即热点Key。如果某个特定Key突然有大量请求,流量集中到某个实例,甚至导致这台Redis服务器因为达到物理网卡上线而宕机,这个时候其实就是遇到了热点Key 问题。

热点key会导致很多系统问题:

1、流量过度集中,无法发挥集群优势,如果达到该实例处理上限,导致节点宕机,进而冲击数据库,有导致缓存雪崩,让整个系统挂掉的风险。

2、由于Redis是单线程操作,热点Key会影响所在示例其他Key的操作。

2.如何发现BigKey和HotKey

2.1.发现BigKey

1、通过Redis命令查询BigKey。

以下命令可以扫描Redis的整个Key空间不同数据类型中最大的Key。-i 0.1 参数可以在扫描的时候每100次命令执行sleep 0.1 秒。

Redis自带的bigkeys的命令可以很方便的在线扫描大key,对服务的性能影响很小,单缺点是信息较少,只有每个类型最大的Key。

$ redis-cli -p 999 --bigkeys -i 0.1

2、通过开源工具查询BigKey。

使用开源工具,优点在于获取的key信息详细、可选参数多、支持定制化需求,后续处理方便,缺点是需要离线操作,获取结果时间较长。

比如,redis-rdb-tools 等等。

$ git clone https://github.com/sripathikrishnan/redis-rdb-tools 
$ cd redis-rdb-tools
$ sudo python setup.py install 
$ rdb -c memory dump-10030.rdb > memory.csv

2.2.发现HotKey

1、hotkeys 参数

Redis 在 4.0.3 版本中添加了 hotkeys (github.com/redis/redis…)查找特性,可以直接利用 redis-cli --hotkeys 获取当前 keyspace 的热点 key,实现上是通过 scan + object freq 完成的。

2、monitor 命令

monitor 命令可以实时抓取出 Redis 服务器接收到的命令,通过 redis-cli monitor 抓取数据,同时结合一些现成的分析工具,比如 redis-faina,统计出热 Key。

3.BigKey问题的解决方法

发现和解决BigKey问题,可以参考以下思路:

1、在设计程序之初,预估value的大小,在业务设计中就避免过大的value的出现。

2、通过监控的方式,尽早发现大Key。

3、如果实在无法避免大Key,那么可以将一个Key拆分为多个部分分别存储到不同的Key里。

下面以List类型的value为例,演示一下拆分解决大Key问题的方法。

有一个User Id列表,有1000万数据,如果全部存储到一个Key下面,会非常大,可以通过分页拆分的方式存取数据。

下面是存取数据的代码实现:

/**
 * 将用户数据写入Redis缓存
 *
 * @param userIdList
 */
public void pushBigKey(List<Long> userIdList) {
    // 将数据1000个一页进行拆分
    int pageSize = 1000;
    List<List<Long>> userIdLists = Lists.partition(userIdList, pageSize);

    // 遍历所有分页,每页数据存到1个Key中,通过后缀index进行区分
    Long index = 0L;
    for (List<Long> userIdListPart : userIdLists) {
        String pageDataKey = "user:ids:data:" + (index++);
        // 使用管道pipeline,减少获取连接次数
        redisTemplate.executePipelined((RedisCallback<Long>) connection -> {
            for (Long userId : userIdListPart) {
                connection.lPush(pageDataKey.getBytes(), userId.toString().getBytes());
            }
            return null;
        });
        redisTemplate.expire(pageDataKey, 1, TimeUnit.DAYS);
    }

    // 存完数据,将数据的页数存到一个单独的Key中
    String indexKey = "user:ids:index";
    redisTemplate.opsForValue().set(indexKey, index.toString());
    redisTemplate.expire(indexKey, 1, TimeUnit.DAYS);
}

/**
 * 从Redis缓存读取用户数据
 *
 * @return
 */
public List<Long> popBigKey() {
    String indexKey = "user:ids:index";
    String indexStr = redisTemplate.opsForValue().get(indexKey);
    if (StringUtils.isEmpty(indexStr)) {
        return null;
    }

    List<Long> userIdList = new ArrayList<>();

    Long index = Long.parseLong(indexStr);
    for (Long i = 1L; i <= index; i++) {
        String pageDataKey = "user:ids:data:" + i;
        Long currentPageSize = redisTemplate.opsForList().size(pageDataKey);
        List<Object> dataListFromRedisOnePage = redisTemplate.executePipelined((RedisCallback<Long>) connection -> {
            for (int j = 0; j < currentPageSize; j++) {
                connection.rPop(pageDataKey.getBytes());
            }
            return null;
        });
        for (Object data : dataListFromRedisOnePage) {
            userIdList.add(Long.parseLong(data.toString()));
        }
    }

    return userIdList;
}

4.HotKey问题的解决方法

如果出现了HotKey,可以考虑以下解决方案:

1、使用本地缓存。比如在服务器缓存需要请求的热点数据,这样通过服务器集群的负载均衡,可以避免将大流量请求到Redis。

但本地缓存会引入数据一致性问题,同时浪费服务器内存。

2、HotKey将复制多份,随机打散,使用代理请求。

/**
 * 将HotKey数据复制20份存储
 *
 * @param key
 * @param value
 */
public void setHotKey(String key, String value) {
    int copyNum = 20;
    for (int i = 1; i <= copyNum; i++) {
        String indexKey = key + ":" + i;
        redisTemplate.opsForValue().set(indexKey, value);
        redisTemplate.expire(indexKey, 1, TimeUnit.DAYS);
    }
}

/**
 * 随机从一个拷贝中获取一个数据
 *
 * @param key
 * @return
 */
public String getHotKey(String key) {
    int startInclusive = 1;
    int endExclusive = 21;
    String randomKey = key + ":" + RandomUtils.nextInt(startInclusive, endExclusive);
    return redisTemplate.opsForValue().get(randomKey);
}

5.参考资料