Redis 内存优化与大 Key 治理实战:把内存花在刀刃上

Redis 内存告警响起来的时候,多数人的第一反应是「数据涨太快了」,但真正去看,往往是另一回事:几个没拆开的大 key 占着大半内存、碎片率虚高、或者淘汰策略配错把缓存打成了穿透。本文以 Redis 6.2 / 7.x 为例(单机与主从均可),把内存治理拆成五步:先看懂内存构成,再定位大 key 并安全处置,然后从数据结构选型上省内存,最后把上限和淘汰策略定对。示例实例运行在 db-01,地址用 192.0.2.21 占位。

前置条件

  • redis-cli 能连上目标实例,账号具备 INFO、MEMORY、SCAN 等命令权限;
  • 已知实例当前的 maxmemory 设置与数据量级,最好手边有近一周的内存监控曲线,方便判断是突涨还是缓涨;
  • 大 key 删除、改配置等动作安排在业务低峰期执行,并提前和业务方打招呼;
  • 若要做 RDB 离线分析,准备一台能落盘的分析机器,不要在生产实例上直接跑重分析工具;
  • 主从或哨兵环境先只在单个节点上验证动作,再推广到全部节点。

第一步:读懂 INFO memory

治理之前先把内存的账算清楚。INFO memory 里最关键的是四个数:used_memory 是 Redis 视角的已用内存(数据加内部开销);used_memory_rss 是操作系统视角的实际占用;两者之比就是 mem_fragmentation_ratio,经验上在 1 到 1.5 之间算正常,超过 1.5 才值得处理,低于 1 反而危险,说明内存被换到了 swap 上;used_memory_dataset 是纯数据部分,拿它和 used_memory 对比,能看出 key 本身的元数据开销占比。另外要同时看 maxmemory 是否已触顶、evicted_keys 是否在持续增长——如果已经在淘汰了,当务之急是先扩容或降流量,而不是慢慢做优化。

redis-cli -h 192.0.2.21 -p 6379 INFO memory | egrep 'used_memory_human|used_memory_rss_human|used_memory_dataset|mem_fragmentation_ratio|maxmemory'
redis-cli -h 192.0.2.21 -p 6379 INFO stats | egrep 'evicted_keys|keyspace_hits|keyspace_misses'

第二步:发现大 key

大 key 的判断要同时看字节数和元素数:string 类型超过 10 KB 量级、hash / list / zset 等集合类型元素超过 5000 个,就值得纳入治理清单(具体阈值按自家业务定)。发现手段有三种,各有取舍。--bigkeys 基于 SCAN 在线抽样,每类只报最大的那个,胜在快、影响小,适合日常巡检;redis-rdb-tools 对 RDB 文件做离线全量分析,结果最全,但需要一份快照并落盘分析,适合定期体检;在线抽样脚本用 SCAN 分批拉 key 再逐个 MEMORY USAGE,粒度最细,但跑得慢、要控制节奏。生产环境一般组合使用:巡检用 --bigkeys,定位到类型后再用抽样脚本把同类大 key 捞全。

# 在线抽样:每类报最大的 key 与平均大小
redis-cli -h 192.0.2.21 -p 6379 --bigkeys

# 离线全量:对 RDB 做内存分析,再按 bytes 列筛大 key
rdb -c memory /data/redis/dump.rdb -f /tmp/memory.csv
# 在线抽样脚本:按前缀捞 key 并输出内存占用最大的 20 个
redis-cli -h 192.0.2.21 -p 6379 --scan --pattern 'cache:*' > /tmp/keys.txt
while read -r k; do
  echo "$k $(redis-cli -h 192.0.2.21 -p 6379 MEMORY USAGE "$k" SAMPLES 5)"
done < /tmp/keys.txt | sort -k2 -nr | head -n 20

第三步:大 key 的拆分与安全删除

治理分两头。拆分是治本:大 hash 按字段哈希拆成多个小 hash,大 list 按时间分桶,排行榜类 zset 按周期拆分,让单个 key 回到合理量级,拆分逻辑要落在写入侧,存量数据用脚本迁移。删除是止损,但删大 key 本身就是高危动作:DEL 是同步释放内存,key 越大主线程卡得越久,几十 MB 的 key 能卡出明显的时延毛刺。正确姿势是用 UNLINK 把释放动作丢给后台线程;集合类先用 HSCAN / ZSCAN 分批删元素、最后删空壳;再把 lazyfree 系列开关打开,让过期删除和显式删除都走异步路径。

# 异步删除,不阻塞主线程
redis-cli -h 192.0.2.21 -p 6379 UNLINK cache:product:all

# 确认 lazyfree 相关配置(改动需同步落配置文件)
redis-cli -h 192.0.2.21 -p 6379 CONFIG GET 'lazyfree-*'

第四步:数据结构选型省内存

很多内存是「选错结构」浪费的。Redis 对小集合有紧凑编码:hash 在元素数不超过 hash-max-listpack-entries(默认 128)、每个 value 长度不超过 hash-max-listpack-value(默认 64 字节)时用 listpack 存储,内存占用比 hashtable 低一个量级;一旦越界就整体转成 hashtable,内存陡增。所以小对象建模时要刻意控制字段数和字段长度,大字段(如整段 JSON)不要塞进 hash,单独用 string 存。类似地,少量状态标记用 bitmap 比开一堆 string key 省得多,去重计数用 HyperLogLog 只要 12 KB;zset、list 也有各自的 listpack 阈值,建模前先查一遍当前配置再决定结构。

redis-cli -h 192.0.2.21 -p 6379 CONFIG GET 'hash-max-listpack-*'
redis-cli -h 192.0.2.21 -p 6379 CONFIG GET 'zset-max-listpack-*'
redis-cli -h 192.0.2.21 -p 6379 OBJECT ENCODING cache:user:1001

第五步:上限与淘汰策略定口径

maxmemory 不要贴着机器内存设:要给操作系统、复制积压缓冲和 fork 时的写时复制留余量,经验上按可用内存的七到八成封顶,并预估数据翻倍时的水位。淘汰策略按场景选:纯缓存用 allkeys-lru 或更贴近访问频率的 allkeys-lfu;只有带过期时间的 key 才允许被淘汰的场景用 volatile-lru / volatile-lfu;noeviction 适合把 Redis 当存储用的场景,配错在缓存上会在写满时直接拒绝写入。改完用 CONFIG REWRITE 落盘,避免重启丢失。热 key 是另一个话题——它是流量问题而非内存问题,常规解法是本地缓存、读副本分摊和 key 打散,不在本文展开。

redis-cli -h 192.0.2.21 -p 6379 CONFIG SET maxmemory 8gb
redis-cli -h 192.0.2.21 -p 6379 CONFIG SET maxmemory-policy allkeys-lfu
redis-cli -h 192.0.2.21 -p 6379 CONFIG REWRITE

验证

  • INFO memory 前后对比:used_memory 水位下降,mem_fragmentation_ratio 回到 1.5 以内;
  • --bigkeys 复跑,清单内的超标 key 已清零或完成拆分;
  • evicted_keys 增量恢复平稳,keyspace 命中率没有异常下跌;
  • 删除动作执行期间,主线程时延监控无明显毛刺。

常见问题

碎片率高就是内存浪费、要重启?

多数情况不是。碎片来自 jemalloc 的分配方式和数据删除后的空洞,先看 rss 与 used 的绝对差值再下结论,差值不大就不用管。真需要处理时,Redis 4.0 以上可开 activedefrag 做主动碎片整理(先在从节点验证参数),不要动不动重启——重启意味着缓存全冷、还要承受回源风暴。

删大 key 时出现时延毛刺

基本是直接用了 DEL 同步释放。改用 UNLINK,集合类先分批删元素再删壳,并且把动作放在低峰期;如果毛刺来自过期删除,把 lazyfree-lazy-expire 打开,让过期路径也走异步释放。

内存一到上限就出现淘汰风暴

通常是上限贴边加上大量 key 集中过期:evicted_keys 暴涨、命中率雪崩、回源把数据库再打一遍。对策是给 TTL 加随机抖动错开过期时间、上限留足余量、把淘汰速率纳入告警,涨水位时先扩容止损再慢慢治理,不要在风暴里做结构手术。

小结

Redis 内存治理的顺序不能乱:先用 INFO memory 把账算清,再定位大 key 并用 UNLINK 这类安全方式处置,结构选型解决的是长期浪费,上限与淘汰策略决定的是出事时的下限。把大 key 清单做成巡检项、把阈值写进规范,内存问题就不会每次都以告警的形式见面。

发表回复

后才能评论