服务覆盖:昆明·曲靖·玉溪·保山·昭通·丽江·普洱·临沧·楚雄·红河·文山·西双版纳·大理·德宏·怒江·迪庆

Redis内存优化实战:线上从8GB降到2GB的调优之路

eycit 2026-04-19 -3 次阅读 系统安装
---

theme: default themeName: "默认主题" title: "Redis内存优化实战:线上从8GB降到2GB的调优之路"


Redis内存优化实战:线上从8GB降到2GB的调优之路

前阵子线上告警炸了,Redis实例内存使用率冲到95%,眼看就要触发OOM。运维同事慌得一批,说加内存吧,我拦住了——8GB的Redis,你能加到多少?16GB?32GB?治标不治本。

花了一周时间排查优化,最终把内存从8GB压到2GB,稳稳当当跑到现在。整个过程踩了不少坑,记录下来给各位参考。

问题暴露:info memory先走一遍

出问题的第一时间,别急着改代码,先摸清家底。连上Redis执行:

redis-cli info memory

输出一堆指标,重点关注这几个:

used_memory:8589934592          # 8GB,实际分配的内存

used_memory_human:8.00G used_memory_rss:9126805504 # 操作系统视角的RSS,比used_memory还大 used_memory_peak:9126805504 mem_fragmentation_ratio:1.06 # 碎片率还算正常 maxmemory:0 # 没设上限! maxmemory-policy:noeviction # 没设淘汰策略!

两个致命问题直接暴露:没设maxmemory没设淘汰策略。这就好比你家的水龙头没装阀门,水想流多少流多少。

再看key的分布情况:

redis-cli --scan --pattern "*" | head -1000

redis-cli dbsize

dbsize返回120万,key数量不算离谱,但内存却吃了8GB,说明要么有大Key,要么数据结构选型有问题。

第一刀:揪出大Key

Redis自带的`redis-cli --bigkeys`是个好东西,直接扫:

redis-cli --bigkeys -i 0.1

输出结果让我倒吸一口凉气:

Biggest string: 'user:cache:profile:batch_001' (524288000 bytes)

Biggest list: 'task:queue:failed' (342891 items) Biggest hash: 'config:all_settings' (8291 fields) Biggest set: 'tags:hot_keywords' (41203 members) Biggest zset: 'rank:daily:score' (28901 members)

一个String类型的Key占了500MB!追到业务代码一看,某位天才开发把整个用户画像批量序列化成一个JSON塞进了Redis,还设了24小时过期。这种Key有十几个,光这一项就干掉将近6GB。

处理方案很直接——拆:

# 之前:一个大Key搞定一切

redis.set("user:cache:profile:batch_001", json.dumps(huge_profile_list))

之后:按用户ID拆分,每个Key只存单条数据

for user_id, profile in profile_list: redis.set(f"user:cache:profile:{user_id}", json.dumps(profile), ex=3600)

拆完之后,500MB的大Key变成了几十KB的小Key,内存直接降到2.3GB。这一刀砍掉了最大的毒瘤。

对于那种确实需要批量获取的场景,用Pipeline或者MGET搞定,别把大象塞进一个Key里。

第二刀:List大Key的清理

`task:queue:failed`这个队列积压了34万条数据,全是失败任务的记录,业务方早就不用了但没人清理。直接DEL会把Redis卡死(后面会说为什么),用`HSCAN`类似的思路分批删:

# 每次弹出1000条,直到清空

while true; do count=$(redis-cli LLEN task:queue:failed) if [ "$count" -eq 0 ]; then break fi redis-cli LTRIM task:queue:failed 1000 -1 sleep 0.1 done

更优雅的做法是删Key时开启lazyfree(Redis 4.0+),后面细说。

第三刀:数据结构优化,ziplist立大功

内存降到2.3GB后还不满意,继续挖。Redis的Hash类型在小数据量时有个隐藏福利——当元素个数少于`hash-max-ziplist-entries`(默认512)且每个元素大小小于`hash-max-ziplist-value`(默认64字节)时,Redis会自动用ziplist编码存储,而不是hashtable。

ziplist是什么?一段连续内存的压缩列表,省掉了hashtable里指针和桶的开销。同样存100个field-value对,ziplist可能只要hashtable三分之一的空间。

问题是我们有些Hash Key的field数超过了512,被自动升级成了hashtable。检查方法:

redis-cli HLEN config:all_settings

8291个field,远超512阈值

优化思路:把大Hash拆成多个小Hash:

# 之前

redis.hset("config:all_settings", mapping=all_settings)

之后:按命名空间拆分

for namespace, settings in grouped_settings.items(): redis.hset(f"config:{namespace}", mapping=settings)

除了Hash,List和Sorted Set也有类似的ziplist优化:

  • `list-max-ziplist-size -2`(默认值,每个节点8KB以内用ziplist)
  • `zset-max-ziplist-entries 128`(默认128个元素以内用ziplist)

合理设计Key的粒度,让数据尽量落在ziplist区间内,内存能省一大截。

第四刀:过期策略和淘汰策略

回看最开始info memory的输出,`maxmemory-policy:noeviction`简直是在裸奔。内存满了直接拒绝写入,线上服务直接炸。

配置淘汰策略:

# redis.conf

maxmemory 4gb maxmemory-policy allkeys-lru

选`allkeys-lru`而不是`volatile-lru`是有原因的——我们的Key有些没设过期时间,`volatile-lru`只淘汰设了TTL的Key,那些不设TTL的冷数据反而赖着不走。`allkeys-lru`不管你设没设过期,最久没访问的就先淘汰,简单粗暴但管用。

同时检查过期Key的情况:

redis-cli info stats | grep expired

expired_keys:238471

24万个Key靠惰性删除+定期删除清理,效率不高。Redis 6.0引入了`lazyfree-lazy-expire`,让过期Key的删除放到后台线程:

# redis.conf

lazyfree-lazy-expire yes lazyfree-lazy-eviction yes lazyfree-lazy-del yes replica-lazy-flush yes

开启之后,淘汰和过期操作不再阻塞主线程,CPU占用率反而降了。

第五刀:UNLINK代替DEL

前面提到删除大Key会卡死Redis,传统DEL命令是同步阻塞的。Redis 4.0提供了`UNLINK`命令,后台异步删除:

# 别用这个

DEL task:queue:failed

用这个

UNLINK task:queue:failed

代码里也要改:

# Python redis-py

redis.delete("big_key") # 同步删除,阻塞 redis.unlink("big_key") # 异步删除,不阻塞

配合前面的`lazyfree-lazy-del yes`配置,即使不小心调了DEL,Redis也会尝试异步处理(仅在配置开启时)。

最终效果

全部优化落地后,再跑一次info memory:

优化前:

used_memory:8.00G used_memory_rss:8.50G mem_fragmentation_ratio:1.06 keys:1200000

优化后: used_memory:1.92G used_memory_rss:2.10G mem_fragmentation_ratio:1.09 keys:1380000

Key数量反而增加了(大Key拆小Key),内存却从8GB降到2GB,降幅超过75%。

几条经验总结

1. 大Key是内存杀手。 一个Key超过1MB就得警惕,超过10MB必须拆。用`redis-cli --bigkeys`定期巡检,别等OOM了才想起来看。 2. 数据结构选型比你想的重要。 同样的数据,用Hash的ziplist编码能比String省3-5倍空间。设计Key结构时就考虑好拆分粒度。 3. maxmemory和淘汰策略必须配。 不配就是给线上埋雷,内存疯涨时连个兜底都没有。 4. lazyfree全家桶都开上。 Redis 6.0的异步删除机制成熟了,线上跑了大半年没出过问题,主线程延迟明显下降。 5. 监控比优化更重要。 上了Grafana监控Redis内存使用率、Key增长趋势、命令延迟分布,异常提前发现,不用等告警了才手忙脚乱。

内存优化没有银弹,每家业务情况不一样。但排查思路是通用的:先看内存分布,再找大Key,然后优化数据结构和淘汰策略,最后上监控形成闭环。照着这套流程走,8GB降到2GB不是什么神话,你也能做到。

【放心,我们兜底】

不管你是自己尝试修复,还是需要专业人员上门,易云城IT服务都给你托底。修不好不收费,修好了质保期内随时找我。

📞 服务热线:13708730161 💬 微信:eyc1689 📧 邮箱:service@eycit.com 🌐 https://www.eycit.com

您身边的IT专家。

上一篇
Linux服务器CPU 100%排查实录:从top到火焰...
下一篇
Linux内核参数调优:让Nginx并发能力从5千到5万...