2. Redis内存模型全景

要治理Redis内存,先得知道内存到底花在了哪里。Redis并不是简单地把数据塞进一块连续内存,而是由分配器、对象系统、编码方式、碎片和统计指标共同构成的一整套内存模型。理解这套模型,才能把used_memory、mem_fragmentation_ratio这些数字翻译成可执行的优化动作。

一、内存分配器:Redis与操作系统之间的中间层

Redis不直接调用malloc/free,而是通过内存分配器管理堆内存。默认使用jemalloc,也可编译为libc、tcmalloc。分配器的作用是:向操作系统申请大块内存,再按需切分给Redis对象,同时记录已分配但未使用的空间。

jemalloc按大小分类管理内存,常见size class如8、16、32、48、64、80、96、112、128字节等。这意味着申请一个100字节的对象,实际可能占用112字节的slot。这种“向上取整”是内存碎片和额外开销的来源之一,也是used_memory与used_memory_rss出现差异的原因。

关键指标:used_memory是分配器认为Redis已使用的字节数;used_memory_rss是操作系统看到的常驻内存;mem_allocator显示当前分配器名称。三者结合,才能判断是“对象太多”还是“分配器留了太多空闲页”。

二、对象系统与编码方式:同一逻辑类型,多种物理形态

Redis中每个键值对都是一个redisObject,包含type、encoding、lru、refcount和ptr。type是逻辑类型(string、list、hash、set、zset),encoding是物理编码。同一type在不同数据规模下会切换encoding,内存占用差异巨大。

以String为例:短整数值可用int编码,直接存在ptr中;长度≤44字节的字符串用embstr,对象头与SDS连续分配;更长的用raw,对象头与SDS分开分配。Hash在小数据量时用listpack(旧版本为ziplist),超过hash-max-listpack-entries或hash-max-listpack-value后转为hashtable。List使用listpack或quicklist,Set使用intset、listpack或hashtable,ZSet使用listpack或skiplist。

编码转换是单向的:一旦从紧凑编码转为大编码,即使后续删除元素,也不会自动回退。因此,批量写入再删除大量元素,可能留下一个“空但占内存”的大编码对象。这是容量治理中非常隐蔽的陷阱。

Redis 内存模型全景:从分配器到统计指标 ① 内存分配器层(jemalloc / libc / tcmalloc) 向 OS 申请大块内存 按 size class 切分 记录已分配 / 空闲页 used_memory / used_memory_rss ② 对象系统层:redisObject type encoding lru refcount ptr 逻辑类型:string / list / hash / set / zset → 物理编码由数据规模决定 ③ 编码方式层:紧凑编码 ↔ 大编码(单向转换) String: int / embstr / raw Hash: listpack / hashtable List: listpack / quicklist Set: intset / listpack / ht ZSet: listpack / skiplist 转换触发:元素数 > max-listpack-entries 或 值长度 > max-listpack-value ④ 碎片与统计指标 mem_fragmentation_ratio = used_memory_rss / used_memory | ratio > 1.5 需关注 | ratio < 1 可能发生 swap

三、内存碎片:ratio背后的两种成因

碎片分为内部碎片和外部碎片。内部碎片来自分配器的size class取整,例如申请100字节实际占112字节;外部碎片来自频繁分配释放后留下的空洞,操作系统无法合并利用。Redis通过mem_fragmentation_ratio = used_memory_rss / used_memory衡量碎片程度。

经验判断:ratio在1.0~1.5之间通常正常;持续大于1.5说明RSS明显高于分配器记录,存在外部碎片或分配器未归还内存;ratio小于1则说明部分内存被换出到swap,性能会急剧下降。注意,ratio高不一定都要立即处理,如果used_memory_rss稳定且远低于maxmemory,可以观察;如果接近上限,则需要activedefrag yes或重启/迁移。

四、统计指标:把数字翻译成动作

常用指标及含义:

  • used_memory:分配器记录的Redis数据+开销,是maxmemory比较的对象。
  • used_memory_rss:操作系统视角的常驻内存,包含碎片和分配器保留页。
  • used_memory_peak:历史峰值,用于评估容量水位。
  • used_memory_lua、used_memory_scripts:Lua脚本和脚本缓存占用。
  • mem_fragmentation_ratio:碎片率,判断是否需要整理。
  • mem_allocator:当前分配器,jemalloc下碎片表现通常更好。
  • maxmemory_policy:达到上限后的淘汰策略,直接影响内存行为。

排查时建议按顺序看:先看used_memory是否接近maxmemory,再看used_memory_rss与used_memory的差值,最后看mem_fragmentation_ratio。如果used_memory正常但RSS很高,优先怀疑碎片或分配器未归还;如果两者都高,则要检查大key、编码转换和过期键堆积。

编码转换不可逆:一个hash从listpack转为hashtable后,即使删到只剩1个字段,仍占用hashtable的内存。治理时要么控制写入规模,要么在批量删除后重建键。

五、一个可复用的检查顺序

面对内存告警,可以按以下顺序定位:

  1. 执行INFO memory,记录used_memory、used_memory_rss、mem_fragmentation_ratio、maxmemory。
  2. 若used_memory接近maxmemory,用redis-cli --bigkeys或MEMORY USAGE key找大key。
  3. 若used_memory_rss远大于used_memory,检查是否有大量过期键未清理,或编码转换后未释放。
  4. 若mem_fragmentation_ratio持续大于1.5,评估开启activedefrag,或安排低峰期重启/迁移。
  5. 检查maxmemory-policy是否与业务匹配,避免淘汰策略导致内存反复波动。

示例:某实例used_memory为3.2GB,used_memory_rss为5.1GB,maxmemory为4GB,mem_fragmentation_ratio约1.59。此时used_memory尚未触顶,但RSS已超过maxmemory,说明碎片和分配器保留页占用了大量空间。可先开启activedefrag yes并观察mem_fragmentation_ratio是否下降;若无效,再考虑在业务低峰期做一次主从切换或重启,让分配器重新整理内存。

把这些层次串起来看:分配器决定“怎么给”,对象系统决定“怎么表示”,编码方式决定“占多少”,碎片决定“浪费多少”,统计指标则是“怎么看”。只有把指标和这四层对应起来,内存治理才不是盲目调参。