乐趣区

关于redis:Reids的BigKey和HotKey

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);
}
退出移动版