1. 先从面试题说起这两件事为什么总被放在一起问Redis 的过期删除和内存淘汰是面试里出现频率相当高的一组问题。不少同学背过八股文能说出“惰性删除”“定期删除”“LRU”这些词但一旦被追问“过期键到底什么时候被删”“Redis 为什么不用定时删除”“淘汰策略里 volatile-lru 和 allkeys-lru 区别在哪”就卡住了。实际上这两个机制解决的是完全不同的两类问题过期删除管的是“到了过期时间的 key 怎么处理”内存淘汰管的是“内存满了以后怎么腾空间”。前者是时间维度上的清理后者是容量维度上的取舍。面试官把两者放在一起问本质上是在考察你对 Redis 内存管理和数据生命周期的整体理解而不只是背两个名词。我多年前带过一个刚入行的同学简历上写着“熟悉 Redis”第一次模拟面试时我把这两个概念放在同一个问题里问他立刻就把“过期删除”和“内存淘汰”混为一谈说“内存满了就把过期的删除掉”。这就是典型的只背答案、没理解机制。这类问题面试官只要换个角度问就露馅。所以这篇文章我会把两条链路拆开讲再把它们交汇的地方讲清楚看完之后你不仅能答上来还能给出让面试官眼前一亮的延伸答案。2. 过期删除Redis 是怎么让“到期的 key”消失的2.1 被动删除只有被访问到才想起来清理Redis 对过期 key 的第一层处理叫惰性删除。当客户端尝试访问一个 key 时Redis 会先检查这个 key 是否设置了过期时间如果已经过期就直接返回 nil同时把这个 key 从内存里删掉。注意这里有个容易被忽略的细节对于 String 类型的 key直接读的时候会触发检查对于 Hash、Set、List 这类容器类型过期检查是在整个 key 级别做的而不是在元素级别。也就是说一个 Hash key 过期之后你在上面执行 hget、hgetall、hlen 都会先触发 key 的过期检查返回空结果而不是说某些 field 还在。这种策略的好处是省 CPU不浪费任何一次扫描去处理没人访问的 key坏处也显而易见如果一个 key 过期后一直没人访问它就会一直占着内存不释放。举个极端例子一个几 GB 的缓存 key 设置了 1 分钟过期但之后没有任何请求再碰到它那这块内存就一直是“僵尸状态”。所以惰性删除必须搭配第二层机制来兜底不能靠它单打独斗。注意惰性删除不只是读操作触发写操作同样会触发。比如对一个已过期的 key 执行 setRedis 会先删掉旧 key 再写入新值。这一点在面试里可以主动提出来能体现你对源码行为的熟悉。2.2 主动删除定时抽样的“碎片化治理”第二层机制叫定期删除。Redis 的每个数据库实例默认是 16 个里都维护着一个持有过期时间的 key 集合内部结构是 dict。Redis 后台会周期性地对这些 key 做抽样检查默认情况下每秒执行 10 次每次抽查一批 key把所有已经过期的 key 删掉。具体的执行逻辑是从过期 key 集合里随机取出一批默认 20 个遍历删除其中已经过期的如果本轮删除的比例超过四分之一说明过期 key 密度还很高就继续重复这一轮直到删除比例降到四分之一以下或者本次执行总时长超过了某个时间预算。这里为什么用“抽样”而不是“全量扫描”道理很简单如果一个实例里存了几千万个带过期时间的 key全量扫一遍会阻塞主线程对线上服务是灾难性的。抽样 比例控制就是典型的“用概率换性能”做法让 Redis 在清理垃圾的同时不至于把自己卡死。这里的“时间预算”也很关键。定期删除不是一次性把所有过期 key 删光而是每次只干一小段“碎片化”的活。比如之前提到的默认 10 次/秒的执行频率它内部把 1 秒拆成 10 份每次执行最多占用一部分时间片宁可这轮没删完也不能影响主线程的正常请求处理。2.3 一个容易被问翻车的点持久化时过期 key 如何处理很多人在聊完惰性删除和定期删除之后就觉得自己过关了结果面试官追加一句“那 RDB 持久化和 AOF 持久化的时候过期 key 怎么处理”这里我给你一个极其清晰的回答框架生成 RDB 文件时已经过期的 key 不会写入 RDB 文件。也就是说持久化快照天然就把过期数据过滤掉了。从 RDB 文件加载数据时即使文件中存在过期 key比如文件是旧版本生成的某些 key 在写文件之后才过期Redis 也不会把它们加载进内存加载时会再检查一次过期时间。AOF 文件的情况稍微复杂。如果用的是 AOF 追加写模式Redis 在某个 key 被惰性删除或定期删除之后会额外记录一条 DEL 命令到 AOF 文件里确保 AOF 重放时也不会残留这个 key。当开启 AOF 重写时Redis 会扫描当前数据已经过期的 key 不会被写进重写后的 AOF 文件。这套回答展示的不只是“知识点记忆”而是你对 Redis 数据落盘链路里“时间戳”和“数据有效性”的处理逻辑有整体认识面试官对你的评价会明显不一样。2.4 主从复制模式下过期删除是怎么传的这个问题再追问一层就到了主从。Redis 的主从节点之间不会把“逻辑删除”这个动作传成“这个 key 还剩多长时间过期”这种消息而是由主节点负责实际触发过期删除然后向从节点发送 DEL 命令。这里有一个容易被误解的关键点从节点遇到客户端读取一个已过期的 key 时在未收到主节点 DEL 命令之前它依然会返回这个 key 的值。从节点不应该自己执行过期删除来阻止读请求因为主从之间靠复制流对齐数据状态从节点的数据必须严格跟随主节点。也就是说从节点的过期 key 只有在主节点真正删除了“事实数据”并同步 DEL 命令之后才会从从节点的内存里消失。这个机制保证了主从数据的一致性但也带来一个实际影响如果主节点长时间不访问某个过期 key它就一直存在从节点上也会一直能看到该过期数据。因此要求“绝对一致的过期时间”的业务场景不能依赖 Redis 过期机制需要应用层做校验。3. 内存淘汰内存不够了谁该“牺牲”掉3.1 先搞清楚 maxmemory 这个“天花板”内存淘汰的前置条件是 Redis 配置了内存上限maxmemory。没有设置这个值64 位系统下默认是 0表示不限制内存使用Redis 就会一直往上吃内存直到操作系统把它 OOM 掉。生产环境一般都会设置比如maxmemory 4gb代表最多使用 4GB。一旦内存达到这个上限新写入操作就会触发内存淘汰策略Redis 会按照你配置的策略删除某些 key腾出空间来接受新数据。注意这里的“写入操作”包含 set、lpush、sadd、zadd 等而读操作不会触发淘汰。因为读操作不会让内存继续增长不需要腾空间。另一个容易忽略的点内存淘汰并不是“把所有 key 一次性清理到低于 maxmemory”而是“在每次写入时检查如果当前内存超过上限就执行淘汰直到内存降到上限以下再继续执行当前写入”。所以实际运行中内存会围绕 maxmemory 值在一个小范围内波动而不是一条严格水平线。3.2 八种淘汰策略一张表看懂Redis 的maxmemory-policy配置项一共支持 8 种策略名字相近、容易搞混我用一个表格帮你在几分钟内建立清晰的区分框架。策略作用范围淘汰依据使用场景noeviction全部 key不淘汰写入直接报错只读缓存、不允许丢数据的场景allkeys-lru全部 keyLRU最近最少使用缓存业务通用默认项allkeys-lfu全部 keyLFU最不经常使用访问热度差异明显的缓存场景allkeys-random全部 key随机淘汰访问均匀、无热点概念的场景volatile-lru仅限设置了过期时间的 keyLRU只淘汰“本身就想让它消失”的 keyvolatile-lfu仅限设置了过期时间的 keyLFU带过期时间的缓存且访问差异明显volatile-random仅限设置了过期时间的 key随机淘汰有过期时间的 key 且无明显热点volatile-ttl仅限设置了过期时间的 key剩余 TTL 最短优先淘汰快过期的 key面试时你不需要把 8 个策略背得一字不差更重要的是讲清楚两组关键区别第一组是allkeys 和 volatile 的区别前者在所有 key 里挑牺牲品后者只在设置过过期时间的那部分 key 里挑第二组是lru / lfu / random / ttl 的区别即到底凭什么决定“谁先死”。把这两条线串起来策略表就变成了一张可以推导的规则表。3.3 近似 LRURedis 为什么不用真正的 LRU纯正的 LRULeast Recently Used算法要求在每次访问时记录 key 的访问时间内存淘汰时找出最久没被访问的那个。理论上这需要维护一个按访问时间排序的链表每次访问一个 key 就要把它移动到链表头部。如果 Redis 真的这样做每次 get / set 除了操作内存数据以外还要维护链表节点位置主线程额外开销会非常大高性能打折扣。Redis 的做法是近似 LRU它不为每个 key 精确维护全局访问顺序而是在需要进行淘汰时从配置的所有 key 空间中随机抽取若干个 key默认采样 5 个由maxmemory-samples控制比较它们各自的空闲时间idle time也就是“距离上次访问过去了多久”然后淘汰空闲时间最长的那一个。这个过程的概率意义是sample 数越大淘汰结果越接近真正的 LRU但付出的 CPU 成本也越高。默认 5 个采样是官方在精度和性能之间取的平衡点。这也解释了为什么 Redis 官方文档说如果内存紧张、抽样样本太小近似 LRU 有可能淘汰掉一个最近刚被访问过的 key只是概率较低。面试场景下能说出“Redis 用的是近似 LRU不是精确 LRU靠随机采样 空闲时间比较来近似全局最久未访问”这句话就说明你是真的理解了这个设计而不是只背了算法名。3.4 LFU 策略解决“历史热点”问题Redis 4.0 引入了 LFULeast Frequently Used淘汰策略这是对 LRU 一个重要缺陷的修正。LRU 只看“最近访问时间”不看“访问频率”所以一个 key 在过去被高频访问、现在是热点衰减后的缓存只要最近被偶尔访问过一次空闲时间很短就很容易在淘汰时“幸存”下来。但对业务来说它早就不是高价值数据了。LFU 给每个 key 维护一个访问频次计数器淘汰时优先淘汰访问次数少的。Redis 的实现里这个计数器不是简单的整数累加它采用了“衰减机制”计数会随着时间推移按一定概率降低防止一个 key 在很久以前被疯狂访问、之后一直不访问却因为计数器数值很高而永远不被淘汰。用生活化的例子来说LFU 像一个“关注热度排行榜”一个话题即使昨天是热搜第一今天没讨论度了排名也会掉下去而不是永远霸榜。在面试里主动提到 LFU 以及它和 LRU 的适用差异属于加分项。面试官会认为你不是只会背默认配置而是真的用过不同场景的数据特征去选策略。3.5 淘汰出的空间去哪了为什么说 Redis 的“缓存”本身就是有损服务很多初学者对淘汰有一个错误预期既然配了 maxmemory内存就一定能精确稳定在上限附近被淘汰的 key 空间也立刻就能复用。实际中你会发现 Redis 淘汰 key 是有代价的每次淘汰至少释放一个 key 的空间但如果淘汰的是大 key释放比较顺利如果淘汰的全是几十字节的小 key那么每释放出几 MB 空间就要淘汰大量 key每一次淘汰都要消耗主线程的 CPU 和额外的内存管理操作。另外淘汰完成后 Redis 并不会把内存归还给操作系统而是放进自己内部的内存分配器继续复用。这些细节解释了为什么生产上内存告警时不应该只盯 maxmemory 数值还要看INFO memory输出里的 used_memory、used_memory_rss、mem_fragmentation_ratio 等指标。面试官如果在场景题里问“为什么内存涨了但淘汰好像没生效”你如果能答到“淘汰触发依赖写入动作、而不是实时监控内存水位”这一层就是碾压级别的回答。4. 面试现场从原理到追问的完整应对框架4.1 基础问题说说 Redis 的过期删除策略回答这类问题我建议用“两条链路 一个兜底”的结构。先说惰性删除强调“访问时检查过期即删节省 CPU 但不主动清理”再说定期删除强调“随机抽样 时间预算 删除比例控制兼顾清理效率与主线程性能”最后点一个兜底——持久化和主从复制场景下是如何保持数据一致的。这个结构的好处是逻辑层层递进从内存到磁盘再到多实例让面试官感受到你的完整知识面。4.2 进阶问题为什么 Redis 不用定时删除面试官问“为什么不用定时删除”其实是在考察你对“单线程模型”的理解。如果 Redis 为每个 key 都设置一个定时器到了过期时间立刻删除CPU 会被大量定时任务打断而且 key 数量巨大时维护定时器的内存开销也高得离谱。Redis 是单线程处理命令的任何阻塞操作都会拖慢所有客户端所以它选择了“以访问触发的惰性删除为主以时间采样的定期删除为辅”的组合方案。这个组合本质上是 CPU 和内存之间的交易用可能的内存浪费换取 CPU 的高效。4.3 场景问题线上缓存命中率低怎么排查这种题没有标准答案考的是你做没做过真实运维。我会这样回答先看INFO stats里的keyspace_hits和keyspace_misses计算命中率再用INFO memory看内存水位和淘汰次数再看maxmemory-policy是不是配置合理。如果发现淘汰次数很高但命中率低可能是 maxmemory 设置太小也可能是淘汰策略不合适——比如全是热点数据却配了 random 策略冤枉了“重要 key”。另一个思路是检查是否存在大量设置了相同过期时间的 key导致同一时间集体过期形成周期性缓存雪崩。这个回答的核心逻辑是不要凭空猜先用 Redis 自带的指标锁定位问题范围再根据业务特征调参数并且观察调整前后的趋势。5. 生产实践配置、命令与避坑经验5.1 三个关键参数怎么配合我们把redis.conf里最关键的三个参数一起看maxmemory、maxmemory-policy、maxmemory-samples。我的建议是maxmemory不要设成“恰好等于业务预估容量”要给 buffer。比如预估峰值 3GB可以设置maxmemory 4gb留出一定余量否则淘汰会过于频繁命中率暴跌。maxmemory-policy缓存类业务一般用allkeys-lru如果对数据有强一致性要求、宁可写入报错也不能丢数据用noeviction如果热点非常集中且访问趋势易衰减用allkeys-lfu。maxmemory-samples默认 5 一般够用如果内存相对充足而你又想让淘汰更精确可以调到 10代价是每次淘汰时的计算量增加性能要求极高的场景不建议调太高。5.2 常用命令速查面试和排查都靠它下面是我认为最值得记住的几组命令CONFIG GET maxmemory/CONFIG GET maxmemory-policy查看当前配置。CONFIG SET maxmemory-policy allkeys-lru临时动态修改淘汰策略不用重启。EXPIRE key seconds给 key 设置过期时间TTL key查看剩余秒数返回 -1 表示永不过期-2 表示 key 不存在或已过期。PERSIST key去掉过期时间让 key 变成持久 key。INFO stats查看expired_keys累计过期删除数量和evicted_keys累计淘汰数量。INFO memory查看used_memory、maxmemory、mem_fragmentation_ratio。MEMORY USAGE key查看某个 key 占用的字节数。我在排查问题的时候第一反应通常不是看代码写的对不对而是先拉INFO stats的expired_keys和evicted_keys。这两个数字能告诉你 Redis 内部到底在发生什么。如果evicted_keys持续快速上涨说明 maxmemory 设置不合理或者业务数据增长比预期快如果expired_keys大量增长说明过期时间设置过于集中或生命周期设计不合理。5.3 我踩过的一个坑把 volatile-lru 当成“只淘汰带过期时间的 key 很安全”有一次负责一个内部项目缓存 key 有两种一种设了过期时间一种没设但没设的那部分其实是“需要长期保留但也不是绝对不可丢”的。我当时的想法是volatile-lru 只动带过期时间的 key不带过期时间的 key 永远不会被淘汰很安全。结果内存持续上涨到 maxmemory 之后设了过期时间的 key 被疯狂淘汰缓存命中率大幅下降而很多不带过期时间的 key 大量堆积占着内存不放。后来我们分析发现业务里 80% 的热点数据恰恰是那些忘设过期时间的 key。于是把策略改为allkeys-lru让所有 key 在内存紧张时都参与淘汰优先淘汰真正不活跃的数据。这个案例给我的教训是策略选择不是背定义而是要看业务里“哪些数据可以被牺牲”以及“热点集中在哪类 key 上”。5.4 避免“缓存雪崩”“缓存击穿”的过期时间设计建议讨论过期删除时面试官一定会顺带问缓存雪崩和击穿。这里我给你一条非常实用的设计建议给同一业务用途的 key 设置过期时间时不要用同一个固定值而是在固定值基础上加一个随机偏移量。比如基础过期时间 3600 秒实际设置时是3600 random(0, 300)秒。这样即使大量 key 在同一时间被写入过期时间点也会被分散开避免出现“同一秒内大面积 key 同时过期”导致的雪崩。这个方法成本极低、效果显著是线上非常常用的技巧。对于热点 key 的击穿问题常见做法是互斥锁重建缓存或者把热点 key 的过期时间直接设为永不过期、由后台任务定期更新。面试时能说出“热点 key 不设过期时间用后台异步更新”这种带实战色彩的操作会比单纯背“缓存击穿是大量请求同时打到数据库”显得有经验得多。5.5 如果 Redis 设置了 noeviction内存满了会发生什么这是面试中经常作为收尾的一个问题。noeviction策略下当内存达到 maxmemory 上限时所有会增加内存的写命令都会返回错误类似OOM command not allowed when used memory maxmemory。读操作和 del、expire 这类能释放内存的命令不受影响。这意味着应用层必须做好错误处理不能盲目相信写入一定成功。生产环境中如果业务对缓存可丢性要求高强烈不建议用 noeviction因为它的行为对业务逻辑侵入性太强容易引发线上报警甚至数据写入链路故障。6. 底层原理补充过期字典、LRU 时钟和内存分配器6.1 过期字典expires dict是怎么工作的Redis 的每个数据库实例redisDb 结构里除了保存所有 key-value 数据的 dict 之外还有一个独立的 dict 专门保存 key 和它的过期时间这个 dict 叫 expires。当执行EXPIRE key 100时Redis 会在这个 dict 里写入 key 对应的过期时间戳当 key 被删除或者PERSIST去掉过期时间Redis 会同步从这个 dict 里移除对应条目。惰性删除和定期删除的检查对象实际上都是这个 expires dict。从内存角度看一个 key 设置过期时间和不设置过期时间占用的内存是不同的因为多了 expires dict 里的一个条目。6.2 LRU 时钟和 idle time 的近似计算Redis 内部维护了一个lru clock它不能精确到绝对时间而是一个以一定分辨率更新的全局时钟。每个 key 在被访问时会记录下当时的 lru clock 值。需要进行近似 LRU 淘汰时Redis 通过比较当前 lru clock 与 key 记录的值算出该 key 的 idle time。需要注意的是lru clock 会因为时钟回绕达到上限后归零产生计算误差所以 Redis 内部对时间差的计算做了回绕处理。这个细节知道的人很少但在面试中如果能说出来会显得你研究过源码逻辑而不只是看二手资料。6.3 内存碎片为什么 maxmemory 没满进程内存却很高内存碎片是生产环境最容易困惑的问题之一。Redis 使用分配器默认是 jemalloc管理内存jemalloc 为了性能会按固定大小分级分配内存。当你写入大量不同大小的 key 并频繁删除后内存中会产生很多细小的空洞导致 used_memory 不高但 used_memory_rss进程实际占用的物理内存却很高。这时即使没有达到 maxmemory也可能因为 RSS 过高被系统 OOM Killer 盯上。应对方式是定期用MEMORY DOCTOR查看碎片建议或者在业务低峰期重启节点、使用activedefrag yes配置开启在线碎片整理。这个指标在面试中不会直接问但在项目经验描述里提到“我处理过内存碎片率过高的问题”是非常好的实战亮点。7. 从验收到实战一份可以直接照着用的自查清单为了让你既能应对面试又不至于在生产环境踩坑我把上面所有内容浓缩成一份自查清单你可以保存下来在项目上线或性能排查时对照使用。[ ] 是否设置了合理的maxmemory并预留了 20% 至 30% 的余量[ ]maxmemory-policy是否匹配业务的数据可牺牲性和热点分布特征[ ] 是否存在大量相同过期时间的 key是否已加随机偏移[ ] 是否对“热点 key 永不过期 后台更新”这种特殊方案做了预案[ ] 是否监控了evicted_keys和expired_keys的增长趋势[ ] 内存碎片率是否长期保持在高位是否需要开启activedefrag或定期维护[ ] 大数据 key 是否单独做了大小限制避免淘汰和迁移过程中出现延迟毛刺这几条如果你能全部回答上来说明你不是“背过 Redis 面试题”而是真正用 Redis 做过内存规划设计。面试官如果给你一个场景题你可以直接把这个清单里的判断逻辑串成一条线讲出来。8. 最后分享一点个人体会这类问题的价值其实不只是面试。我在实际排查过一个类似“线上缓存命中率骤降”的问题花了两小时才发现是某次配置更新把maxmemory-policy从 allkeys-lru 改成了 volatile-lru而大部分热点 key 都没设过期时间导致内存一满就开始疯狂误杀少量带过期时间的 key很多本该被淘汰的冷数据反而一直占着内存。那次之后我养成一个习惯不管改 Redis 哪个参数改完都立刻拉一次INFO stats观察evicted_keys和expired_keys的增量变化。你如果也想在这个领域变得扎实建议不只背策略名字而是亲手对一个测试实例写入不同类型的 key删掉一些、改改策略再用INFO观察数字变化。踩过一次坑比看十篇博客都管用。