一、面试官为什么总爱问这个问题
很多候选人觉得 Redis 就是一个「快一点的缓存」,面试时背几个淘汰策略就能过关。但是在天猫这样的业务体量下,一次 Redis 内存耗尽造成的故障,可能会直接打挂首页、搜索、详情页、购物车和交易链路。面试官真正想考察的不是你有没有背过 LRU 和 LFU,而是你能不能把「内存耗尽」这件事,从配置、数据结构、写入路径、持久化、主从复制到集群,完整地串成一条故障推演链路。
这个问题之所以经典,是因为它同时考察了三个层次的能力:
- 基础层:是否理解 maxmemory、OOM 拒绝、淘汰策略这些基本概念。
- 机制层:是否清楚内存耗尽后,读命令、写命令、持久化、主从同步分别会发生什么。
- 工程层:是否知道在生产环境中如何提前监控、如何兜底、如何回答得让面试官觉得你「真的处理过线上问题」。
本文会以天猫二面的视角,把这一问题拆开揉碎,从 Redis 的内存构成讲起,到内存耗尽瞬间的行为变化,再到持久化、主从复制、集群场景的连锁反应,最后给出可以直接用于面试的答题框架和线上实战排查思路。
二、先搞明白:Redis 的内存都花在哪里了
在讨论「内存耗尽」之前,必须先搞清楚 Redis 的内存到底是由哪些部分组成的。如果连内存账单都算不清楚,后面讨论淘汰策略和故障排查就会变得非常模糊。Redis 占用的内存主要由以下几部分组成:
2.1 数据本身占用的内存
这是最重要的部分,也就是存储的键值对本身。Redis 的键和值都是对象,每个对象在底层都由redisObject结构体包装。一个 redisObject 在 64 位系统上约占 16 字节,其中包含类型、编码方式、LRU 时钟、引用计数和指向实际数据的指针。
一个字符串键值对的内存构成: + 键本身:redisObject + SDS 结构 + 字符串内容 + 值本身:redisObject + SDS 结构 + 字符串内容 + dictEntry:哈希表节点,约 24 字节也就是说,一个很小的 key,即便值只有几个字节,Redis 实际占用的内存也会因为各种结构体开销而膨胀。这也是为什么在大量小 key 的场景下,Redis 的内存使用率会显得特别「虚高」。
2.2 进程自身运行占用的内存
Redis 主进程本身在启动和运行时需要占用一部分内存,包括代码段、数据段、栈和堆的初始开销。这部分内存通常在几 MB 到十几 MB 的量级,对于大内存实例来说占比很小,但在讨论极限内存时需要纳入考虑。
2.3 缓冲区和复制积压缓冲区
Redis 在处理客户端请求时,会为客户端分配输入缓冲区和输出缓冲区。正常情况下这些缓冲区很小,但如果客户端大量写入、或者从节点长时间同步慢,输出缓冲区可能会膨胀。此外,主节点维护的复制积压缓冲区repl-backlog也会占用固定内存,默认 1MB,用于部分重同步。
2.4 内存碎片
Redis 使用jemalloc作为默认的内存分配器。频繁的键值增删会产生内存碎片,导致「实际分配的内存」大于「逻辑上使用的内存」。衡量碎片率的指标是mem_fragmentation_ratio,等于used_memory_rss / used_memory。正常情况下这个比值在 1 到 1.5 之间,如果远超 1.5,说明碎片严重。
2.5 AOF 重写和 RDB 子进程的内存
Redis 进行持久化时,会 fork 子进程。借助操作系统的写时复制机制,子进程理论上共享父进程的内存页。但如果主进程在持久化期间继续大量写入,就会触发页复制,导致实际内存翻倍甚至更多。这是内存耗尽的一个非常隐蔽的诱因,后面会详细展开。
2.6 一张表看清内存构成
| 内存组成部分 | 说明 | 受什么控制 |
|---|---|---|
| 键值数据 | redisObject、SDS、底层数据结构 | 业务写入、淘汰策略 |
| 客户端缓冲区 | 输入/输出缓冲区 | 客户端行为、配置 |
| 复制积压缓冲区 | 主节点用于部分重同步 | repl-backlog-size |
| 内存碎片 | jemalloc 分配产生的碎片 | 写入模式、内存分配器 |
| 持久化开销 | fork 子进程的写时复制 | 写入量、持久化频率 |
| Lua 脚本缓存 | 脚本运行时的临时数据 | 业务脚本 |
三、inforedismemory:先学会看内存账单
在讲内存耗尽之前,面试官常常会追问一句:「你怎么判断 Redis 是不是要撑爆了?」这时候必须能熟练解读INFO memory的输出。下面是一份典型的内存信息,其中每个字段都对应前面讲的内存构成。
redis> INFO memory # Memory used_memory:926640 used_memory_human:904.92K used_memory_rss:2965504 used_memory_rss_human:2.83M used_memory_peak:1051200 used_memory_peak_human:1.01M used_memory_peak_perc:88.20% used_memory_overhead:915904 used_memory_startup:916896 used_memory_dataset:9736 used_memory_dataset_perc:1.05% allocator_allocated:946784 allocator_active:981216 allocator_resident:917504 total_system_memory:16799477760 total_system_memory_human:15.65G used_memory_lua:37888 used_memory_vm_eval:37888 used_memory_scripts_eval:0 number_of_cached_scripts:0 number_of_functions:0 number_of_libraries:0 used_memory_vm_functions:32768 used_memory_vm_total:70656 used_memory_vm_total_human:69.00K used_memory_functions:184 used_memory_scripts:184 used_memory_holders:70656 used_memory_scripts_holders:184 used_memory_functions_holders:184 mem_fragmentation_ratio:3.21 mem_fragmentation_bytes:2035352 mem_not_counted_for_evict:0 mem_replication_backlog:0 mem_total_replication_buffers:0 mem_clients_slaves:0 mem_clients_normal:0 mem_cluster_links:0 mem_aof_buffer:0 mem_allocator:jemalloc-5.3.0重点关注的指标如下:
- used_memory:Redis 分配器实际分配的内存,也就是逻辑上已使用的内存。
- used_memory_rss:操作系统视角下 Redis 进程占用的物理内存,包含碎片。
- used_memory_peak:历史内存峰值,用于判断内存是否持续增长。
- mem_fragmentation_ratio:碎片率,rss 与 used_memory 的比值,是内存健康度的关键指标。
- mem_not_counted_for_evict:即便配置了 maxmemory,这部分内存也不参与淘汰统计,例如客户端缓冲区等。
需要特别注意上面的例子中mem_fragmentation_ratio高达 3.21,说明used_memory_rss是used_memory的三倍多,进程实际占用远大于逻辑数据规模,这是典型的内存碎片问题。
四、maxmemory:内存的上限是谁规定的
Redis 本身并没有默认的内存上限。如果不设置maxmemory,在 64 位系统上 Redis 会一直使用内存,直到把服务器的物理内存耗尽。对于生产环境来说,这是一个非常危险的默认行为,尤其是在主机上还部署着其他服务的时候。
4.1 maxmemory 的配置方式
# redis.conf 中设置最大内存为 4GB maxmemory 4gb 也可以在运行时动态调整 redis> CONFIG SET maxmemory 4gb redis> CONFIG GET maxmemory "maxmemory" "4294967296"当设置了maxmemory之后,Redis 会在写入路径中检查是否超过了这个阈值,并根据淘汰策略决定下一步动作。
4.2 不设置 maxmemory 的后果
如果 maxmemory 为 0,Redis 在 32 位系统上最多只能使用 3GB 内存,这是历史遗留限制;在 64 位系统上则没有限制。很多线上事故的根源就是没有设置 maxmemory,Redis 一路吃满内存,最终触发操作系统的 OOM Killer,把 Redis 进程直接杀掉,而不是优雅地拒绝写入。
这里要重点区分两种「内存耗尽」:
- Redis 主动发现超过 maxmemory:Redis 会按照淘汰策略处理,相对可控。
- 操作系统内存真正被耗光:可能触发 OOM Killer 或 swap,Redis 被动挨打,后果严重得多。
面试中主动区分这两点,会是一个很好的加分项。
五、内存淘汰策略:八种策略一次讲透
当 Redis 的内存占用逼近或超过 maxmemory 时,最关键的问题就是:哪些 key 可以被淘汰?这就是内存淘汰策略的作用。Redis 4.0 之后提供了八种淘汰策略,分为三大类:不淘汰、针对全体 key 的淘汰、针对设置了过期时间的 key 的淘汰。
5.1 noeviction:拒绝写入,不淘汰
这是 Redis 的默认策略。当内存达到 maxmemory 时,Redis 不会淘汰任何数据,而是直接拒绝所有可能导致内存增加的写命令,返回 OOM 错误。读命令仍然可以正常执行。
redis> SET key value (error) OOM command not allowed when used memory > 'maxmemory'.这个策略适用于不能丢失任何数据的场景,例如把 Redis 当作持久化数据库使用,或者存储的是核心交易数据。但代价是内存耗尽时写入全部失败。
5.2 针对所有 key 的淘汰策略
这类策略会在所有 key 中挑选淘汰对象,不管 key 是否设置了过期时间。
- allkeys-lru:在所有 key 中,淘汰最近最少使用的 key。这是最常用的策略,适合热点数据访问有明确区分度的场景。
- allkeys-lfu:在所有 key 中,淘汰访问频率最低的 key。适合冷热数据分布明显、访问频率差异大的场景。
- allkeys-random:在所有 key 中随机淘汰。适合 key 的访问模式完全随机、无法预测热点的场景。
5.3 针对设置了过期时间的 key 的淘汰策略
- volatile-lru:只在设置了过期时间的 key 中淘汰最近最少使用的 key。
- volatile-lfu:只在设置了过期时间的 key 中淘汰访问频率最低的 key。
- volatile-random:只在设置了过期时间的 key 中随机淘汰。
- volatile-ttl:优先淘汰剩余过期时间最短的 key,即越接近过期的 key 越先被淘汰。
5.4 需要注意的坑
使用 volatile 系列策略有一个前提:如果内存耗尽时,Redis 中所有 key 都没有设置过期时间,或者设置了过期时间的 key 已经被淘汰完了,那么 Redis 的行为会退化成 noeviction,直接拒绝写入并返回 OOM。这一点很多候选人会忽略,面试时如果能主动点出来会显得思考更完整。
另外,淘汰策略虽然叫 allkeys,但它不会淘汰那些不计入淘汰统计的内存占用,也就是mem_not_counted_for_evict中的部分,典型如客户端缓冲区、复制缓冲区等。这意味着即使淘汰了所有可淘汰的数据,仍有可能因为缓冲区膨胀而超过 maxmemory。
六、内存耗尽瞬间:Redis 到底会发生什么
现在来到本文的核心问题:当内存真的耗尽,也就是 used_memory 达到或超过 maxmemory 之后,Redis 会发生什么?答案是:行为不是单一的,而是取决于正在执行的是什么操作。不能笼统地说「Redis 会拒绝写入」或「Redis 会淘汰数据」,而要分命令类型、分场景来看。
6.1 写命令:被拒绝或被淘汰后执行
对于会新增内存占用的写命令,例如SET、LPUSH、SADD、ZADD等,Redis 在真正执行命令之前,会先调用performEvictions函数检查内存是否超限。如果超限:
- 若淘汰策略是 noeviction,直接返回 OOM 错误,命令不执行。
- 若淘汰策略允许淘汰,Redis 会尝试淘汰一批 key,腾出内存后再执行命令。
- 如果淘汰完成后内存仍然不够,最终还是会返回 OOM 错误。
127.0.0.1:6379> SET name zhangsan (error) OOM command not allowed when used memory > 'maxmemory'.6.2 读命令:大多数情况下可以正常执行
内存耗尽主要影响的是写入路径。对于GET、HGET、LRANGE等读命令,内存耗尽时通常仍然可以正常返回结果,因为读命令不会新增内存占用。当然也有例外:某些会改变 key 的访问统计的命令,例如使用 LFU 策略时,读命令会更新访问频率计数,但这不会导致额外的显著内存增长。
6.3 更新已有 key 的值:可能写入也可能 OOM
如果对一个已经存在的 key 执行SET,新值比旧值更大,就会产生新的内存占用,可能触发淘汰或 OOM。如果新值比旧值小,实际上释放了内存,通常可以正常执行。这取决于写入后内存是否仍然超限。
6.4 删除命令:不仅可以执行,还能缓解内存压力
DEL、UNLINK等删除命令在内存耗尽时依然可以执行,而且恰恰是缓解内存压力的手段。删除 key 后会释放内存,使 used_memory 降低到 maxmemory 以下。这也是为什么线上遇到内存告警时,第一步往往是快速定位大 key 并删除。
6.5 事务和 Lua 脚本:可能存在特殊行为
内存耗尽发生在事务或 Lua 脚本执行过程中时,情况会更复杂。Redis 的单线程特性保证了原子性,但在写入检查上,Lua 脚本内部的写命令同样会受到 maxmemory 限制。如果脚本执行到一半内存超限,可能会报错。这里不展开太深,但面试时如果能提到「脚本执行期间的 OOM 可能导致脚本中断」,会体现对 Redis 执行模型的深入理解。
七、深入理解 LRU 和 LFU:淘汰的底层逻辑
面试官在确认你知道八种策略之后,通常会继续追问:「LRU 和 LFU 有什么区别?Redis 的 LRU 是精确的吗?」这一节把这部分讲透。
7.1 传统 LRU 的问题
传统的 LRU 是基于双向链表实现的,每次访问都把节点移动到链表头部,淘汰时从链表尾部移除。这种实现需要维护链表结构,内存开销大,而且在高并发访问下链表的更新会成为性能瓶颈。
7.2 Redis 的近似 LRU
Redis 没有采用精确的 LRU,而是实现了一种近似算法。每个 redisObject 中保存了一个 24 位的 LRU 时钟,记录该对象最后一次被访问的时间戳。当需要淘汰时,Redis 会随机采样 N 个 key,从中选择最久未被访问的那个淘汰。采样数量由maxmemory-samples配置,默认是 5。采样数量越大,淘汰结果越接近精确 LRU,但性能开销也越大;采样数量越小,误差越大,但速度更快。Redis 通过这种方式在准确性和性能之间做了折中。
7.3 Redis 的 LFU:不只是看最近,更看频率
Redis 4.0 开始支持 LFU。LFU 的核心思想是:一个 key 被访问的次数越多,说明它越热,越不应该被淘汰;反之,长期不被访问的冷 key 应该优先被淘汰。Redis 没有为 LFU 单独分配大块内存,而是复用了 redisObject 中原本用于 LRU 时钟的 24 位字段。
在 LFU 模式下,这 24 位被拆成两部分:高 16 位记录访问时间衰减后的「逻辑时间量」,低 8 位记录「对数访问频率」。每次访问 key 时,计数器会按照对数方式递增,也就是访问次数越多,计数增长越慢,从而避免热门 key 的计数溢出;同时随着时间推移,计数会逐步衰减,让长期不再被访问的历史热点有机会被淘汰。
这也解释了为什么 LFU 比 LRU 更适合「冷热数据分布明显」的场景:LRU 只看最后一次访问时间,如果一个历史高频 key 很久没访问,它依然可能因为「最近一次访问较晚」而被保留;而 LFU 会把访问频率低、热度持续下降的 key 更快识别出来。
7.4 LRU 和 LFU 到底怎么选
- allkeys-lru:大多数缓存场景的默认选择。它实现简单、开销低,对时间局部性明显的业务效果很好。
- allkeys-lfu:当业务数据有明显冷热分层,且希望长期保留高频热点、稳定淘汰低频冷数据时,可以用 LFU。
- volatile 系列:只有当缓存 key 都设置了过期时间,且希望把永久 key 保留在内存中时才考虑。
一个实用的判断方法是:先看业务数据是否区分明显的「热点」和「长尾」。如果只是普通缓存,优先 allkeys-lru;如果能明确识别出少部分 key 贡献了大部分访问量,可以尝试 allkeys-lfu,并通过压测对比命中率。
八、内存耗尽后,持久化为什么也容易翻车
很多候选人会把「内存耗尽」和「持久化」当成两件事来讲,但在真实线上故障里,它们经常是互相放大的。内存已经接近 maxmemory 的实例,再叠加一次 RDB 或 AOF 重写,很可能直接把进程推进 OOM。
8.1 RDB 持久化:fork 子进程带来的内存放大
RDB 快照通过 fork 子进程完成。fork 本身很快,但 fork 之后父子进程会基于操作系统的写时复制机制共享内存页。如果父进程在主进程继续处理大量写入,这些写入会触发内存页复制,导致进程实际占用内存上升。极端情况下,一个已经使用 4GB 内存的实例,在 RDB 生成期间如果写入非常频繁,RSS 可能接近 8GB 甚至更多。
这正是内存耗尽场景中最隐蔽的「最后一根稻草」:Redis 逻辑内存还没超过 maxmemory,但 fork 导致的写时复制先让物理内存耗尽,最终被操作系统 OOM Killer 杀掉。
8.2 AOF 重写:缓冲区和父子进程同时吃内存
AOF 重写同样需要 fork 子进程,因此也有写时复制带来的内存放大问题。除此之外,AOF 重写期间主进程新产生的写命令还会写入「AOF 重写缓冲区」,这段时间的写入量越大,缓冲区占用越高。如果重写时间很长,或者写入 QPS 很高,这部分内存也会持续增长。
8.3 内存吃紧时,更要注意持久化策略
- 避免把
maxmemory设置得接近物理机内存上限,要预留 fork 写时复制的余量。 - 生产环境建议给 Redis 实例预留至少 20% 到 30% 的额外内存,尤其是开启了 RDB 或 AOF 的高写入场景。
- AOF 重写和 RDB 尽量错峰,避免短时间内多次 fork 造成连续内存抖动。
九、主从复制场景下的内存压力
主从复制并不会自动解决内存耗尽问题,反而可能带来新的内存风险。面试时如果能从「主节点」和「从节点」两个视角分别展开,会显得更完整。
9.1 主节点:复制缓冲区可能被慢从库拖垮
主节点为每个从节点维护输出缓冲区,用来存放待同步的命令。如果某个从节点网络较慢或者负载较高,消费命令的速度跟不上主节点写入速度,缓冲区就会不断堆积。Redis 通过client-output-buffer-limit replica限制复制客户端的输出缓冲区大小,超过限制后主节点会断开该从节点的复制连接。
如果这个限制配置得过松,缓冲区就可能占用大量内存,甚至把主节点内存撑爆。需要注意的是,这部分内存通常不计入淘汰统计,即使主节点把所有可淘汰 key 都淘汰掉,也无法缓解复制缓冲区造成的 OOM。
9.2 从节点:不要忘记它也有自己的 maxmemory
从节点同样会执行写入命令,因此也会消耗内存。如果从节点内存耗尽,它也一样会触发淘汰或拒绝写入。一个常见的误解是「从节点只是备份数据,可以随便配小内存」,但实际上从节点在同步大量写命令时,同样面临内存压力。
此外,从节点在初次进行全量同步时,会接收主节点生成的 RDB 并加载到本地。如果从节点内存不足,加载过程也可能失败,导致同步反复重试。
9.3 全量同步和部分重同步
当主从连接中断后重新建立时,Redis 优先尝试部分重同步,通过repl-backlog积压缓冲区补齐断连期间的命令。如果断连时间过长,积压缓冲区已经不再包含所需的命令,就会退化为全量同步。全量同步不仅网络开销大,还会引发前面提到的 fork 和 RDB 内存放大问题。
十、Redis Cluster 集群下的内存耗尽
Redis Cluster 把数据分散到多个节点,但这并不意味着单个节点就不会内存耗尽。恰恰相反,如果数据分布不均匀,某个热点槽位所在的节点可能比其他节点更早触发 OOM。
10.1 单节点 OOM,不代表集群整体不可用
在 Redis Cluster 中,每个节点都有自己的 maxmemory 和淘汰策略。当某个主节点达到内存上限并拒绝写入时,访问该节点负责槽位的写请求会失败,但其他节点仍然可以正常服务。问题在于,如果业务对一致性要求较高,局部写入失败同样会造成数据不一致。
10.2 槽位迁移时的内存变化
集群扩容或缩容时,需要进行槽位迁移。迁移过程中,目标节点会同时接收迁移数据和客户端写入,内存占用可能在短时间内显著上升。如果目标节点内存余量不足,迁移过程中更容易触发 OOM。因此集群变更操作前,通常需要先检查各节点内存水位。
10.3 集群监控不能只看平均值
很多监控大盘只展示集群整体内存使用率,这会掩盖单节点热点问题。比如整体使用率只有 60%,但某个大 key 所在节点已经 95%。面试时可以强调:集群场景下要逐节点监控used_memory、used_memory_peak和mem_fragmentation_ratio。
十一、线上内存告警,应该怎么排查和处理
面试聊完原理之后,面试官往往还会问:「如果现在线上 Redis 内存告警了,你作为负责人会怎么做?」这一节给出可以直接搬到工作里的排查思路。
11.1 先止血,再定位
- 判断告警级别:通过 used_memory、used_memory_peak_perc、mem_fragmentation_ratio 快速确认内存水位。
- 快速扩容:如果是集群节点,优先做水平扩容或调整 maxmemory,争取恢复写入。
- 及时删除大 key:如果已经定位到明显的大 key,评估业务影响后尽快删除或裁剪。
- 临时调整淘汰策略:如果业务能接受缓存丢失,可以临时切换到 allkeys-lru 或 allkeys-lfu,让 Redis 自己腾出内存。
11.2 定位大 key 和内存大头
redis-cli --bigkeys MEMORY USAGE key redis-cli -a password --rdb dump.rdb其中redis-cli --bigkeys只能看出 key 的数据规模,不能完全等同于内存占用;对于复杂数据结构,可以用MEMORY USAGE key精确分析。更深入的排查还需要结合 RDB 分析工具,找到占用内存最多的 key 和数据类型。
11.3 建立常态化监控
- 监控
used_memory和used_memory_rss的差值,及时发现内存碎片。 - 对
used_memory_peak_perc设置阈值告警,避免每次只看到实时值而错过峰值。 - 关注
mem_fragmentation_ratio,明显超过 1.5 时结合activedefrag或重启评估处理方式。 - 持续统计大 key,避免单个大 key 拖垮整个实例。
十二、面试答题框架:这样回答既完整又有亮点
最后回到面试场景。面对「内存耗尽后 Redis 会发生什么」这个问题,可以先给出一句结论,再分场景展开,最后落到工程实践,形成一条完整的回答链路。
12.1 推荐答题结构
- 先明确前提:是否设置了 maxmemory,以及配置的是哪种淘汰策略。
- 区分两种耗尽:Redis 自己发现超限,还是操作系统真的把内存耗光。
- 分命令说明:写命令可能 OOM 或淘汰后执行;读命令通常不受影响;删除命令可以正常执行。
- 补充连锁反应:持久化 fork 放大内存、主从复制缓冲区堆积、集群节点热点。
- 落到工程实践:监控指标、大 key 排查、容量评估和兜底方案。
12.2 一句话总结
内存耗尽后,Redis 不会简单地「拒绝一切」,而是在 maxmemory 和淘汰策略的约束下,对写命令进行拦截或淘汰腾挪;同时,持久化、主从复制和集群还会带来额外的内存放大风险。真正要在面试里脱颖而出,就要把「命令行为 + 底层机制 + 故障连锁 + 线上治理」串成一条完整链路。
十三、总结
回到最初的问题:内存耗尽后 Redis 会发生什么?答案不是一个简单的「报 OOM」,而是一整套受配置、命令类型、持久化、复制和集群共同影响的行为集合。
理解这条链路,需要先能读懂 Redis 的内存构成和INFO memory,再掌握 maxmemory 与八种淘汰策略,最后把内存耗尽放到真实架构中推演:写命令被拦截或淘汰、持久化 fork 放大内存、主从复制缓冲区堆积、集群节点热点。掌握这些之后,无论面试官从哪个角度追问,都能把问题接得住、答得深。