面试考察点
- 是否区分 Key 过期与达到 maxmemory 后的淘汰。
- 能否解释惰性删除和主动过期扫描的组合。
- 是否会根据缓存语义选择淘汰策略。
核心答案
过期删除处理 TTL 到期的 Key,Redis 通过访问时惰性检查和后台主动采样删除组合控制成本;内存淘汰则在达到
maxmemory后,根据 noeviction、LRU、LFU、random 或 TTL 等策略为新写入腾出空间。
过期 Key 不保证到期瞬间立刻物理删除,因此监控内存时要理解后台清理节奏。
淘汰策略
策略还分为 allkeys 和 volatile 范围:前者在所有 Key 中选择,后者只在设置 TTL 的 Key 中选择。纯缓存常用 allkeys-lru 或 allkeys-lfu;缓存与不可丢数据混用会让策略难以设计,最好拆分实例。
过期删除不是定时精确删除
Redis 不会为每个 Key 创建一个定时器。访问 Key 时会检查过期时间并惰性删除;后台周期性抽样带 TTL 的 Key,发现一定比例已过期就继续清理。这种组合避免为海量 Key 维护精确时间轮,但意味着过期 Key 可能短暂留在内存中。
因此业务不能把“TTL 到 0”理解为那一毫秒一定释放内存,也不能依赖过期作为毫秒级任务调度。需要精确延时执行时应使用延时队列或持久化任务系统。
maxmemory 触发后的行为
当 Redis 尝试分配内存且超过 maxmemory,才会按配置尝试淘汰或拒绝写入。不同策略意味着不同业务后果:
| 策略类型 | 含义 | 适合场景 |
|---|---|---|
| noeviction | 不淘汰,写命令报错 | 不能静默丢数据的实例 |
| allkeys-lru/lfu | 所有 Key 中近似淘汰 | 纯缓存 |
| volatile-lru/lfu | 只淘汰带 TTL 的 Key | 明确只有缓存 Key 设 TTL 的混合场景 |
| allkeys-random | 随机淘汰 | 很少作为首选,成本低但命中率不可控 |
| volatile-ttl | 优先淘汰更快过期 Key | TTL 与价值强相关时 |
如果选择 volatile 策略但大量缓存忘记设置 TTL,候选集合可能很小,写入仍会失败。策略配置必须与写入规范一起评审。
LRU 和 LFU 的现实含义
Redis 使用近似采样算法,不维护全量严格的访问排序。LRU 偏好最近访问,LFU 偏好长期频繁访问,LFU 还会对计数进行衰减以避免历史热点永久霸占缓存。采样大小、访问分布和内存压力都会影响实际淘汰结果。
选型前应看业务数据的访问曲线:短期突发热点可能更适合 LRU,长期稳定热门内容可能受益于 LFU;最可靠的依据是线上命中率、淘汰量和回源成本,而非算法名称。
TTL 设计方法
TTL 应反映数据的新鲜度、重建成本、写入失效机制和容量目标。可以用“基础 TTL + 随机抖动”打散到期,但不要为了抖动让低价值数据长期占内存。对强一致写后读,TTL 不能替代主动失效;对逻辑过期数据,物理 TTL 应比最大允许陈旧时间更长一些作为安全清理。
商品详情:30 分钟 + 0~5 分钟抖动
登录验证码:5 分钟且单次使用后立即删除
分布式锁:仅覆盖临界区预期时长,必须有续期/超时策略
监控与容量预案
持续观察 used_memory、maxmemory、mem_fragmentation_ratio、evicted_keys、expired_keys、keyspace_hits/misses、命令延迟和回源 QPS。淘汰开始持续增长并不只是 Redis 问题,往往说明容量预算、TTL 设置或流量模型发生变化;应提前扩容、清理低价值数据或调整应用缓存策略,而不是等写入大量报错。
TTL 设计
TTL 应来自业务新鲜度要求,并增加随机抖动避免集中到期。更新 TTL 时注意覆盖写是否保留或清除原过期时间;使用命令时应明确版本语义并测试。
常见误区
Redis 的 LRU/LFU 通常是近似算法,不会维护全量精确排序。设置淘汰策略也不是容量治理的替代品,频繁驱逐会造成命中率下降和数据库压力。
高频追问与参考回答
追问:怎么判断正在发生严重淘汰?
观察 evicted_keys 增速、命中率、内存使用、写入错误和下游回源量,并关联实例 maxmemory 与业务流量变化。
追问:EXPIRE 会覆盖已有 TTL 吗?
通常会按命令语义更新过期时间,某些带条件选项的版本可限制仅在无 TTL、已有 TTL 等条件下生效。关键业务应明确使用的命令版本并写测试。
追问:删除一个大 Key 为什么会卡顿?
某些复杂类型释放大量元素需要较长时间,可能阻塞事件循环。可使用异步释放、渐进删除或按业务维度拆小 Key。
追问:缓存实例可以存永久 Key 吗?
可以,但在 allkeys 淘汰下它仍可能被淘汰;在 volatile 策略下它可能挤占内存却不成为候选。永久业务状态和缓存通常应分实例或至少分清可靠性边界。
总结
过期负责数据时效,淘汰负责内存压力;策略应结合实例用途、命中率和数据库承压能力选择。
机制全景图
下面把「Redis 过期删除和内存淘汰策略是什么?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。
flowchart LR
A["写入 Key 与过期时间"]
A --> B["惰性访问检查"]
B --> C["周期主动采样删除"]
C --> D["内存达到 maxmemory"]
D --> E["按策略淘汰并反馈"]
完整链路:从输入到结果
沿着「写入 Key 与过期时间 → 惰性访问检查 → 周期主动采样删除 → 内存达到 maxmemory → 按策略淘汰并反馈」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。
1. 写入 Key 与过期时间
EXPIRE 与数据写入应尽量在同一原子操作中完成,避免进程崩溃留下永不过期 Key。
2. 惰性访问检查
客户端访问 Key 时先检查 TTL,已过期则删除并返回不存在,这是惰性删除。
3. 周期主动采样删除
后台定期随机采样带 TTL Key,若过期比例高会继续循环,但不会逐键精确按时删除。
4. 内存达到 maxmemory
used_memory 超过 maxmemory 后按 noeviction、LRU/LFU、random、TTL 等策略选择候选。
5. 按策略淘汰并反馈
淘汰是容量保护而非业务过期,关键数据被驱逐后调用方必须有回源、降级或错误语义。
源码与实现定位
| 入口 | 阅读重点 |
|---|---|
| src/expire.c | activeExpireCycle 主动过期 |
| INFO stats/memory | expired_keys、evicted_keys |
源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。
参数配置与可复现实验
CONFIG SET maxmemory 4gb
CONFIG SET maxmemory-policy allkeys-lfu
写入 100 万个同秒 TTL 与带 20% 抖动 TTL,对比过期 CPU、延迟和回源峰值;再压满内存验证淘汰语义。
验证步骤与预期结果
1. 固定输入和基线
先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「expired/s」为主基线,记录值应满足「应平滑」;同时保存 expired_keys、evicted_keys,使后续变化能够回到同一时间轴比较。
2. 从实现入口确认路径
在「src/expire.c」确认请求确实进入「activeExpireCycle 主动过期」对应的实现,再沿「INFO stats/memory」观察「expired_keys、evicted_keys」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。
3. 注入本文特有的失败模式
优先复现「缓存与业务状态混用同一淘汰实例」,并把单一变量逐级放大,直到「expired/s」越过「整点尖峰」。随后再分别验证「过期时间无抖动集中触发」和「误把 TTL 到期当成实时定时器」,三类故障分开执行,避免多个变量同时变化而无法归因。
4. 执行止损和根因修复
第一轮只应用「TTL 加随机抖动」,确认它能控制影响范围;第二轮应用「状态数据用 noeviction 独立实例」,验证核心链路恢复;最后落实「回源加并发闸门」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。
5. 通过退出条件
实验只有同时满足三项才算通过:「expired/s」回到「应平滑」、「evicted_keys」回到「非缓存实例为 0」、「回源 QPS」回到「受闸门限制」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。
量化基线
| 指标 | 样例基线/口径 | 风险线 | 结论 |
|---|---|---|---|
| expired/s | 应平滑 | 整点尖峰 | TTL 集中 |
| evicted_keys | 非缓存实例为 0 | 持续增长 | 容量不足 |
| 回源 QPS | 受闸门限制 | 接近全量 | 雪崩 |
这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。
事故复盘:缓存集中失效导致数据库雪崩
百万 Key 使用同一个整点 TTL,主动过期与回源请求同时出现。写入 TTL 加随机抖动、热点逻辑过期并限制回源并发后,负载被摊平。
| 失败模式 | 首要证据 | 第一处置动作 |
|---|---|---|
| 缓存与业务状态混用同一淘汰实例 | expired_keys | TTL 加随机抖动 |
| 过期时间无抖动集中触发 | evicted_keys | 状态数据用 noeviction 独立实例 |
| 误把 TTL 到期当成实时定时器 | 内存水位 | 回源加并发闸门 |
发布与回滚检查点
- 发布前:确认「src/expire.c」对应实现和上述配置在目标版本仍然有效,并保存「expired/s」基线。
- 灰度中:同时观察 expired_keys、evicted_keys、内存水位;任一指标越过表中风险线,就停止继续扩量。
- 回滚时:先执行「TTL 加随机抖动」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
- 发布后:至少覆盖一个完整峰值周期,确认「缓存与业务状态混用同一淘汰实例」没有再次出现,才关闭变更观察窗口。
方案对比与选型
| 方案 | 更适合的场景 | 主要收益 | 代价与边界 |
|---|---|---|---|
| 惰性+定期过期 | 通用 TTL 数据 | Redis 自动平衡 CPU 与内存 | 删除时间不精确 |
| allkeys-lfu | 纯缓存且热点相对稳定 | 保留高频数据 | 频率估算并非业务优先级 |
| noeviction | 不能丢的状态数据 | 写失败显式暴露容量问题 | 达到上限后新写失败 |
选型至少带上 Key 数量、Value 大小、命令复杂度、热点分布和过期速率,并用上面的量化基线验证;未知数据应明确为待测假设。
设计边界与工程取舍
TTL 只保证访问时不会返回过期值,不保证物理删除时刻;需要精确定时业务应使用延迟队列或持久状态机。
工程落地遵循:Redis 是高性能数据结构服务,不应被当成没有容量边界的黑盒。回答时直接引用「src/expire.c」、配置实验和事故数据,比复述固定模板更有说服力。