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);
}
复制代码
评论