一文教你彻底打败Redis Bigkey和Hotkey问题
创始人
2025-05-30 22:50:30
0

前言

bigkey和hotkey是Redis生产中两个比较常见的问题,本文从它们的概念、危害、发现、解决的角度,来分析一下这两个问题。

bigkey

概念

通俗易懂的讲,Big Key就是某个key对应的value很大,占用的redis空间很大,本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。

redis中这些Big Key对应的value值很大,在序列化/反序列化过程中花费的时间很大,因此当我们操作Big Key时,通常比较耗时,这就可能导致redis发生阻塞,从而降低redis性能。

用几个实际的例子对大Key的特征进行描述:

  • 一个String类型的Key,它的值为5MB(数据过大)
  • 一个List类型的Key,它的列表数量为20000个(列表数量过多)
  • 一个ZSet类型的Key,它的成员数量为10000个(成员数量过多)
  • 一个Hash格式的Key,它的成员数量虽然只有1000个但这些成员的value总大小为10MB(成员体积过大)

一般业界(参考阿里、快手Redis开发规范)对于key的规范如下:

string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。

危害

  • 慢查询:由于bigkey包含的数据量很大,导致一次请求的执行时间可能会很长,导致慢查询问题
  • 集群内存分布不均衡:集群模式下,bigkey较多的节点内存占用偏高,影响集群稳定性
  • 过期阻塞:当bigkey过期(删除)时,由于Redis单线程,会导致Redis阻塞从而影响客户端命令执行
  • 网卡超负荷:试想一个string类型的key数据量为10mb,这时10000个用户请求该key,那么会需要约100G的网卡带宽,影响服务器的正常运转

发现

主要思路为扫描Redis的所有Key,判断Key的长度即可。

  1. Redis 4.0以后的客户端提供了bigkeys命令,可以找出每种数据类型占用内存最多的Key。

  2. 阿里云redis大key搜索工具

删除

主要有两个办法1.遍历删除 2.Redis 异步删除命令

循环遍历删除

  1. Hash删除: hscan + hdel
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> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);List> entryList = scanResult.getResult();if (entryList != null && !entryList.isEmpty()) {for (Entry entry : entryList) {jedis.hdel(bigHashKey, entry.getKey());}}cursor = scanResult.getStringCursor();} while (!"0".equals(cursor));//删除bigkeyjedis.del(bigHashKey);
}
  1. List删除: ltrim
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;}//最终删除keyjedis.del(bigListKey);
}
  1. Set删除: sscan + srem
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 scanResult = jedis.sscan(bigSetKey, cursor, scanParams);List memberList = scanResult.getResult();if (memberList != null && !memberList.isEmpty()) {for (String member : memberList) {jedis.srem(bigSetKey, member);}}cursor = scanResult.getStringCursor();} while (!"0".equals(cursor));//删除bigkeyjedis.del(bigSetKey);
}
  1. SortedSet删除: zscan + zrem
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 scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);List tupleList = scanResult.getResult();if (tupleList != null && !tupleList.isEmpty()) {for (Tuple tuple : tupleList) {jedis.zrem(bigZsetKey, tuple.getElement());}}cursor = scanResult.getStringCursor();} while (!"0".equals(cursor));//删除bigkeyjedis.del(bigZsetKey);
}

异步删除

Redis4.0 已经支持key的异步删除,使用unlink命令即可,Redis lazyfree

解决

主要通过大Key拆分的方法来解决大Key问题,我们需要将一个大key的数据拆分到多个小key上,然后通过客户端分片的方式来访问。

比如:

  • string类型
    • 将string类型转为hash类型,list类型,再对hash,list进行拆分
    • 对于纯字符串类型,可以通过
      • 1.使用更节省数据的序列化协议
      • 2.使用数据压缩算法,存取过程进行相应的压缩解压缩操作
  • list类型:拆分为list:0,list:1,list:2,list:N等小key,通过对id % N 将数据Hash到不同的子key中
  • set类型:同list

hotkey

概念

由于某个 Key 的数据一定是存储到后端某台服务器的 Redis 单个实例上,如果对这个 Key 突然出现大量的请求操作,这样就会造成流量过于集中,达到 Redis 单个实例处理上限,可能会导致 Redis 实例 CPU 使用率 100%,或者是网卡流量达到上限等,对系统的稳定性和可用性造成影响,或者更为严重出现服务器宕机,无法对外提供服务。

对于Redis单机来说,业界一般认为理论上极限OPS在10W左右,实际情况还跟具体机器配置相关。

危害

流量过于集中,导致单个Redis节点负载(单节点一般10W)过大,导致Redis服务崩溃,大量 Redis 请求失败,查询操作可能打到数据库,数据库崩溃,导致整个服务不可用。

由此可见,hotkey对于服务的可用性会产生较大的危害,所以我们应该及时的发现hotkey并解决它。

发现

通过上面的分析,出现热 Key 的危害还是很大的,我们不可能等到热 Key 出现已经拖垮了服务再去处理,那个时候业务一定已经收到影响,损失也是不言而喻的;那么能够在热 Key 出现前通过一些手段提前监控到热 Key 的出现,对于保证业务系统的稳定性是非常重要的,那么我们都有哪些手段提前观测到热 Key 的出现呢?

1.预估业务流量

根据业务系统上线的一些活动和功能,我们是可以在某些场景下提前预估热 Key 的出现的,比如业务需要进行一场商品秒杀活动,秒杀商品信息和数量一般都会缓存到 Redis 中,这种场景极有可能出现热 Key 问题的。

  • 优点:简单,凭经验发现热 Key,提早发现提早处理;
  • 缺点:没有办法预测所有热 Key 出现,比如某些热点新闻事件,无法提前预测。

2.客户端监控

一般我们在连接 Redis 服务器时都要使用专门的 SDK(比如:Java 的客户端工具 JedisRedisson),我们可以对客户端工具进行封装,在发送请求前进行收集采集,同时定时把收集到的数据上报到统一的服务进行聚合计算。

  • 优点:方案简单
  • 缺点:
    • 对客户端代码有一定入侵,或者需要对 SDK 工具进行二次开发;
    • 没法适应多语言架构,每一种语言的 SDK 都需要进行开发,后期开发维护成本较高。

3.代理层监控

如果所有的 Redis 请求都经过 Proxy(代理)的话,可以考虑改动 Proxy 代码进行收集,思路与客户端基本类似。

img

  • 优点:对使用方完全透明,能够解决客户端 SDK 的语言异构和版本升级问题;
  • 缺点:
    • 开发成本会比客户端高些;
    • 并不是所有的 Redis 集群架构中都有 Proxy 代理(使用这种方式必须要部署 Proxy)。

4.Redis自带命令

hotkeys 参数

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

  • 优点:无需进行二次开发,能够直接利用现成的工具;
  • 缺点:
    • 由于需要扫描整个 keyspace,实时性上比较差;
    • 扫描时间与 key 的数量正相关,如果 key 的数量比较多,耗时可能会非常长。

monitor 命令

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

  • 优点:无需进行二次开发,能够直接利用现成的工具;
  • 缺点:该命令在高并发的条件下,有内存增暴增的隐患,还会降低 Redis 的性能。

5.依靠大厂的基建能力

其实各大云厂商都有提供发现hotkey,bigkey的能力,包括各大厂的基架也都有对应的Redis监控工具,可以对hotkey和bigkey进行发现。

解决

1.多级缓存

当出现热 Key 以后,把热 Key 加载到系统的 JVM 中。后续针对这些热 Key 的请求,会直接从 JVM 中获取,而不会走到 Redis 层。这些本地缓存的工具很多,比如 Ehcache,或者 Google GuavaCache 工具,或者直接使用 HashMap 作为本地缓存工具都是可以的。

使用本地缓存需要注意两个问题:

  • 如果对热 Key 进行本地缓存,需要防止本地缓存过大,影响JVM Heap空间;
  • 需要处理本地缓存和 Redis 集群读写数据一致性问题。

2.负载均衡

通过前面的分析,我们可以了解到,之所以出现热 Key,是因为有大量的对同一个 Key 的请求落到同一个 Redis 实例上,如果我们可以有办法将这些请求负载到不同的实例上,防止出现流量倾斜的情况,那么热 Key 问题也就不存在了。

那么如何将对某个热 Key 的请求打散到不同实例上呢?我们就可以通过热 Key 备份的方式,基本的思路就是,我们可以给热 Key 加上前缀或者后缀,把一个热 Key 的数量变成 Redis 实例个数 N 的倍数 M,从而由访问一个 Redis Key 变成访问 MRedis KeyMRedis Key 经过分片分布到不同的实例上,将访问量均摊到所有实例。

// N 为 Redis 实例个数,M 为 N 的 2倍
func getData() {const M = N * 2//生成随机数random = GenRandom(0, M)//构造备份新 KeybakHotKey = hotKey + "_" + randomdata = 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
}

问题:

  • 浪费Redis内存空间,可以通过配置中心设置一个开关,当开关打开时才会访问临时的hotkey节点
  • 数据一致性
    • 多个Redis节点之间数据的一致性无法保证,数据同步的时候会存在部分数据不一致性的情况
    • 如果存在数据更新的情况,需要同时更新所有Redis节点,此处也会有数据不一致的情况

总结

对于hotkeys的解决办法,没有一个办法是所有场景的银弹,我们需要针对业务场景来选择具体的方案,但是我们可以看到的是,无论是哪种方案,都会存在一定的一致性问题,不过既然都出现hotkey了,那么肯定是并发很高的情况,对于这种情况,一般我们保证最终一致性即可,无需追求数据强一致性。

而这也给我们揭示了一个道理 —— 一致性和可用性不可兼得啊,这很CAP。

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...