☰
Redis内存模型全解析:从内存开销到碎片治理实战
2026/10/7 22:51:28 网站建设 项目流程

聊Redis内存模型,很多人第一反应是“这不就是看看内存里存了多少数据嘛”,真到了线上出问题才发现,内存这块的水比想象中深得多。我见过不少团队,业务高峰期Redis突然OOM,日志刷出一堆错误,排查到最后发现根本不是数据量暴增,而是某个list里塞了几个超大对象;也见过明明只存了几GB数据,RSS却飙到十几GB,碎片率高得吓人,服务一重启又恢复正常。这些现象,没点内存模型的底子,排查起来基本靠猜。

这篇笔记想把Redis内存模型讲透,包括内存从哪来、花在哪、怎么监控、怎么优化,以及我实际踩过的一些坑。它适合正在系统学习Redis的开发者,也适合线上Redis出过问题、想真正搞明白内存开销的运维和架构师。我不会堆概念,更多是拿命令和实例去验证,尽量做到看完能直接上手排查。

1. 为什么需要单独理解Redis内存模型

1.1 内存模型决定了容量估算和性能上限

Redis号称全内存数据库,这句话大家都会说,但“一个key-value到底占多少内存”,能一口答上来的人真不多。我自己做过一次实测:写入500万个短字符串key-value,key大约10个字符、value大约50个字符,内存占用轻松超过600MB。这里有个最常见的估算错误——很多人只算了value字符串的长度,把key本身的长度、结构体开销、编码头部、哈希表指针这些全部忽略了。容量规划一旦从一开始就偏差,后面加内存、加淘汰策略都只是救火,不是治病。

Redis的高性能也严重依赖内存管理方式。它不像MySQL那样有复杂的缓冲池,而是把对象直接交给内存分配器管理。分配器选得不好、碎片控制不力,内存使用率会直线下降。理解了内存模型,你才知道为什么某些操作会造成内存暴涨,比如写大key、批量写入、删除大集合,它们底层的扩容和回收机制完全不一样。

1.2 建议的学习路径:先建模型,再谈调优

如果你第一次接触Redis内存模型,我强烈建议按“内存从哪来、花在哪、怎么看、怎么省”四步走。先搞清楚used_memory、used_memory_rss、mem_fragmentation_ratio这几个指标的含义,再拿实际数据做实验,用memory usage命令验证自己对某个类型内存开销的估算。最后再谈调优和淘汰策略。最忌讳的做法是照着网上的“内存优化十条”直接抄配置,不理解背后机制,出了问题依然无从下手。

我写这篇笔记的起因,是因为一次线上事故复盘时发现,团队对Redis内存的认知停留在“满了就expire,再不行就加内存”的层面,完全没有系统性模型。那次之后我把内存相关的内容逐个验证了一遍,这篇文章就是验证记录。

2. Redis内存全景拆解:内存花在哪

2.1 内存的四个主要去向

Redis的内存消耗不能简单理解成“数据占了多大”。以我的排查经验看,一台Redis实例的内存去向大致可以分为四类。

第一类也是最大的一类,是数据自身占用的内存,包括所有key的字符串内容、所有value的内容,以及字典结构本身。这里特别容易被忽视的是key的开销。很多业务为了可读性,key起得特别长,比如user:profile:10086:2024,一个key就接近30个字符了,如果value只有几十字节,key的开销占比已经相当可观。我们在线下环境用memory usage命令实测过,一个长度为40字符的key加一个长度为40字符的value,整体内存开销可以到150字节左右,比你预期的80字节高出近一倍。

第二类是公共元数据开销。Redis作为多线程IO、单线程执行命令的架构,每个对象都有类型、编码、引用计数等元信息,这些在RedisObject结构体里体现。这部分在每个key-value上都会叠加,而且是固定的,数据量小时占比不明显,数据量大时就是几GB的差异。后面我会专门算一下这笔账。

第三类是各类缓冲区。包括客户端的输入输出缓冲区、主从复制使用的积压缓冲区、AOF重写和RDB持久化过程中的临时内存。线上最常见的问题是客户端输出缓冲区不受控制,某个慢消费的客户端订阅了大量消息,缓冲区会持续膨胀,直接把内存打满。

第四类是内存碎片。这是最容易被忽视、也最影响判断的一类。碎片来自分配器的内存分配策略,比如jemalloc按固定大小类别分配,申请64字节时分配器可能给了80字节;又比如频繁增删键导致的内存空洞。碎片率一高,used_memory看起来正常,RSS却暴涨,非常迷惑人。

2.2 jemalloc分配器与碎片率

Redis 4.0以后,官方强烈推荐使用jemalloc作为内存分配器,原因在于它在高并发分配场景下有更好的性能,并且提供了内存碎片统计能力。不过jemalloc不是银弹,它的分配单元是按大小类别划分的,比如tiny、small、large这几个级别。当你申请一个随机的字符串长度时,实际分配的内存往往大于你申请的字节数,多出来的部分就是“内部碎片”。

内部碎片本身不可怕,正常情况下碎片率在1.0到1.5之间都算健康。真正麻烦的是“外部碎片”,即频繁写入、删除、过期之后,内存区出现了大量空闲但不连续的小块,新的申请无法复用这些区块。这时候mem_fragmentation_ratio会持续走高,甚至超过2.0,而数据本身并没有增长多少。

我在生产环境遇到过最典型的情况:某个业务每小时批量写入一批带TTL的key,key生命周期只有30分钟,高峰时每秒删除上千个key。运行一个月后,used_memory只有2GB,RSS却占到7GB,碎片率3.5。后来开启了activedefrag,碎片率慢慢回落到1.8左右。关于碎片整理的详细操作,我在后面的排查专题里再展开。

3. 内存开销的核心机制细节

3.1 一个key-value到底吃掉多少内存

要真正理解Redis内存模型,必须知道一个key-value在内存里的组成。我以String类型、短字符串为例拆解一下。

一个普通短字符串key,在Redis中有三部分开销:

  • dictEntry:哈希表中的一个条目,占用24字节左右,包含key指针、value指针、next指针。
  • redisObject:表示value的对象头,占用16字节左右,包含type、encoding、lru、refcount、ptr指针。
  • SDS:Redis自己的字符串结构。以sdshdr8为例,头占3字节,加数据长度再加结尾的null字符。

我做个简单估算。假设key是“hello”,长度5字符,SDS占3+5+1=9字节;value是“world”,同样9字节。加上dictEntry 24字节、redisObject 16字节,一共约58字节。但这里是理论值,jemalloc会按大小类别做对齐分配,实际用memory usage命令跑出来往往是72字节、80字节甚至更多。注意,这还是没有算上哈希表渐进式扩容时多出来的bucket数组。

有了这个估算基础,你就能推导出一个经验公式:Redis中一个短字符串key-value,真实内存占用通常是key和value加起来字面长度的3到5倍。我在面试候选人时经常问这个问题,能答到这个量级的,说明对Redis底层存储有真正概念。

3.2 五种数据类型的编码与内存差异

Redis五种基本类型的内存差异,核心在于编码方式不同。我用最典型的两个版本对比来说明。

String类型有三种编码:int、embstr、raw。当一个字符串可以被解析为整数时,Redis直接把它以整型存在ptr里,不再分配SDS,内存效率最高。短字符串(通常小于44字节,具体看版本)使用embstr编码,redisObject和SDS在连续内存块里,只分配一次内存。超过阈值后变为raw编码,redisObject和SDS分开分配,多一次内存分配开销,同时SDS结构本身也会升级到更大的头部。

Hash、List、Zset这几个类型在小规模数据时使用紧凑编码。Redis 7.x中,小hash使用的是listpack编码,当field数量小于512个且每个field和value长度小于64字节时,所有元素连续存放在一个紧凑结构中,每个entry的额外开销只有几个字节。一旦超过阈值,就会转为hashtable编码,每个field都会独立创建redisObject和SDS,内存开销会明显跳变。这个切换过程在线上很容易被忽略,你只需要往一个hash里多塞几个大字段,内存可能瞬间涨几倍。

Set类型也有类似机制,全部是整数且数量小于512时使用intset编码,每个元素直接存整数,内存效率极高。一旦插入非整数或数量超限,就退化为hashtable编码。

这里最大的启示是:在设计数据结构时,尽量控制单个hash、list、zset的规模在紧凑编码阈值内,这是一个性价比极高的优化点。我后面会给出具体参数和配置。

3.3 缓冲区、复制积压与持久化额外开销

除了数据本身,还有几块容易忽视的内存开销。

第一个是客户端缓冲。Redis默认对普通客户端不限制输出缓冲区大小,这意味着如果有客户端订阅了大量pub/sub消息,或者执行了类似keys *这类返回数据超大的命令,缓冲区会不断膨胀。线上出过一次问题:一个业务用blpop阻塞读list,结果消息生产方短时间写入了大量数据,消费端处理速度跟不上,客户端输出积压到GB级别,直接把实例内存打爆。这个问题的标准解法是设置client-output-buffer-limit,给普通客户端和pub/sub客户端都加上硬限制和软限制。

第二个是复制积压缓冲区。Redis的主从复制有一个固定大小的backlog,默认1MB,用于增量同步。如果从库断线时间较长,backlog太小会触发全量同步,而且这1MB内存是预分配的。在大量写入场景下,可以把repl-backlog-size调大一些来避免频繁全量同步,但同时要意识到这是常驻内存开销。

第三个是持久化期间的额外内存。RDB持久化通过fork子进程实现,子进程和父进程共享内存页,期间如果有写入,会触发写时复制,RSS会临时上涨。AOF重写也是类似机制。这是Redis无法避免的额外内存开销,很多人在监控到fork期间RSS突然升高会紧张,其实这是正常现象,只要峰值没超过物理内存就行。

4. 内存监控与优化实操

4.1 INFO memory命令快速定位内存现状

排查内存问题,我第一步永远是先执行info memory,看十几行关键指标。

这里挑重点解释:

  • used_memory:Redis分配器实际分配出去的内存总量,这是最核心的数据。
  • used_memory_rss:从操作系统角度看,Redis进程占用的物理内存。
  • used_memory_peak:历史峰值,用来判断当前是不是在历史高位。
  • mem_fragmentation_ratio:used_memory_rss / used_memory,碎片率。
  • maxmemory和maxmemory_policy:配置的实例内存上限和淘汰策略。

我举一个典型的异常判断场景。如果used_memory只有2GB,但used_memory_rss是5GB,mem_fragmentation_ratio是2.5,这不是因为数据多,而是碎片严重。反过来,如果used_memory已经到6GB,maxmemory是7GB,used_memory_rss也是7GB,那就是数据真的快要满了,接下来要处理的是淘汰策略或者扩容,不需要考虑碎片问题。

还有一个细节点,Redis 4.0以后,used_memory中区分了overhead和dataset。used_memory_overhead是除了数据之外的开销,包括dictEntry、客户端缓冲区、复制积压等;used_memory_dataset是数据本身。看这两个值可以快速判断大数据量场景下,数据之外的开销占了多大比例。有时候overhead占比超过30%,说明数据建模一定是哪里有问题。

4.2 扫描bigkey的实操方法与健康检查

定位哪个key占内存大,最直接的工具是redis-cli的--bigkeys扫描。它会遍历整个keyspace,统计出每种数据类型中最大的key和占用空间最大的key。用法很简单:

redis-cli --bigkeys

不过这里我要泼一盆冷水:--bigkeys是遍历命令,虽然底层用了scan而不是keys *,不会阻塞服务,但在超大key、海量key的场景下,长时间扫表依然会产生一定的CPU和内存压力。我建议在业务低峰期执行,或者用sample限制采样数量,比如redis-cli --bigkeys -i 0.1,每隔0.1秒扫一批,降低负载。

另外一个适合排查单个key内存开销的命令是memory usage,比如:

127.0.0.1:6379> set user:10086 "zhangsan" OK 127.0.0.1:6379> memory usage user:10086 (integer) 80

对于大集合,还可以指定采样数:memory usage myset sample 100,表示从集合里抽取100个元素做内存估算,避免全量遍历带来的开销。对生产来说,这个命令比--bigkeys更精准,也更安全。

4.3 可落地的内存优化方案

第一,缩短key的设计。key既是业务标识,又是实打实的内存开销。我见过一个系统的key长这样:order:2024:10:23:userId:123456,接近40字符。如果能用更紧凑但可读的编码,比如order:241023:123456,能省接近一半的key内存,在几亿key规模下,这个优化值几GB的容量。

第二,控制集合类型在紧凑编码范围内。Redis默认的阈值是hash-max-listpack-entries 128、hash-max-listpack-value 64,不同版本略有差异。如果你的hash大多数只有几十个field,完全可以保持默认。但如果单hash超过200个field且字段都是小字符串,可以考虑拆分为多个小hash,让每个都保持在listpack范围内。这么做在读取时可能需要多次查询,但对内存的节省非常可观。

第三,用hash代替大量String key。比如用户信息有10个字段,如果每个字段单独存一个String key,光是key的内存开销就占了很大比例。打包成一个hash存储,用user:10086作key,field分别是name、age、email等,整体内存能省一半以上,并且还能用hgetall一次取回。这是最常见的“对象建模优化”,我强烈建议新项目直接这么做。

第四,认真设置过期时间和主动清理。带TTL的key到期后,Redis并不保证立刻删除,有惰性删除和周期性主动删除两种机制。如果某个业务大量写入短生命周期数据,比如验证码、会话,建议设置合理的随机TTL,避免同一秒内大批量过期,造成CPU抖动和内存碎片增多。

5. 内存相关常见问题与排查技巧实录

5.1 OOM与淘汰策略的正确姿势

Redis达到maxmemory并设置为noeviction时,写入命令会报错。错误信息一般是:

(error) OOM command not allowed when used memory > 'maxmemory'

这个报错很多团队见过,但处理方式并不正确。有些人第一反应是调大maxmemory,这只能临时缓解。真正要做的是分三步排查:先看是不是某个业务key异常膨胀,用--bigkeys扫一遍;再看是不是客户端输出缓冲区失控,用info clients看连接数和每个连接的输出内存;最后才考虑淘汰策略是否有问题。

关于淘汰策略的选择,我的建议是:如果Redis只做纯缓存,可以接受数据丢失,用allkeys-lru,保证整体命中率优先;如果Redis存储了部分业务数据,不能随便淘汰,用volatile-lru,只对设置了TTL的key做淘汰;如果数据访问频率差异不大,用allkeys-random。这里要注意noeviction只适合不允许任何丢失的场景,但它意味着写满即拒绝,容量规划必须留足余量。

我个人习惯给maxmemory设置一个缓冲带,比如物理内存8GB,系统其他进程占用1GB,那么Redis的maxmemory设置为6GB而不是7GB。这样即使RSS略高于used_memory,也不会直接触发系统OOM Killer。

5.2 碎片率过高怎么处理

当mem_fragmentation_ratio长期大于1.5,且数据量没有明显增长时,基本可以确认是内存碎片问题。

处理方向有三个,优先用第一个。第一个是开启自动整理。Redis 4.0以后支持activedefrag,设置两个参数:

config set activedefrag yes config set active-defrag-threshold-lower 10 config set active-defrag-threshold-upper 100

注意,activedefrag并不是没有代价的,它会占用CPU时间片,如果CPU已经很紧张,开jemalloc自动整理反而会影响延迟。所以更稳妥的办法是手动找到碎片最严重的时段做一次低峰重启,重启之后所有碎片被回收,RSS会断崖式下降。

第二个办法是从源头控制碎片产生。碎片主要来自频繁创建和删除大小不一的key。如果业务允许,把短字符串统一调整为相近长度,或者用hash代替散碎的String key,都能减少碎片。

第三个办法是升级Redis版本。Redis 4.0之前没有碎片整理能力,4.0之后jemailloc的碎片管理能力也在持续优化。如果还在用3.x,内存碎片问题基本只能靠重启解决。

5.3 内存突然飙升的通用排查流程

我还想把内存突然飙升的排查流程整理一下,这算是我治线上事故的一套标准动作。

第一步看info memory。分别判断used_memory和used_memory_rss哪个涨得快。前者涨得快,说明数据量或者元数据在增长;后者涨得快且used_memory平稳,说明碎片在恶化。

第二步看info clients。很多内存问题的根源是客户端。如果连接数突然从几百涨到几千,每个客户端都分配了输入输出缓冲区,内存涨幅会非常惊人。执行client list可以查看每个连接的输出缓冲区大小,找到可疑的大缓冲区连接及时断掉。

第三步看复制积压和持久化。执行info replication,看repl_backlog是否已经使用到接近上限;执行info persistence,看最近有没有AOF重写或RDB fork在跑。这两个场景产生的内存峰值是瞬时的,过几分钟会回落,需要确认是不是正好撞上。

第四步扫数据。用memory usage加sample来验证怀疑对象。我之前排过一个最严重的案例,一张业务表被误操作写入了一个包含千万级别元素的zset,总量只有200MB的实例直接涨到了1.8GB。任何写法、任何操作,一旦判断是可疑点,先用memory usage验证,比拍脑袋猜靠谱得多。

6. 关于内存模型,最后唠叨几句

在做Redis容量规划时,我会提醒自己多留出至少30%的buffer,不只是给数据增长,更是给碎片、缓冲区和fork瞬间留出余地。很多线上事故不是数据量算错了,而是忽略了这些“看不见的内存”。

最后分享一个我觉得特别实用的小技巧:不要只在出问题时才看info memory,建议所有Redis实例接入监控,把used_memory、used_memory_rss、mem_fragmentation_ratio三个指标按分钟采集,保留30天。这样问题发生时能回看趋势,判断内存是缓慢爬坡还是瞬时暴涨。绝大多数Redis内存事故,在监控图上都会有预兆。你有这个历史数据,排查思路会清晰非常多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询