1. 过期删除策略全解析Redis 凭什么做到“该删的删不该删的不删”很多人一看到“过期删除”这四个字第一反应就是Redis 肯定像闹钟一样到点就把 key 删掉。这个理解不能说错但和你脑海里的实现方式大概率不一样。Redis 既没有为每个 key 单独挂一个定时器也没有每秒钟把所有 key 都扫一遍它采用的是“惰性删除 定期删除”的组合打法。这套组合方案说白了就是用最少的开销把绝大多数该删的 key 删掉剩下的实在来不及删的交给内存淘汰策略兜底。为什么这么设计我举个现实中的例子你就懂了。你手机里装了 50 个 App后台通知中心不可能每秒都把所有 App 的状态刷新一遍那样电量撑不住。它的做法是你点进某个 App 的瞬间再去检查这个 App 有没有新消息。这就是惰性思想。定期删除就相当于系统每隔一段时间统一清理一次后台缓存避免你点进去的时候发现东西已经堆成山了。1.1 惰性删除只有在被访问时才检查过期惰性删除的逻辑非常朴素每次执行读写命令之前Redis 会先检查 key 是否已过期如果过期了就直接删除然后返回 nil 或者走“key 不存在”的逻辑。你可能会问那如果这个 key 过期之后一直没人访问呢它不就一直占着内存确实如此。惰性删除的缺陷就是“过期了但不删”内存会被这些“僵尸 key”慢慢吃掉。但换个角度想如果一个 key 过期后永远没人访问那它占用的内存可以理解为“被遗忘的资源”Redis 选择把清理它的成本推迟到下次访问时再付。这就是经典的 lazy 策略把开销延迟到不得不付的时候才付。在 Redis 的实现里所有会读取 key 的命令入口比如 get、setnx、incr、hget 等等都会调用一个expireIfNeeded函数。这个函数会先判断 key 是否在过期字典里然后比较当前时间和过期时间一旦发现已过期就直接删除。注意一点这个删除操作是同步的如果 key 的数据量特别大——比如一个大 Hash 有几百万个字段——那么删除时释放内存的过程可能会让单次命令的响应时间变长这在高并发场景下是实实在在需要留意的坑。1.2 定期删除周期性扫描控制删除频率与时长既然惰性删除有“漏网之鱼”Redis 就必须有个后台机制定期清理过期 key。这个机制就是定期删除active expire cycle。它并不是扫描全库的所有 key那样成本太高了。Redis 会从设置了过期时间的 key 集合中随机取一批出来检查删除其中已过期的 key然后判断本次循环的耗时和删除比例决定是继续还是要休息。我翻过 6.0 版本左右的源码核心逻辑在expire.c的activeExpireCycle函数里。有几个关键参数值得你记住面试聊到的时候会显得你读过源码CRON_DEFAULT_HZ默认值为 10表示serverCron每秒执行 10 次定期删除检查。这意味着每 100ms 左右会触发一次。每次会随机抽取ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP个 key默认是 20。也就是说一轮最多检查 20 个设置了过期时间的 key。每轮循环的总耗时被限制在ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC默认是 25%。这表示定期删除不会占用太多 CPU。这是一组非常务实的参数。它保证了 Redis 在单线程模型下不会因为清理过期 key 而把主线程卡太久。即便一段时间内产生大量过期 keyRedis 也不会一次性全删而是分多轮慢慢清。这种“细水长流”的设计和你日常工作中对线上服务做限流削峰是一个道理——总不能让后台清理任务把高峰期的业务请求给拖垮了。1.3 两种策略的配合逻辑与性能上限现在把两种策略放到一起看惰性删除负责“精确打击”key 被访问了己经到期立刻删。定期删除负责“广撒网”后台每 100ms 随机抽一批过期 key能删多少算多少。但这里必须承认两种策略配合依然存在“过期了但没被删掉”的窗口期。如果一个 key 设置了 10 秒过期但在这 10 秒后没有任何访问而且它一直没被定期删除抽中那它就会继续留在内存里。这种状态可以持续很久唯一能强制回收它的就是下一节要讲的内存淘汰策略或者你手动执行scan配合del去清理。所以面试时如果有人问“Redis 的过期 key 一定会立刻被删除吗”正确答案是不会。你应该把“过期删除”理解为一种尽力而为的机制而不是精确的定时任务。顺带一提如果操作系统的maxmemory没有被限制这些过期 key 甚至可能“永生”。Redis 的内存使用率会因此慢慢上涨这也是很多人在低峰期不重启 Redis 就发现内存只涨不降的原因之一。2. 内存淘汰策略深入拆解内存不够时Redis 怎么“让位”过期删除解决的是“key 到了生命周期末端”的问题而内存淘汰解决的是“内存空间已经耗尽必须牺牲一部分 key 来保全整体”的问题。两者的触发条件完全不同过期删除key 设置了 TTLTTL 到了尝试删除。内存淘汰maxmemory被设置了当前内存使用量突破了阈值必须腾出空间。我之前在实际运维中踩过一个大坑以为只要设置了maxmemory就万无一失结果忽略了淘汰策略的默认配置。Redis 的默认淘汰策略是noeviction意思是内存满了之后不淘汰任何 key直接对写命令返回 OOM 错误。如果你的项目里 Redis 承担的是消息队列缓冲或者写入频繁的缓存这个默认策略会直接让你的线上接口报错。2.1 八种淘汰策略全景对比Redis 从 4.0 版本开始总共提供了 8 种淘汰策略。我整理成了表格方便你面试前快速过一遍策略全称/含义淘汰范围适用场景noeviction不淘汰写命令报错无强一致场景如分布式锁、事务性数据allkeys-lru从全键中按 LRU 淘汰所有 key一般缓存场景volatile-lru从设置了 TTL 的键中按 LRU 淘汰仅过期键希望冷数据优先淘汰的缓存场景allkeys-random全键随机淘汰所有 key所有 key 访问频率相似volatile-random过期键集合随机淘汰仅过期键兜底策略很少单独选allkeys-lfu全键按 LFU 淘汰所有 key访问频率差异大的热点场景volatile-lfu过期键集合按 LFU 淘汰仅过期键缓存加频率感知需求的混合场景volatile-ttl优先淘汰剩余 TTL 短的 key仅过期键让即将过期的 key 提前腾位置看到这你可能发现了带 volatile 前缀的策略只会在“设置了过期时间的 key 集合”里挑选淘汰对象。这意味着如果某个 key 没有设置 TTL那你就算配了volatile-lru这个 key 也永远不会被淘汰。它和过期删除的优势就体现出来了——如果数据你想长期保留不设 TTL它就基本不会被「过期家族」的策略打败。2.2 LRU 与 LFU 的底层实现与选择逻辑面试官特别喜欢追问 LRU 和 LFU 的区别尤其是问你如果缓存的数据有突发性的热点LRU 和 LFU 哪个更合适这里先说结论再把原理展开。LRU (Least Recently Used)是基于“最近访问时间”来淘汰的它的假设是如果某个 key 很久没被访问那未来被访问的概率也低。Redis 采取的不是教科书里那种标准的双向链表 哈希表实现而是一种近似 LRU的方案。每个 key 会记录一个最近访问的lru时间戳24 位精度是秒级。在需要淘汰时Redis 会随机抽取一批 key然后从中选择lru时间戳最小的那个 key 进行淘汰。为什么用近似方案因为标准 LRU 在 Redis 的单线程模型下维护双向链表成本太高随机采样加排序已经是性能和准确度之间的最佳平衡。LFU (Least Frequently Used)则是基于“访问频率”来淘汰的。它的核心逻辑是如果一个 key 在过去被访问的次数很少那它未来被访问的概率也低。Redis 的 LFU 实现比 LRU 复杂不少每个 key 用 24 位空间拆成两部分高 16 位存上次衰减时间分钟级低 8 位存访问次数logarithmic counter对数计数器。为什么用对数计数器因为如果直接用 8 位帮你数访问次数最多数到 255 次就溢出了。对数计数器的意思是访问次数越大增长越慢这样能区分高频访问的 key 之间的差异同时又不会很快爆掉。我个人的建议如果你的业务有“突然爆火”的场景比如热榜、秒杀用 LRU 更合适。因为 LFU 会对旧的高频 key 产生“惯性”——它以前很热即使现在不怎么热了频率计数的衰减也慢结果就是真正的新热 key 很难挤进来。反过来如果你的流量模型是稳定的、有明确的长尾分布LFU 的缓存命中率会明显优于 LRU。2.3 实际场景怎么选缓存、热点、数据一致性的权衡场景永远比理论更有说服力。我从真实项目里挑几个典型配置分享给你通用缓存服务容忍短暂的数据缺失allkeys-lru。Redis 中缓存的数据基本都可重建内存满了丢几个冷 key 影响不大。缓存与业务数据混用且缓存设置了 TTLvolatile-lru。这种方式最稳妥因为只有设置了过期时间的 key 才参与淘汰你手写的那些需要长期存在的 key 不容易被“误杀”。热点数据访问频率极不均匀allkeys-lfu。比如推荐系统、排行榜热门数据占用 80% 的访问量淘汰时优先淘汰低频的冷门数据命中率会非常亮眼。缓存 key 有过期时间但业务可以容忍任意淘汰volatile-ttl。这个策略在内存紧张时非常“冷酷”谁离过期最近谁先滚蛋。适合那种 TTL 设置得很有层次感的场景。另外有个误区想提醒一下每次内存淘汰的代价是“内存的写入操作或者命令会被阻塞”。因为 Redis 是单线程执行命令在内存达到maxmemory后会先执行淘汰逻辑再去执行你的写命令。如果淘汰策略是 LRU它还需要抽样比较这个过程也是消耗 CPU 的。因此淘汰策略不是一个「配置完就不管」的东西它是需要你根据业务特征去持续调优的。3. 面试高频追问与坑点过期和淘汰背后隐藏的机制这一节本来不在我的大纲里但是最近面试实习生时发现几乎所有候选人到了“过期删除和内存淘汰的配合流程”这就会被卡住。所以特意把这部分拆出来当成面试官的视角来拷问一下。3.1 两者的执行顺序先过期删除还是先内存淘汰先说结论在一条写命令的执行链路里Redis 会先处理过期删除如果 key 本身已过期然后检查内存是否达到 maxmemory如果达到再执行内存淘汰判断。具体来说一个写命令进入 Redis 后首先会调用 expireIfNeeded把已经过期的 key 清掉然后走到processCommand中的performEvictions阶段此时如果maxmemory被触发就开始执行内存淘汰。这个顺序很有讲究。你可以想象一个餐厅到了打烊时间过期服务员先把桌上没人吃的菜收掉但收完之后如果发现后厨仓库还是满了就得继续把一些还没上桌的食材也扔掉淘汰。这就引出一个容易忽略的点过期删除和内存淘汰是两套独立的逻辑前者只关心 key 是否过了 TTL后者只关心内存还够不够。哪怕你的内存还没满过期 key 也可能存在哪怕你的内存已经满了没有设置 TTL 的 key 也可能被淘汰如果选的是 allkeys 系策略。3.2 主从复制下的过期删除主删了从是怎么跟随的如果你在面试时提到主从复制面试官大概率会追一句“主节点删除了过期 key从节点怎么知道”这里牵扯到一个非常有意思的设计。从节点不会主动执行过期删除也不会在执行命令时检查 key 是否过期。它会在处理客户端读请求时如果发现 key 已过期仍然返回 nil但不会自己去删这个 key而是等待主节点发送删除命令。主节点在惰性删除或定期删除一个 key 后会向所有从节点广播一条 DEL 命令从节点收到后才会真正执行删除。这样设计的核心原因是避免主从节点的删除时间不一致。如果每个从节点都独立判断过期时间可能因为网络延迟、时钟差异导致主节点还没删除从节点已经把数据删了。等到主节点把新的数据同步过来时数据就出现了分叉。更重要的是如果从节点承接了读流量它的本地时钟又比主节点快那从节点就会提前丢掉未过期的数据造成数据不一致。所以 Redis 干脆把删除这个动作的“决定权”统一交还给主节点从节点只能被动执行。3.3 Redis 持久化、分布式锁与淘汰策略的纠缠这两个话题经常被单独提出来考但很少有人把它们和淘汰策略联系在一起。我挑两个最常见的坑讲讲。第一个坑RDB 快照与过期 key 的纠缠。当 Redis 执行BGSAVE生成 RDB 文件时快照里可能包含还没被删除的过期 key。主节点在加载 RDB 时会对过期 key 进行过滤过期的一律不载入。但如果你用旧版本的 RDB 文件做数据恢复或者迁移必须意识到过期 key 不会在文件生成的那一刻消失它只是在你加载时才被过滤掉。第二个坑分布式锁 key 与 allkeys 策略的冲突。假设你用 Redis 实现分布式锁key 设置了过期时间作为兜底比如 10 秒。如果内存不足而 redis 配置的是allkeys-lru那么这个锁 key 完全可能还没到 10 秒就被淘汰了。一旦锁 key 被淘汰其他客户端就都能获取锁分布式锁就失效了。所以对于分布式锁这种强一致场景应该使用noeviction策略或者将锁 key 与可淘汰的缓存 key 隔离开用独立的 Redis 实例。这个点我在生产环境里救过一次团队提出来之后面试官直接给我打了个高分。4. 监控、踩坑与实战调优让淘汰策略真正在线上跑稳原理讲透了最终还是要回到线上。这一节我从真实运维和调优经验出发把最关键的监控指标、排查手段和常见故障整理给你并附上一些人事后才会告诉你的事。4.1 关键监控指标与排查手段线上 Redis 到底该看哪些指标我按重要性排个优先级used_memory 和 maxmemory 的比值这是最基础的。如果你已经用了 80% 以上那就得关注淘汰是不是开始频繁触发了。evicted_keys 指标这个是INFO stats里的一个计数器统计累计淘汰的 key 数量。如果你发现它在快速增长说明内存压力非常大缓存命中率大概率在下降。expired_keys 指标累计过期 key 数量。配合INFO stats能判断过期删除是否在正常工作。latency命令响应延迟如果 CPU 使用率正常但命令延迟变高很可能是因为淘汰过程中抽样和删除大量大 key 导致主线程阻塞。我在公司里给 Redis 加过一套监控面板核心逻辑就一行used_memory / maxmemory超过 85% 时报警并把 evicted_keys 的增量纳入告警条件。这比单纯看 CPU 或内存绝对值的告警要直观得多。很多时候你去看内存使用率可能只有 50%但淘汰却已经发生了——因为 Redis 的 maxmemory 设置得比实例物理内存小这是很多人配置时忽略的。排查手段方面推荐两个命令redis-cli INFO stats | grep evicted_keys redis-cli INFO memory | grep used_memory如果需要精确知道哪些 key 占了大量内存别用keys *一定要用scan配合debug object或者memory usage来定位。线上执行keys *是条高危命令大数据量时直接卡死主线程这个是运维底线问题。4.2 常见淘汰与过期问题速查表我把过去几年在论坛和实际项目里遇到的典型问题整理成了一个速查表强烈建议你收藏现象可能原因解决方案内存满了写命令报 OOM配了noeviction改为allkeys-lru或volatile-lru并确认业务可容忍丢失内存不到 100%key 却被淘汰maxmemory设置为当前实例内存的某个百分比调大maxmemory或清理不必要的 key缓存命中率断崖式下降淘汰策略选错比如 LFU 选了之后旧热点把新热点挤没改用allkeys-lru或为热 key 单独设置更长的 TTL设置了 TTL 的 key 一直存在定期删除没抽查到且期间没被访问手动执行scandel或者等待内存淘汰兜底主从切换后从节点数据比主节点多从节点已过期的 key 未删除等主节点广播 DEL避免切主后立刻使用 old slave 数据大 key 过期删除导致阻塞删除大 Hash/List 耗时过久使用unlink命令异步删除或者拆分大 key分布式锁被“偷走”锁 key 被 allkeys 策略淘汰独立 Redis 实例或改用noeviction策略这里我要单独强调一下unlink这个命令。Redis 4.0 之后有了 it它会在后台异步释放内存避免大 key 删除时长时间阻塞主线程。凡是生产环境我建议把大 key 删除的脚本一律用unlink代替del。对于超过 100MB 的 keydel花的时间可能让你眼睁睁看着超时增多。4.3 从一次线上事故看策略调优的全过程最后讲一个我亲身经历的事故。某次凌晨高峰期用户反馈订单查询和商品详情接口大面积超时Redis 的 CPU 使用率飙到 90% 以上。当时我们用的是一台 8GB 内存的实例maxmemory设置为 6GB淘汰策略是allkeys-lru。我先看evicted_keys的增长速度5 分钟内增加了 30 多万。再配合慢查询日志发现大量超时命令集中在对大字符串 key 的读取上。进一步排查内存才知道有个业务方把一批图片 base64 编码后塞进了 Redis单 key 接近 20MB。当内存达到上限淘汰策略要淘汰这些大 key 时删除的内存释放操作阻塞了主线程导致所有读写命令排队。当时的处理步骤很简单但我复盘了很久临时把淘汰策略改为allkeys-random加快淘汰速度缓解写入压力。用scan找到那批大 key用unlink异步删除。给业务方制定规范单 key 超过 1MB 的内容一律不能进 Redis用对象存储替代。这件事给我最深的教训是淘汰策略不是万能兜底它也有自己的性能成本。如果大 key 太多淘汰时的内存释放代价足以拖垮整个实例。所以内存治理的关键不仅是选对淘汰策略更要在源头上管住 key 的大小和数量。5. 面试答题逻辑与核心话术既然标题是“面试必问”那最后这部分咱们就直接站在面试官对面看看如何把前面这些内容组织成一条清晰的答题线。其实面试官考察的不是你背了多少参数而是你能不能把机制讲清楚、把场景匹配对。5.1 一分钟极简答辩模板如果面试官问“说说 Redis 的过期删除与内存淘汰策略”你可以按照这个逻辑组织先给结论Redis 的删除链路包含两层兜底过期删除惰性 定期负责处理已到 TTL 的 key内存淘汰负责处理内存耗尽时的 key 驱逐。再分点展开惰性删除的“检查时删”定期删除的“周期抽样删”以及为什么两者都无法 100% 清理所有过期 key。引出淘汰策略8 种策略按两个维度分类——是否带 volatile只在过期 key 集合里选以及淘汰算法是 LRU/LFU/random/TTL。结合实际场景给出倾向缓存场景推荐 allkeys-lru强一致场景选 noeviction热点不均匀场景选 allkeys-lfu。最后抛出一个亮点主从复制下从节点不主动删过期 key依赖主节点广播 DEL这既是面试加分项也是线上一致性保障的关键。这个模板把“是什么、为什么、怎么做、有什么坑”全部覆盖了。我见过不少候选人把前三点背得特别熟但一到场景选择就含糊其辞。面试官并不期待你选“最正确”的那个策略——本身就是取舍题——但你得能说出选择的理由说出权衡的依据这就比单纯背参数高一档。5.2 面试官最喜欢追问的三个延伸问题“如果 Redis 内存满了又触发了大量过期 key会先处理哪个” 这个问题我在 3.1 节已经拆过你先执行过期检查再进入内存淘汰判断。“AOF 重写时过期 key 会写进 AOF 文件吗” 不会。AOF 重写时会过滤掉已经过期的 key但如果你用BGSAVE生成的 RDB 文件加载时也会过滤过期 key。这个细节可以体现你对持久化机制的理解。“volatile-lru和allkeys-lru在内存满时如果所有 key 都没设置 TTL会发生什么”volatile-lru找不到可淘汰的过期 key和noeviction一样直接 OOM 报错。这个坑我当年就踩过。根据我个人的经验面试官真正想听到的不是“LRU 怎么实现”这种教科书答案而是你能不能在讲完原理之后顺手带上一句生产实践的感悟。哪怕只是说一句“我在线上用 allkeys-lru 时发现大 key 淘汰会阻塞主线程所以后来改成了 unlink 配合拆 key”整个回答的层次感就会完全不一样。如果你正准备面试把这一点刻意练一练效果比背十道题都来得明显。