bigkey和hotkey是Redis生产中两个比较常见的问题,本文从它们的概念、危害、发现、解决的角度,来分析一下这两个问题。
通俗易懂的讲,Big Key就是某个key对应的value很大,占用的redis空间很大,本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。
redis中这些Big Key对应的value值很大,在序列化/反序列化过程中花费的时间很大,因此当我们操作Big Key时,通常比较耗时,这就可能导致redis发生阻塞,从而降低redis性能。
用几个实际的例子对大Key的特征进行描述:
一般业界(参考阿里、快手Redis开发规范)对于key的规范如下:
string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
主要思路为扫描Redis的所有Key,判断Key的长度即可。
Redis 4.0以后的客户端提供了bigkeys命令,可以找出每种数据类型占用内存最多的Key。
阿里云redis大key搜索工具
主要有两个办法1.遍历删除 2.Redis 异步删除命令
public void delBigHash(String host, int port, String password, String bigHashKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Entry<String, String>> entryList = scanResult.getResult();
if (entryList != null && !entryList.isEmpty()) {
for (Entry<String, String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigHashKey);
}
public void delBigList(String host, int port, String password, String bigListKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
long llen = jedis.llen(bigListKey);
int counter = 0;
int left = 100;
while (counter < llen) {
//每次从左侧截掉100个
jedis.ltrim(bigListKey, left, llen);
counter += left;
}
//最终删除key
jedis.del(bigListKey);
}
public void delBigSet(String host, int port, String password, String bigSetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if (memberList != null && !memberList.isEmpty()) {
for (String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigSetKey);
}
public void delBigZset(String host, int port, String password, String bigZsetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if (tupleList != null && !tupleList.isEmpty()) {
for (Tuple tuple : tupleList) {
jedis.zrem(bigZsetKey, tuple.getElement());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigZsetKey);
}
Redis4.0 已经支持key的异步删除,使用unlink命令即可,Redis lazyfree
主要通过大Key拆分的方法来解决大Key问题,我们需要将一个大key的数据拆分到多个小key上,然后通过客户端分片的方式来访问。
比如:
由于某个 Key 的数据一定是存储到后端某台服务器的 Redis 单个实例上,如果对这个 Key 突然出现大量的请求操作,这样就会造成流量过于集中,达到 Redis 单个实例处理上限,可能会导致 Redis 实例 CPU 使用率 100%,或者是网卡流量达到上限等,对系统的稳定性和可用性造成影响,或者更为严重出现服务器宕机,无法对外提供服务。
对于Redis单机来说,业界一般认为理论上极限OPS在10W左右,实际情况还跟具体机器配置相关。
流量过于集中,导致单个Redis节点负载(单节点一般10W)过大,导致Redis服务崩溃,大量 Redis 请求失败,查询操作可能打到数据库,数据库崩溃,导致整个服务不可用。
由此可见,hotkey对于服务的可用性会产生较大的危害,所以我们应该及时的发现hotkey并解决它。
通过上面的分析,出现热 Key
的危害还是很大的,我们不可能等到热 Key
出现已经拖垮了服务再去处理,那个时候业务一定已经收到影响,损失也是不言而喻的;那么能够在热 Key
出现前通过一些手段提前监控到热 Key
的出现,对于保证业务系统的稳定性是非常重要的,那么我们都有哪些手段提前观测到热 Key
的出现呢?
根据业务系统上线的一些活动和功能,我们是可以在某些场景下提前预估热 Key
的出现的,比如业务需要进行一场商品秒杀活动,秒杀商品信息和数量一般都会缓存到 Redis
中,这种场景极有可能出现热 Key
问题的。
Key
,提早发现提早处理;Key
出现,比如某些热点新闻事件,无法提前预测。一般我们在连接 Redis
服务器时都要使用专门的 SDK(比如:Java
的客户端工具 Jedis
、Redisson
),我们可以对客户端工具进行封装,在发送请求前进行收集采集,同时定时把收集到的数据上报到统一的服务进行聚合计算。
SDK
工具进行二次开发;SDK
都需要进行开发,后期开发维护成本较高。如果所有的 Redis
请求都经过 Proxy
(代理)的话,可以考虑改动 Proxy
代码进行收集,思路与客户端基本类似。
SDK
的语言异构和版本升级问题;Redis
集群架构中都有 Proxy
代理(使用这种方式必须要部署 Proxy
)。hotkeys 参数
Redis
在 4.0.3
版本中添加了 hotkeys 查找特性,可以直接利用 redis-cli --hotkeys
获取当前 keyspace
的热点 key
,实现上是通过 scan + object freq
完成的。
keyspace
,实时性上比较差;key
的数量正相关,如果 key
的数量比较多,耗时可能会非常长。monitor 命令
monitor
命令可以实时抓取出 Redis
服务器接收到的命令,通过 redis-cli monitor
抓取数据,同时结合一些现成的分析工具,比如 redis-faina,统计出热 Key。
Redis
的性能。其实各大云厂商都有提供发现hotkey,bigkey的能力,包括各大厂的基架也都有对应的Redis监控工具,可以对hotkey和bigkey进行发现。
当出现热 Key
以后,把热 Key
加载到系统的 JVM
中。后续针对这些热 Key
的请求,会直接从 JVM
中获取,而不会走到 Redis
层。这些本地缓存的工具很多,比如 Ehcache
,或者 Google Guava
中 Cache
工具,或者直接使用 HashMap
作为本地缓存工具都是可以的。
使用本地缓存需要注意两个问题:
Key
进行本地缓存,需要防止本地缓存过大,影响JVM Heap空间;Redis
集群读写数据一致性问题。通过前面的分析,我们可以了解到,之所以出现热 Key
,是因为有大量的对同一个 Key
的请求落到同一个 Redis
实例上,如果我们可以有办法将这些请求负载到不同的实例上,防止出现流量倾斜的情况,那么热 Key
问题也就不存在了。
那么如何将对某个热 Key
的请求打散到不同实例上呢?我们就可以通过热 Key
备份的方式,基本的思路就是,我们可以给热 Key
加上前缀或者后缀,把一个热 Key
的数量变成 Redis
实例个数 N
的倍数 M
,从而由访问一个 Redis
Key
变成访问 M
个 Redis
Key
。 M
个 Redis
Key
经过分片分布到不同的实例上,将访问量均摊到所有实例。
// N 为 Redis 实例个数,M 为 N 的 2倍
func getData() {
const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新 Key
bakHotKey = hotKey + "_" + random
data = redis.GET(bakHotKey)
if data == NULL {
data = redis.GET(hotKey)
if data == NULL {
//这里可以注意一下缓存击穿和缓存雪崩的问题
data = GetFromDB()
redis.SET(hotKey, data, expireTime)
redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
} else {
redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
}
}
return data
}
问题:
对于hotkeys的解决办法,没有一个办法是所有场景的银弹,我们需要针对业务场景来选择具体的方案,但是我们可以看到的是,无论是哪种方案,都会存在一定的一致性问题,不过既然都出现hotkey了,那么肯定是并发很高的情况,对于这种情况,一般我们保证最终一致性即可,无需追求数据强一致性。
而这也给我们揭示了一个道理 —— 一致性和可用性不可兼得啊,这很CAP。