侧边栏壁纸
  • 累计撰写 142 篇文章
  • 累计收到 2 条评论

Redis 删了大量 key 内存为什么一点没降?我排查 bigkey 和内存碎片踩过的几个坑

2026-10-1 / 0 评论 / 17 阅读
AI 摘要由 AI 生成

Redis删除key后内存未释放,原因在于jemalloc内存分配器,它不会立即回收标记为空闲的内存页。为避免服务雪崩,应避免重启或在线抓数据。Redis 4.0及以上版本支持在线内存碎片整理,可通过配置参数微调碎片整理策略,优化内存使用。排查大key时,不要在生产环境使用`keys *`命令。

上周半夜收到了云服务器的内存报警短信,Redis 实例占用了整整 3.8GB 内存,眼看着就要撞上 4GB 的上限阈值被系统干掉。

我赶紧连上服务器,把业务里一批已经失效的活动历史缓存找出来,写了个脚本清掉了几十万个过期的 Hash 数据。

删完之后在 redis-cli 里敲下 info memory,第一眼看到 used_memory_human 确实从 3.8G 降到了 1.2G,以为万事大吉准备去睡。

结果打开服务器的 top 命令一看,Redis 进程在 Linux 操作系统层面占用的物理内存(RES/RSS)居然纹丝没动,依然是稳稳的 3.8GB。

不仅如此,由于数据被清空了一大半,mem_fragmentation_ratio(内存碎片率)直接从原本健康的 1.1 飙升到了 3.1 以上。

很多用 Redis 的开发者都会遇到这个灵魂拷问:数据明明白白从 Redis 里删掉了,为什么操作系统占用的内存不降反升?

如果对底层机制不了解,盲目重启或者在线抓数据,很容易引发更严重的单线程卡顿甚至服务雪崩。把当时排查出来的几个关键暗坑理清楚,下次遇到 Redis 内存暴涨就能从容处理。

删了 key 内存却不释放,根源在 jemalloc 内存分配器

要弄清楚为什么物理内存没降,先要看懂 info memory 里两个最核心的指标:

# Memory
used_memory:1288490188
used_memory_human:1.20G
used_memory_rss:4080218112
used_memory_rss_human:3.80G
mem_fragmentation_ratio:3.17

这里的 used_memory 是 Redis 自己记录的、存储当前所有键值对所需要的纯数据大小;而 used_memory_rss 则是操作系统分配给 Redis 进程的实际常驻物理内存大小。

两者相除得到的 mem_fragmentation_ratio 就是内存碎片率。大于 1.5 说明碎片化已经比较严重,超过 2.0 就意味着超过一半的物理内存都在被碎片空耗。

为什么删掉数据后,操作系统不回收内存?

Redis 默认使用的是 jemalloc 作为内存分配器。为了追求极高的读写性能,jemalloc 不会按需分配零散的字节,而是按照固定大小的内存页(比如 8B、16B、32B、4KB 等)统一向操作系统申请整块内存池。

当你删掉一个 key 时,Redis 确实把这个 key 所占的数据块标记为了空闲,但 jemalloc 并不会立刻把这整块物理内存页交还给操作系统。

这就好比你包下了一整个酒店楼层,哪怕大部分房间里的客人退房了,只要还有少数几个房间有人住,整层楼的租约依然被挂在你的账上。只有当某个连续内存页里的所有数据全部被清空,底层才可能把内存归还给 Linux 内核。

不重启实例,在线怎么做无损碎片整理?

很多老教程遇到碎片率高,教的方法简单粗暴:主从切换或者重启 Redis。

如果是单机运行或者核心线上服务,重启会导致缓存穿透,瞬间把数据库打崩。从 Redis 4.0 开始,官方就内置了在线主动内存碎片整理功能(Active Defragmentation)。

在客户端执行下面这行命令,立刻开启动态碎片整理:

config set activedefrag yes

开启之后,Redis 会在后台利用 CPU 的空闲时间周期性扫描内存页,把零散的数据搬迁合并到连续的新内存页中,然后把完全腾空的旧页释放给操作系统。

为了防止碎片整理吃太多 CPU 影响正常的读写性能,可以在生产环境微调这几项阈值:

# 内存碎片占用超过 100MB 且碎片率超过 1.5 时才触发整理
config set active-defrag-ignore-bytes 104857600
config set active-defrag-threshold-lower 50

# 限制碎片整理占用的 CPU 上限,默认 20% 到 75% 之间
config set active-defrag-cycle-min 10
config set active-defrag-cycle-max 25

配上这些参数后观察了二十分钟,used_memory_rss 平稳回落到了 1.4GB 左右,碎片率恢复到 1.15,整个过程没有引起任何请求延迟毛刺。

千万别在生产环境执行 keys * 查大键

查出是哪些 key 在疯狂吃内存是排查的第一要务。但很多新手上去就执行:

keys *

只要 Redis 里存了几十万上百万个 key,这一行命令就会把 Redis 单线程主事件循环彻底堵死。在此期间,所有的读写请求全部排队超时,秒级延迟直接把依赖缓存的微服务接口全部拖挂。

安全揪出内存元凶的正确姿势

排查大键(bigkey)有两个安全又规范的姿势。

第一种是用 Redis 内置的非阻塞大键扫描工具。在服务器终端上执行:

redis-cli -h 127.0.0.1 -p 6379 -a "your_password" --bigkeys -i 0.01

这里的 -i 0.01 非常关键,它代表每扫描 100 个 key 就休眠 0.01 秒(10 毫秒),确保扫描过程不会占用过高 CPU,对线上业务完全透明。

扫描完毕后它会输出每种数据类型里元素最多的大键。

第二种姿势是针对具体疑似大键,查看它的实际内存开销:

MEMORY USAGE user:session:cache:98231

返回的数字是该 key 在内存中占用的实际字节数。这比简单的 STRLEN 或者 HLEN 更准确,因为它把 jemalloc 的元数据开销也算进去了。

批量 DEL 大键直接导致服务卡死几百毫秒

找到那个占了几十兆内存的 Hash 大键之后,很多人的下意识操作是敲:

DEL my_huge_hash_key

这又是一个致命暗坑。

Redis 处理 DEL 命令是在主线程中同步释放内存的。如果这个 key 是一个包含数十万元素的 Hash、List 或者 ZSet,单线程需要遍历释放上万个节点指针,这一步可能耗时几百毫秒甚至数秒。

对于微秒级响应的 Redis 来说,几百毫秒的阻塞足以让 upstream 接口抛出大量的超时报警。

怎么异步安全释放大键?

在 Redis 4.0 之后,删除大键必须换成非阻塞删除指令:

UNLINK my_huge_hash_key

UNLINK 会先在逻辑层把这个 key 从字典中摘除(客户端瞬间感知到删除完成),真正的内存释放操作则转交给后台专门的异步线程池慢条斯理地清理,丝毫不耽误主线程处理业务命令。

更彻底的做法是在 redis.conf 里把所有潜在的阻塞删除全都改成异步化:

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

开启这四项之后,不管是因为达到 maxmemory 触发淘汰、还是 key 到期自动过期、或者是被覆盖写入,后台一律异步释放,再也不怕偶发性卡顿。

淘汰策略配错,写入直接报 OOM command not allowed

当内存确实被正常业务数据占满时,如果配置不当,客户端就会收到:

OOM command not allowed when used memory > 'maxmemory'

这是因为 Redis 默认的淘汰策略是 noeviction。在这个策略下,一旦内存用满,除了读请求和删除请求,所有新增的写操作一律直接报错拒绝。

很多独立项目部署时图省事,连 maxmemory 都不设。不设上限的后果更惨烈:Redis 肆无忌惮地吃满宿主机内存,直接被 Linux 内核的 oom-killer 无情打死。

生产环境的标准防爆仓配置

一个稳健的生产 Redis 实例,通常必须加上这两条防线:

# 为 Redis 设置一个硬顶上限,通常给宿主机留出 20% 到 30% 余量
maxmemory 3gb

# 达到上限后,优先淘汰设置了过期时间的冷门键
maxmemory-policy volatile-lru

业务里大部分缓存数据都会带上过期时间(TTL)。选用 volatile-lru 可以确保只有具有生存周期的缓存数据会被自动清理,而那些没有设置 TTL 的核心持久化配置或永久 Token 绝不会被误删。

如果你的 Redis 纯粹充当无状态热点缓存,也可以改成 allkeys-lru,让它像个滑动窗口一样只保留最新热点数据。

一套检查 Redis 内存健康的排障步骤

下次遇到 Redis 内存居高不下,按照这套步骤顺藤摸瓜即可:

  1. 查指标:执行 info memory,对比 used_memory 和 used_memory_rss。如果两者差距悬殊且碎片率大于 1.5,说明是碎片问题,配置 activedefrag yes 在线平息。
  2. 扫大键:执行 redis-cli --bigkeys -i 0.01,找出占领内存的集合大键,千万别用 keys *。
  3. 查单键体积:用 MEMORY USAGE <key> 验证大键的精确字节大小。
  4. 异步清除:删除大键用 UNLINK 代替 DEL,配置文件开启 lazyfree 相关选项。
  5. 守住底线:明确设置 maxmemory 和合适的 maxmemory-policy,防止无休止占用导致系统强杀。

把分配器回收机制、后台碎片整理与异步释放理解透彻,Redis 的内存报警就不再是个让人头大的黑盒难题。

评论一下?

OωO
取消