多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Redis过期删除与内存淘汰:策略选型、底层原理与生产实践

Redis过期删除与内存淘汰:策略选型、底层原理与生产实践 这些年我被问到最多的 Redis 题目不是 zset 底层结构也不是主从复制而是“过期删除”和“内存淘汰”这两件事。很多候选人能说出“定期删除 惰性删除”“LRU、LFU、volatile-ttl”这些名词但一旦被追问“为什么需要两套机制”“淘汰策略选错了会有什么后果”就明显底气不足。说实话这两个机制是 Redis 缓存治理的基石搞懂了它们你对 Redis 的理解会直接上一个台阶而且它们是面试官验证你有没有真实生产经验的高频切入口。先说清楚适用范围这篇内容适合所有用过 Redis、准备面试或正在做缓存优化的开发者以及想搞清楚“明明设了过期时间内存却还是爆炸”这类问题的运维和架构师。我会把策略拆开揉碎讲配套实际配置命令、参数选型和排查思路还会附上我整理过的面试应答框架。看完你不仅能应付问题还能在真实项目里把 Redis 内存压得住、缓存命中率提上去。1. 两套机制并存的原因各自解决不同的“垃圾”问题很多新手容易把“过期删除”和“内存淘汰”混为一谈但它们在 Redis 里是完全独立的两个模块唯一的共同点是都会让 key 消失。理解这个区别是整道面试题的核心前提。1.1 过期删除针对的是“主动设了 TTL 的 key”当你执行SET key value EX 60之后这个 key 就在 Redis 内部被打上了“将在 60 秒后失效”的标记。过期删除机制要处理的就是这些到了时间却依然占着内存的 key。注意一个关键点Redis 默认不会在 key 到期的那一瞬间立刻把它物理删除因为维护一个专门的时间轮询线程会浪费大量 CPU而且很多 key 可能还没被访问就白白占用了资源。所以 Redis 采用了“惰性删除 定期删除”的组合策略具体我后面会展开讲。这里要理解的核心是过期删除的目标是“尽力清理那些生命周期已经结束但还没被用户访问的 key”它治理的是 TTL 属性存在的数据的生命周期而不是“内存满了怎么办”的问题。也就是说哪怕你所有 key 都设置了过期时间过期删除也只是一个后台的尽力而为行为它并不能保证内存永远不会满。1.2 内存淘汰针对的是“物理内存达到上限”之后的兜底内存淘汰策略只有在maxmemory达到或超过阈值时才会被触发。比如你设置了maxmemory 4gb当 Redis 已用内存超过 4GB写入新 key 时就会触发淘汰逻辑。它不像过期删除那样看 key 是否到期而是根据规则挑选“最不值得保留的 key”从内存中移除腾出空间给新的数据。因此核心结论可以一句话概括过期删除是“守时”的保洁员到了点就清理内存淘汰是“救火队”内存不够了就必须踢人。两者并不冲突反而会协同工作——许多被内存淘汰策略选中的 key往往也是已经过期但还没来得及被惰性删除清理掉的 key。面试官问出这个问题往往是想看你能否说清“TTL 过期”和“maxmemory 强制驱逐”是两个层面的事而不是只背一堆策略名字。1.3 这两个机制引出的第一个面试坑过期 key 的删除不一定立即释放内存我面试别人的时候经常听到一句话“key 设置了过期时间到期后 Redis 内存会自动降下来。”这其实是个误解。由于惰性删除的存在一个 key 过期后如果没有再次被访问Redis 本身并不知道它应该被删掉只有等到定期删除的抽样扫描到它或者在读它的时候触发了惰性删除内存才会真正释放。所以你会看到一种现象明明大量 key 已经过期但used_memory依然高位徘徊。验证方法很简单INFO memory里的expired_keys一直在增长但内存不降这种场景在小内存实例上尤其常见。你要是能在面试中主动说出这个现象再补一句“所以不能把过期删除当成内存回收的保证必须设置合理的 maxmemory 作为兜底”面试官基本就会觉得你是真碰过生产环境的人。2. 深度拆解过期删除策略惰性删除、定期删除与副作用Redis 过期删除的核心代码分散在expire.c和部分事件循环逻辑里读懂它的设计思路比记代码更有用。整体上就是两条腿走路一懒一勤各补短板。2.1 惰性删除读的时候顺手删代价是残留垃圾惰性删除的触发点在每次访问 key 的命令执行之前。lookupKeyRead这类入口函数会先检查 key 是否已经过期如果过期就直接删除并返回空避免你读到一个过期数据。这个机制的好处是删除操作完全平摊到普通命令的路径上不需要额外线程CPU 开销几乎为零。坏处也很明显如果一组 key 过期后再也没有人访问它们就会一直躺在内存里永远不被清理直到被淘汰策略或其他机制覆盖。实际操作中大家会发现惰性删除对“热 key”很友好因为热 key 一旦过期下一次访问就能立刻感受到但对“冷 key”极不友好大量冷数据带着过期标记长期占用内存。所以在内存敏感的订单缓存、token 缓存场景里单纯依赖惰性删除是完全不够的。2.2 定期删除周期性抽样用 CPU 换内存空间为了弥补惰性删除的盲区Redis 在 serverCron周期性任务函数中安排了定期删除。它并不是全表扫描所有 key而是通过activeExpireCycle函数在每个周期里“抽样”一部分设置了过期时间的 key检查是否过期过期则删除。核心设计逻辑是“每次最多执行一段时间且不超过一定比例”避免长时间阻塞主线程。具体参数包括循环次数、抽样数量、删除耗时上限等不同版本略有调整但本质都是控制单次清理的 CPU 成本。这样设计的好处显而易见不需要像全量扫描那样牺牲 O(N) 的时间也能让那些长期没被访问的过期 key 有机会被清理。代价则是“概率性”——一次没扫到可能得等很多轮才能扫到冷 key 依然会在内存里滞留很久。我把这三者的协作比作“单元楼的保洁阿姨”——定期来打扫公共区域但每层楼不一定每次都扫到而住户自己家用完东西有人来拿才会顺手清理掉放在门口的垃圾。2.3 版本演进中的关键变化从“是否有 TTL 的 key”到“分数据库采样”早期的 Redis 版本在定期删除时是对每个数据库Redis 默认有多个 db进行遍历每个库中再遍历所有“带过期时间的表”expires dict。实现对抽样比例、运行时长和数据库数量都有严格限制。这里有一个非常值得面试展开的细节过期 key 很可能长时间滞留在 redisDb 的 expires 字典里但因为抽样概率不高无法被及时清理。Redis 4.0 以后引入了一些优化例如更动态的循环策略Redis 7.0 进一步优化了 expire 相关性能。生产实践中最直观的体会是如果线上实例的过期 key 非常多可以调大hz默认是 10表示每秒执行 10 次 serverCron让定期删除跑得更密集一些但hz太大会消耗更多 CPU一般到 100 就差不多了。另外一个隐藏点如果 Redis 内存指标持续上涨且 expired_keys 数字快速增长说明定期删除抽样和实际过期量不匹配这时除了调hz还得检查是否存在大量同时过期的 key 造成“过期风暴”比如缓存雪崩场景。2.4 与过期删除强相关的命令和排查手段运维层面我们需要主动干预的地方主要有三个TTL key/PTTL key查看某个 key 还有多久过期注意负数表示已到期或不存在。SCAN cursor MATCH pattern COUNT n在小批量遍历 key 时不能因为看了几个过期 key 没删除就认为没问题因为 SCAN 是增量式和过期删除是两套逻辑。INFO stats里的expired_keys这个累计值可以帮你判断过期删除是否在正常推进例如通过前后两次采样比较增量大约能算出每分钟清理了多少 key。我还建议在监控面板上同时观察used_memory和expired_keys。如果前者一直涨而后者也在涨说明清理速度跟不上过期速度内存压力主要来自“过期但未清理”的 key如果前者涨但后者几乎不动那问题多半出在大量 key 根本没有设置 TTL这时要从业务代码层面审查 key 的设计。3. 内存淘汰策略全解析八种策略的适用场景与底层选择逻辑如果说过期删除是 Redis 内部的“养生”那么内存淘汰策略就是“极限生存”。这一节是面试含金量最高的部分也是实际生产环境优化内存的硬骨头。需要先明确一个基本概念不设maxmemory时Redis 在 64 位系统上默认可以用完物理内存直到系统 OOM设置了 maxmemory 之后一旦内存达到上限写命令会直接收到OOM command not allowed错误除非淘汰策略允许腾出空间。3.1 maxmemory 参数与动态配置方法先看配置方式。修改redis.conf中的maxmemory 4gb maxmemory-policy allkeys-lru也可以在运行时通过 CONFIG SET 动态调整无需重启CONFIG SET maxmemory 4gb CONFIG SET maxmemory-policy volatile-lru CONFIG GET maxmemory特别提醒maxmemory的实际单位支持kb、mb、gb如果设置为 0表示不限制。生产环境一般建议预留物理内存的 20% 给操作系统缓冲和 fork 开销maxmemory不要直接等于物理内存。另外很多云厂商的 Redis 服务有默认策略开通前先查CONFIG GET maxmemory-policy不然发现缓存穿刺到 DB 已经晚了。3.2 各策略对比与选型volatile 系列 vs allkeys 系列我用了一张表格方便你快速对照策略名称淘汰范围核心逻辑适用场景noeviction不淘汰写命令直接报错只希望 Redis 当数据库不允许丢数据的强一致场景allkeys-lru所有 key用近似 LRU 淘汰最久没访问的 key用于纯缓存不区分是否设 TTL是最常见的缓存兜底策略volatile-lru设置了过期时间的 key在 expires 集合中选最久没访问的 key 淘汰希望只淘汰有 TTL 的缓存保留持久 key 的场景allkeys-lfu所有 key用 LFU 淘汰访问频率最低的 key热点分布极不均匀、希望保留真正高频 key 的缓存场景volatile-lfu设置了过期时间的 key在 expires 集合中选访问频率最低的 key 淘汰有 TTL 数据为主想优先淘汰低频数据的场景allkeys-random所有 key随机淘汰缓存数据无冷热之分时随机几乎等价于最公平volatile-random设置了过期时间的 key随机淘汰只希望淘汰可丢失缓存且冷热不明显的场景volatile-ttl设置了过期时间的 key淘汰剩余 TTL 最短的 key希望先淘汰即将过期的 key但要注意 TTL 可能不准确实际业务里最常用的组合是allkeys-lru或allkeys-lfu。原因很简单一旦把 Redis 当成缓存就应该假设任何 key 都允许被淘汰而不去纠结它有没有 TTL否则大量没设 TTL 的业务 key 可能把内存占满造成缓存永不清除。另外noeviction在 Redis 作为分布式锁或计数器存储时很合适因为锁 key 一旦被淘汰会导致锁失效业务上要注意这种风险。3.3 近似 LRU 的实现技巧不好精确排序就采样后“矮子里拔高个”Redis 的 LRU 并不是维护一个严格按访问时间排序的链表那样太浪费内存。它的实现是给每个 key 记录一个24 bit的 LRU 时钟默认每 24 天溢出回绕一次。当需要淘汰时并非在所有 key 中找最久未访问的而是从“采样池”中随机抽取若干 key默认 5 个然后淘汰其中最旧的一个。这个采样数可以通过maxmemory-samples参数调整默认是 5调大到 10 能让淘汰结果更接近真实 LRU但会略微增加 CPU 和耗时。我实测过一个现象如果缓存数据量巨大且访问模式非常均匀近似 LRU 和精确 LRU 的效果差距不大但如果热点差异明显抽样数量偏小会导致个别刚写入的热 key 被误杀。所以调大 samples 在某些高命中率场景下收益很大但也不宜过大20 以上收益递减明显。3.4 LFU 的实现与参数细节为什么要关注访问频率而不是最后访问时间LRU 有一个经典缺陷如果一个 key 曾经是热点后面突然冷下来了但因为“最后一次访问时间”较新LRU 还是会把它当作热数据保留白白浪费内存。Redis 4.0 引入 LFU通过一个特殊编码存储访问计数和衰减因子每次访问计数增加但计数会随时间衰减从而反映“近期热度”。LFU 相关的核心参数是lfu-log-factor和lfu-decay-time。lfu-decay-time默认是 1表示每 N 分钟计数减半lfu-log-factor控制计数增长速度越大越不容易被淘汰。实际操作中如果业务存在“突发流量访问某 key 一次然后不再访问”的扫描型场景LFU 比 LRU 更适合因为能精准排除这类一次性热点。这里也引出一个面试加分点Redis 的淘汰决策是分布式的元数据层面的近似决策不是按容量去做的所以每个 key 本身要承担少量内存开销。比如 LRU 字段在 RedisObject 里面占了 24 位这已经通过内存复用实现而 LFU 也复用这个字段。讲到这里面试官基本会觉得你对内存模型有了解。3.5 淘汰触发时机与主线程阻塞风险淘汰并不是后台线程干的而是发生在命令处理的主流程中。写入新 key 时freeMemoryIfNeeded会被调用然后根据策略循环淘汰 key直到内存低于 maxmemory 才继续执行。需要注意如果 key 体积很大比如 value 是几 MB一次淘汰多个大 key 可能造成命令延迟变高。更隐蔽的一个问题主从复制结构下主节点淘汰某个 key 后会向从节点发送一条删除命令DEL从节点的淘汰动作其实是被动执行的。如果主节点因为内存压力短时间内大量淘汰会在主从之间产生大量的删除同步流量甚至导致从节点收到命令后短期阻塞。所以内存水位告警一定要提前设不要等淘汰风暴发生后再补救。这个经验在线上大促前尤其重要我会在容量预估时给 maxmemory 留出至少 30% 的缓冲。4. 实操落地从配置、监控到面试应答的一站式指南前面的原理讲得再透最终还是要落到“我能做什么”。这一节我按运维排查和面试应答两个角度给出可直接抄作业的方法。4.1 常用命令与监控指标清单建议把下面这些命令加入你的 Redis 运维手册# 查看内存使用情况 redis-cli info memory | grep -E used_memory|maxmemory|used_memory_human # 查看淘汰策略当前值 redis-cli config get maxmemory-policy # 动态修改策略无需重启 redis-cli config set maxmemory-policy allkeys-lru # 统计被淘汰的 key 数量 redis-cli info stats | grep evicted_keys # 查看过期 key 总数 redis-cli info stats | grep expired_keys # 查看键空间统计 redis-cli dbsize重点看这几个趋势evicted_keys如果长时间持续增长说明业务可能超出容量设计或淘汰策略不合理expired_keys增长正常不代表没有问题还需要结合内存曲线判断清理是否及时。开源的redis_exporter配合 Prometheus 可以轻松拿到这些指标很多公司的 Redis 监控面板里都有标识。4.2 实战案例缓存穿透与淘汰策略的“爱恨情仇”我遇到过一个印象非常深的线上事故。业务方给一批商品数据设置了 24 小时 TTL但预热任务只在活动前执行一次活动期间访问量飙升大量 key 同时过期后Redis 内存压力骤增。因为当时的策略是volatile-lru内存上限又设置得偏低导致一批仍然有效的缓存 key 被淘汰。用户请求落空后全部打到数据库数据库连接数瞬间被打满。当时的修复过程很值得参考第一步把淘汰策略从volatile-lru改成allkeys-lru确保所有 key 都在统一淘汰池里避免“只淘汰带 TTL 的”这种偏科行为同时调大 maxmemory 到物理内存的 80%。第二步给缓存 key 的 TTL 加上随机抖动比如 24 小时 ± 10 分钟防止同一时间集中过期。第三步在业务侧加一层基于空值缓存的黑名单缓存把“查不到的数据”也缓存起来防止穿透。最终缓存命中率从 72% 回升到 97%。这个案例能说明选择淘汰策略不是简单背参数而是要看数据冷热模型、TTL 分布和数据库负载。面试时把这个案例讲完整远比干背八种策略更有说服力。4.3 常见问题与排查思路速查表现象可能原因排查建议内存居高不下明明多数 key 已过期惰性删除未触发定期删除频率不足调大hz抽查是否有大量无 TTL key写命令报 OOM但内存还有空闲淘汰策略设为noeviction且 maxmemory 太小改为allkeys-lru并检查maxmemory配置缓存命中率骤降DB 压力剧增淘汰策略不合理或 key 集中过期检查evicted_keys、过期 key 分布加 TTL 抖动主从延迟突然加大主节点淘汰风暴删除命令过多提前规划容量降低 maxmemory 告警阈值maxmemory-samples调大后 CPU 上升采样计算开销增加一般 5~10 够用不要盲目调到 20某个热点 key 总是被淘汰LRU 被最近访问干扰或 LFU 计数衰减过快考虑换 LFU或手动PERSIST临时不设 TTL表格里最后一行是个容易忽略的点PERSIST命令可以移除 key 的过期时间但移除后该 key 就变成永不过期如果你用的是volatile-lru这个 key 永远不会被淘汰反而可能成为内存钉子户。所以不要轻易对核心 key 执行 PERSIST除非你有明确的缓存变更方案。4.4 面试答题框架与话术参考很多人面试时会罗列策略名称然后等着面试官追问这是最吃亏的做法。建议按“总分总 案例佐证”的方式回答话术可以参考第一步亮结论Redis 有两套机制过期删除负责清理 TTL 到期的 key内存淘汰是在 maxmemory 达到上限时兜底。第二步展开细节过期删除采用惰性删除 定期删除惰性删除是读时检查定期删除是周期性抽样扫描内存淘汰分 volatile 和 allkeys 两个系列常用的有 allkeys-lru、allkeys-lfu、volatile-ttl 等核心用近似 LRU 和 LFU 来决策。第三步讲选择逻辑如果是纯缓存推荐 allkeys-lru 或 allkeys-lfu如果有部分 key 绝对不能丢需要更仔细地设计 maxmemory-policy 和数据分层如果是 Redis 作为数据库或锁服务建议 noeviction 并配好容量告警。第四步加案例可以说“我曾经遇到过一个缓存雪花型穿透的问题当时通过把策略从 volatile-lru 改为 allkeys-lru 并且调整 TTL 随机抖动解决了”把步骤讲清楚即可。考官如果继续深问还会问到“LRU 和 LFU 的区别”“为什么 Redis 不用精确 LRU”“随机淘汰适合什么场景”等这些我在前面都已覆盖。最后再提示一点面试过程中如果能自然提到evicted_keys字段和主从同步删除流量的影响会让你的回答明显区别于背题党。写到这儿我想分享一个自己踩过多次坑之后的体会Redis 的内存治理永远不能靠单一机制。过期删除、内存淘汰、持久化策略、业务 key 设计必须放在一起考虑。比如你即使用了 allkeys-lru如果很多 key 没设 TTL也没设合理 maxmemory缓存依然会慢慢把物理内存吃光反过来如果你只靠过期删除而不配置淘汰策略一次流量毛刺就能让 Redis 直接 OOM。事后复盘这类问题大多不是不懂某个命令而是没有建立“容量水位 — 淘汰策略 — TTL 设计 — 监控告警”的闭环。第一次做缓存治理时我觉得 maxmemory-policy 不就是一行配置嘛后来才发现它背后藏着容量预估、数据冷热分析和事故兜底机制。这些年我再给团队做 Redis 培训都会把“过期删除和内存淘汰”放在第一课因为理解了这两件事后续的缓存穿透、缓存雪崩、主从一致性等问题都会变得清晰很多。
返回列表