
自Redis 4.0版本起,MEMORY命令家族新增了一个极为实用的子命令——MEMORY USAGE。它允许使用者直接传入一个或多个Key作为参数,返回这些Key及其值在Redis内部近似占用的内存字节数。这对于排查内存占用大户、评估优化效果有着立竿见影的帮助。基本语法很简单:MEMORY USAGE key [SAMPLES count]。如果不带SAMPLES选项,命令默认对嵌套数据结构(如List、Set、Hash、Sorted Set)进行完整扫描以得出精确的估算值;若加上SAMPLES参数并指定采样数量,Redis则会随机抽取指定数量的元素来估算平均值,从而在性能和准确度之间取得平衡,特别适用于包含海量元素的集合。
在实际执行这一命令的过程中,有几处细节值得留意。MEMORY USAGE返回的是字节数,直接对应used_memory相关的统计口径,但并不等同于操作系统进程的RSS(常驻内存集),因为未计入内存分配器的额外开销和碎片。此外,对于已经过期但尚未被删除的Key,该命令仍然会返回内存占用,因为Key尚未真正释放。对于包含超多元素的大集合,不带SAMPLES参数的调用可能造成Redis短暂阻塞,因为需要遍历整个数据结构来汇总大小,此时应当优先采用采样模式进行预估。从Redis 7.0起,该命令还可以接受多个Key,一次返回所有指定Key的估算内存,极大方便了批量诊断。
MEMORY USAGE如何计算Key的内存:从底层结构讲起
要真正理解MEMORY USAGE的输出,必须回溯到Redis对象系统的内存布局。每一个Redis Key在内部都对应一个redisObject结构,这个结构体占用16字节(64位系统下),包含类型、编码、lru时钟、引用计数以及指向底层数据的指针。除了这个对象头部外,Key本身是一个SDS(简单动态字符串)来存储名称。SDS的结构体包含len、alloc和flags等字段,外加柔性数组存放字符串内容,通常一个长度为N的Key,其SDS占用为sizeof(struct sdshdr) + N + 1(额外1字节为隐式结束符),对于较小字符串大约为N+9字节左右。因此,仅是存储一个短Key,就要付出对象头+Key名称SDS的双重成本。
值对象同样遵循redisObject + 具体编码实现的构成。以最简单的字符串为例,值对象如果是整数编码(INT),则指针直接存整数,无需额外SDS;若是EMBSTR或RAW编码,则会再分配一个SDS来承载字符串内容,EMBSTR编码下SDS与redisObject分配在同一内存块,紧凑但不可变,而RAW则独立分配。对于复杂类型,比如HASH,在小规模时使用ziplist(紧凑列表)或listpack(Redis 7.0后替代ziplist),它们将键值对紧凑地连续存储,内存占用高度压缩;但当字段数目或单个值变大后,会转为hashtable,由字典(dict)加上每个dictEntry组成,每个dictEntry除了存储指向键值的指针外,还包含指向下一个条目的next指针,指针本身在64位环境下就是8字节,可见内存膨胀会非常显著。MEMORY USAGE正是通过递归遍历这些内部结构并累加各组件的大小得出估算值。
估算中的隐藏成本:内存分配器与碎片开销
即使MEMORY USAGE返回了一个看似精确的数值,它也无法直接等同于操作系统分配给Redis的物理内存。因为Redis底层使用了高性能的内存分配器,例如默认的jemalloc。分配器为了提高分配效率、减少碎片,会预先向操作系统申请大块内存,并在内部切割成不同大小的slab类。当Redis请求分配一批45字节的对象时,jemalloc可能会从48字节的slab中分配,这就会产生3字节的内部碎片。此外,频繁的释放和分配还会产生外部碎片,导致虽然Redis逻辑上只使用了1GB,但分配器从OS申请的总内存可能达到1.3GB或更多。MEMORY USAGE反映的是Redis对象逻辑尺寸的总和,完全不包括分配器层面的碎片。
除了碎片,还有一些共享内存的场景需要考虑。Redis 在服务启动时会预分配一些共享对象,比如从0到9999的整数回复对象。如果你使用的是SET命令存储数字字符串,Redis可能直接将其解析为整数并使用共享的redisObject,此时多个Key共享同一个值对象,引用计数会大于1。MEMORY USAGE对于这种共享对象,并不会重复计算它的大小,而是仅统计一次,并在任何引用该对象的Key上返回其引用分摊后的极微量开销。另外,过期Key的删除机制也不同步:惰性删除意味着Key在被实际访问时才会检查并删除,而MEMORY USAGE在命令执行时若Key尚未过期,仍然会计算它的内存,因此可能看到“已逻辑过期但仍占据内存”的现象。这提醒我们,在大规模过期场景下,仅靠MEMORY USAGE可能无法获得完全干净的内存视图。
实战:利用MEMORY USAGE定位大Key与内存优化
排查内存问题最直接的方式就是找出占用最高的Key。可以借助redis-cli的管道配合MEMORY USAGE对全量Key进行扫描,但生产环境中全量遍历大数据库风险很高。一种折中但安全的方法是使用SCAN类命令进行游标迭代,每次取出少量Key来执行MEMORY USAGE,并记录超过阈值的Key。例如,通过以下Lua脚本嵌入扫描逻辑,可以有效避免长时间阻塞:
-- 扫描全库,输出内存占用超过100MB的Key
local cursor = 0
local limit = 1000000 -- 阈值:1,000,000字节 ≈ 1MB
repeat
local result = redis.call('SCAN', cursor, 'COUNT', 100)
cursor = result[1]
for _, key in ipairs(result[2]) do
local usage = redis.call('MEMORY', 'USAGE', key)
if usage > limit then
redis.log(redis.LOG_WARNING, 'Large key: ' .. key .. ' size: ' .. usage)
end
end
until cursor == '0'
实践中也可以使用Redis自带的--bigkeys命令行选项,它内部会结合SCAN和DEBUG OBJECT或STRLEN/HLEN等命令给出元素数量维度的统计,但--bigkeys不直接返回内存字节数,而MEMORY USAGE则提供了更精确的字节级视野。对于发现的大Key,进一步观察其内部编码至关重要。使用DEBUG OBJECT key可以获知serializedlength和encoding,结合MEMORY USAGE能够判断该Key是否需要优化。例如,一个包含数百万短字段的Hash,如果当前编码是hashtable,内存占用可能极度膨大,此时可以考虑调整阈值hash-max-ziplist-entries和hash-max-ziplist-value,让Redis更倾向于使用紧凑编码,或者将超大集合切分成多个较小集合。
另一个值得注意的优化点是Key名称的长度。很多人会将业务描述完整写入Key,例如user:10001:profile:detailed:cache,这样的Key名SDS就会消耗约40字节,加上对象头足有50多字节,如果存在上千万个类似的Key,仅Key名称本身就消耗数百MB内存。利用MEMORY USAGE逐一观察可以发现,缩短Key名可以立即释放可观内存,而且通过UNLINK异步删除可以在不影响服务的情况下完成清理后的重建。
注意事项与局限性
虽然MEMORY USAGE是极为趁手的工具,但它并不能完全替代完整的内存分析。第一,针对Stream类型,由于Stream底层的Radix Tree节点会包含压缩前缀,MEMORY USAGE的估算在某些版本中可能不够精确,尤其是在Redis 5.0和6.0中,建议结合MEMORY STATS宏观观察。第二,命令对过期但未被物理删除的Key统计会高估,若搭配Active Expiration机制(随机采样删除),实际情况会变得复杂。第三,在Cluster集群模式下,MEMORY USAGE只能针对当前节点的Key进行,如需评估整个集群的大Key分布,需要轮询所有节点并汇总,并且要小心在重分片过程中Key的迁移。
最后,不要过度依赖采样估算。当使用SAMPLES参数时,Redis会随机选取指定数量的元素,计算其平均大小后乘以元素总数,从而推算出整体内存。对于元素大小分布极度不均的数据集,采样误差可能会让人误判。比如一个List,绝大部份元素是几字节的短字符串,但是内部混杂少量百KB的大字符串,此时采样数若偏小,可能根本不会命中大元素,导致估算值远远低于实际内存。因此建议先通过LLEN、HLEN等命令观察元素数量与类型,再决定是否使用采样的模式,必要时还是要进行全量扫描来获取准确结果。MEMORY USAGE是内存诊断的一把快刀,但只有理解了它的量尺原理与适用边界,才能真正用好它。
Redis MEMORY USAGE内存估算key_memory_usage修改时间:2026-08-12 13:34:05