Redis大key与热key的排查与解决方法(redis-cli --bigkeys、--memkeys与--hotkeys定位、UNLINK删除与拆分打散)

Redis实例的内存莫名其妙涨上去、个别接口延迟偶尔飙到几百毫秒、主从同步延迟抖一下又自己恢复,这几种现象背后常常是同一件事:某些key特别大,或者某些key特别热。大key让单次操作耗时变长、删除时把主线程卡住、持久化和迁移跟着受影响;热key则把流量全压到集群里的某一个分片上,其它节点闲着,那一个节点的CPU下不来。下面按「先找到、再确认、再处理」的顺序,记录实际能用的一套命令。

1、先用--bigkeys扫一遍,找元素数量最多的key

redis-cli自带的扫描器,按string、list、set、hash、zset、stream每种类型各挑一个元素数量最多的key出来,底层走SCAN,不会像KEYS *那样把主线程一次性堵死。-i参数表示每100次SCAN之间休眠多少秒,线上跑的时候带上,压低对业务的影响。实测一个400万key的实例,不加-i跑完大概两分多钟,加上-i 0.1会慢一些但QPS基本没有抖动。

redis-cli -h 127.0.0.1 -p 6379 -a 'password' --bigkeys -i 0.1

输出末尾的summary部分会给出每种类型的最大key名字、元素个数和平均大小。这个选项自Redis 3.0.0起就有,老版本也能用。

它有个明显的盲区:只统计元素个数,不管每个元素多大。一个list只有800个元素、每个元素是几百KB的JSON,--bigkeys会把这个key当作小key放过去。这种场景要换--memkeys。

2、用--memkeys按真实内存占用排

--memkeys内部调MEMORY USAGE逐key算占用,能抓出「元素少但个头大」的那类key,自Redis 6.0.0起提供。想看前N个而不是每种类型一个,用--keystats配合--top:

redis-cli --memkeys -i 0.1
redis-cli --keystats --top 20 -i 0.1
redis-cli --memkeys --memkeys-samples 100 --pattern 'order:*'

这三个命令都不会修改数据,可以放心在从库上跑。有条件的话尽量连从节点扫,主节点的CPU留给业务。集群模式下要逐个节点跑,--bigkeys和--memkeys不会自动遍历所有分片。

3、热key靠--hotkeys,但要先改淘汰策略

--hotkeys依赖LFU计数,从Redis 4.0.3起提供,前提是maxmemory-policy是allkeys-lfu或volatile-lfu。配置不对的话直接报错退出:

redis-cli config get maxmemory-policy
redis-cli config set maxmemory-policy allkeys-lfu
redis-cli --hotkeys
redis-cli object freq user:125

OBJECT FREQ返回0到255之间的一个值,用8位空间按概率增长记录访问频率,255大概对应上百万次访问,是个对数尺度的估计值不是精确计数。改淘汰策略这件事本身有副作用,allkeys-lfu会让实例开始按频率淘汰key,确认完记得把maxmemory-policy改回原来的值。之前有台机器改完忘了改回去,第二天业务key被淘汰了一批,排查方向全被带偏。

见过有人把--hotkeys留在定时脚本里跑,几百万key扫一次要好几分钟,热度还没统计完业务已经变了。真要长期盯着热key,用客户端埋点或者MONITOR采样更实在。

4、确认单个key的体量

扫出来名字之后,还得确认它到底多大、用的什么编码:

redis-cli memory usage cache:order:20260815
redis-cli object encoding cache:order:20260815
redis-cli strlen cache:order:20260815
redis-cli hlen user:profile:all
redis-cli zcard rank:daily

OBJECT ENCODING返回listpack、quicklist、hashtable这类内部编码。同一个hash,元素少的时候是listpack,元素涨到阈值之上会转成hashtable,内存占用可能翻好几倍——这个转换是不可逆的,把元素删回去也不会变回listpack。Redis 7.0、7.2、7.4,这几个版本在编码阈值上都有调整,hash-max-listpack-entries、hash-max-listpack-value这两个参数可以按业务改。

5、佐证:慢查询和延迟监控

大key往往在SLOWLOG里留下痕迹,一条GET或HGETALL耗时几毫秒以上就值得看:

redis-cli slowlog get 20
redis-cli slowlog len
redis-cli info commandstats
redis-cli latency doctor
redis-cli --latency-history -i 5

commandstats里能看到每个命令的调用次数和总耗时,usec_per_call偏高的那个命令就是嫌疑对象,再结合key名去反查代码。

6、大key的处理

删除这一步最容易出事。DEL一个百万元素的zset会把主线程堵住几百毫秒到几秒,期间所有请求排队。换成UNLINK,回收动作交给后台线程,自Redis 4.0.0起可用:

redis-cli unlink rank:daily
redis-cli config set lazyfree-lazy-user-del yes
redis-cli config set lazyfree-lazy-expire yes
redis-cli config set lazyfree-lazy-eviction yes

开了lazyfree-lazy-user-del之后,普通的DEL也会走异步路径,对老代码友好。遇上不能一次性删掉的,用分步的方式:list用LTRIM逐段截,zset用ZREMRANGEBYRANK一批几千个地删,hash用HSCAN配HDEL,每批之间sleep几十毫秒。

治本还是拆。一个大hash按字段哈希拆成几十个小key,一个按天增长的list改成按天分片的多个key,读的时候用MGET或pipeline拼回来。字符串类型的超大value可以拆成多段,或者干脆压一下——JSON压成MessagePack或gzip能省掉六七成空间,代价是读写多一次CPU开销。

7、热key的处理

应用层本地缓存是见效最快的:把那个key在JVM或Go进程里缓存几秒到几十秒,绝大多数读请求根本到不了Redis。担心本地缓存不一致,就把过期时间设短一点、更新时主动失效。

另一个办法是打散。给热key加随机后缀做成N个副本,写入时全写、读取时随机挑一个,压力就平摊到N个槽位上。代价是写入放大N倍,更新的时候要保证所有副本都被写到,业务上要能接受短暂不一致才好这么干。

读写分离也能缓解。把读流量切到从节点,主节点只扛写。前提是业务能容忍主从之间那点延迟,而且要注意从节点本身也可能被同一个热key打满。

8、离线分析RDB

线上扫描总有性能顾虑,定期把RDB拉下来离线分析是更稳的做法。老牌工具redis-rdb-tools已经很久没更新,不支持Redis 7的RDB格式;现在更活跃的替代品是HDT3213/rdb,Go写的,解析一个3GB的RDB大约50秒,同样的文件redis-rdb-tools要跑25分钟以上。redis-cli --rdb能把RDB拉到本地,但云Redis普遍禁掉了SYNC,只能从控制台下载。

不管用哪种方式,大key的监控都应该做成常态告警,而不是等内存告警了才去查。把--memkeys的结果每天落一份,对比一下就能看出哪个key在持续膨胀——很多大key都是悄悄长起来的,等到它拖慢整个实例,改数据结构就得停业务了。