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 userIdList) {
// 将数据1000个一页进行拆分
int pageSize = 1000;
List> userIdLists = Lists.partition(userIdList, pageSize);
// 遍历所有分页,每页数据存到1个Key中,通过后缀index进行区分
Long index = 0L;
for (List userIdListPart : userIdLists) {
String pageDataKey = "user:ids:data:" + (index++);
// 使用管道pipeline,减少获取连接次数
redisTemplate.executePipelined((RedisCallback) 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 popBigKey() {
String indexKey = "user:ids:index";
String indexStr = redisTemplate.opsForValue().get(indexKey);
if (StringUtils.isEmpty(indexStr)) {
return null;
}
List 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
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);
}