📌PDF:大白话说Java面试题 — 07_Redis篇
第11题:Redis 的过期策略和内存淘汰策略
📚回答:
- 核心考点:Redis 的过期策略和内存淘汰策略是 Redis 内存管理的核心机制,大厂面试不会只问"有哪几种",而是深入考察惰性过期与定期过期的协同工作原理(Redis 源码级)、定期删除的随机采样算法(
activeExpireCycle)、LRU 的近似实现(lruclock与 24 位时钟)、LFU 的计数器衰减机制(logc与lfu_decay_time),以及生产环境中如何根据业务特征选型。面试官真正想判断的是:你是否理解 Redis 在"内存"和"CPU"之间的工程化权衡,以及能否针对具体业务场景给出合理的配置建议。
1. Redis 过期策略的本质
Redis 中设置了过期时间的 Key,其过期信息存储在过期字典(expires字典)中,Key 指向过期时间戳。Redis 不会立即删除所有过期 Key,而是采用惰性删除 + 定期删除的组合策略,在内存占用和 CPU 开销之间取得平衡。
2. 三种过期策略深度解析
2.1 定时过期(Time-Based Expiration)
原理:为每个设置了过期时间的 Key 创建一个定时器,到达过期时间时立即删除。
优点:内存友好,过期 Key 被立即释放,无内存残留。
缺点:- CPU 开销巨大:大量 Key 同时过期时,需要创建大量定时器,触发大量删除操作;
- 影响主线程:Redis 是单线程模型,定时删除会阻塞其他命令执行。
结论:Redis未采用纯定时过期策略,仅在特定场景(如过期 Key 被访问时)触发惰性删除。
2.2 惰性过期(Lazy Expiration / 被动删除)
原理:访问 Key 时,先检查是否过期。如果过期,则删除并返回空;如果未过期,则正常返回。
// Redis 源码逻辑(简化)robj*lookupKeyRead(redisDb*db,robj*key){robj*val=lookupKey(db,key);if(val&&expireIfNeeded(db,key)==1){// Key 已过期,被删除,返回 NULLreturnNULL;}returnval;}优点:
- CPU 友好:无需额外的定时扫描任务,删除操作只在访问时触发;
- 实现简单:无需维护定时器或扫描线程。
缺点:
- 内存不友好:长期未被访问的过期 Key 会一直占用内存("内存泄漏"风险);
- 批量过期问题:如果大量 Key 同时过期且长期不被访问,内存不会被释放。
适用场景:作为辅助策略,与定期删除配合使用。
2.3 定期过期(Active Expiration / 主动删除)
原理:Redis 每 100ms(
server.hz控制,默认 10)执行一次过期 Key 的随机采样删除。源码级实现(
activeExpireCycle):// Redis 5.0 源码(简化)voidactiveExpireCycle(inttype){// 每次随机从 expires 字典中采样 20 个 Key// 删除其中过期的 Key// 如果过期 Key 比例 > 25%,继续采样;否则结束for(j=0;j<dbs_per_call;j++){redisDb*db=server.db+(current_db%server.dbnum);current_db++;// 采样 20 个 Keydo{de=dictGetRandomKey(db->expires);if(de==NULL)break;ttl=getExpire(db,dictGetKey(de));if(activeExpireCycleTryExpire(db,de,now))expired++;if((iteration&0xf)==0){// 每 16 次迭代检查一次时间,防止阻塞过久elapsed=ustime()-start;if(elapsed>timelimit)timelimit_exit=1;}}while(expired<ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4&&!timelimit_exit);}}关键参数:
参数 默认值 说明 server.hz10 每秒执行 serverCron的次数(即每 100ms 一次)ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP20 每次采样 Key 的数量 过期比例阈值 25% 如果过期 Key 比例 > 25%,继续采样;否则结束 时间上限 25ms 每次 activeExpireCycle的最大执行时间执行流程:
每 100ms 触发一次 serverCron │ ▼ ┌─────────────────────────────┐ │ activeExpireCycle │ │ 1. 随机采样 20 个 Key │ │ 2. 删除过期 Key │ │ 3. 检查过期比例 > 25%? │ │ ├─ 是 → 继续采样 │ │ └─ 否 → 结束 │ │ 4. 检查执行时间 > 25ms? │ │ ├─ 是 → 强制结束 │ │ └─ 否 → 继续 │ └─────────────────────────────┘优点:
- 平衡 CPU 和内存:通过随机采样和比例控制,避免一次性删除大量 Key 导致阻塞;
- 自适应:过期 Key 多时多删,少时少删,动态调整。
缺点:
- 清理不及时:如果过期 Key 数量远大于采样数量,部分过期 Key 可能长期残留;
- 内存峰值:大量 Key 同时过期时,定期删除无法及时清理,内存可能短暂飙升。
适用场景:作为主策略,与惰性删除配合,构成 Redis 的过期策略体系。
2.4 三种策略对比
策略 触发时机 CPU 开销 内存友好度 阻塞风险 Redis 采用 定时过期 定时器触发 极高 ⭐⭐⭐⭐⭐ 高(单线程) ❌ 未采用 惰性过期 访问 Key 时 低 ⭐⭐ 无 ✅ 辅助 定期过期 每 100ms 采样 中 ⭐⭐⭐⭐ 低(时间上限保护) ✅ 主策略 2.5 Redis 的实际过期策略Redis 采用“定期过期为主 + 惰性过期为辅”的组合策略:
- 定期过期:每 100ms 主动采样删除,控制内存增长;
- 惰性过期:访问时检查并删除,确保过期 Key 不会返回给客户端。
重要认知:Redis 的过期删除是概率性的,不是实时的。即使 Key 已过期,如果未被访问且未被定期采样到,仍可能短暂存在。
3. 内存淘汰策略深度解析
当 Redis 内存达到maxmemory上限时,需要淘汰部分 Key 以释放内存。Redis 提供 8 种淘汰策略(Redis 4.0+)。
3.1 策略分类
策略 范围 淘汰逻辑 适用场景 noeviction全部 Key 不淘汰,直接返回错误 禁止淘汰(不推荐生产) allkeys-lru全部 Key LRU 淘汰最近最少使用 通用首选 allkeys-random全部 Key 随机淘汰 数据无热点 allkeys-lfu全部 Key LFU 淘汰最少使用频率 热点数据差异大 volatile-lru有过期时间的 Key LRU 淘汰 需保留永久 Key volatile-random有过期时间的 Key 随机淘汰 数据无热点 volatile-ttl有过期时间的 Key 淘汰 TTL 最短的 需优先释放即将过期数据 volatile-lfu有过期时间的 Key LFU 淘汰 热点数据差异大 3.2 LRU 近似实现
传统 LRU 的问题:需要维护一个双向链表,每次访问时移动节点,时间复杂度 O(1) 但内存开销大,且 Redis 单线程下频繁移动节点影响性能。
Redis 的近似 LRU:
- 每个对象(
robj)的lru字段(24 位)记录最后一次访问时间(Redis 全局时钟的低 24 位,精度约 1 分钟); - 淘汰时,随机采样 N 个 Key(默认 5 个,
maxmemory-samples配置),选择lru值最小(最久未访问)的 Key 淘汰。
// Redis 对象结构(简化)structredisObject{unsignedtype:4;// 数据类型unsignedencoding:4;// 编码方式unsignedlru:LRU_BITS;// 24 位 LRU 时钟(Redis 3.0+)intrefcount;// 引用计数void*ptr;// 数据指针};近似 LRU 的误差:
- 采样数量少(默认 5 个),可能错过真正的最久未访问 Key;
- 增大
maxmemory-samples(如 10)可提高精度,但增加 CPU 开销。
maxmemory-samples近似度 CPU 开销 建议 3 低 低 测试环境 5(默认) 中 中 大多数场景 10 高 高 内存敏感场景 - 每个对象(
3.3 LFU 实现(Redis 4.0+)
核心思想:不仅考虑"多久没访问",还考虑"访问频率"。适合访问模式变化大的场景(如某些 Key 曾经很热门,现在冷门)。
数据结构:Redis 4.0 后,
lru字段复用为 LFU 计数器:lru 字段(24 位)拆分: ┌─────────────┬─────────────────┐ │ 高 16 位 │ 低 8 位 │ │ logc 计数器 │ ldt 最后访问时间 │ └─────────────┴─────────────────┘字段 位数 说明 logc(Logistic Counter)16 位 访问计数器,非线性增长(新访问+1,旧访问+更少) ldt(Last Decrement Time)8 位 最后访问时间(分钟精度),用于衰减计算 计数器增长规则:
- 每次访问时,
logc增加,但增长量随当前值增大而减小(非线性):logc较小时,每次访问 +1;logc较大时,每次访问 +0.1 或更少。
- 最大值为 255。
衰减机制:
- 每隔
lfu-decay-time分钟(默认 1 分钟),logc衰减一次:logc = logc - (当前时间 - ldt) / lfu_decay_time
- 长时间未访问的 Key,
logc逐渐衰减,最终被淘汰。
LFU vs LRU 对比:
维度 LRU LFU 核心指标 最后访问时间 访问频率 适用场景 访问模式稳定(热点长期不变) 访问模式变化大(热点会转移) 内存开销 24 位时间戳 16 位计数器 + 8 位时间 计算复杂度 低 中(需衰减计算) 典型业务 商品详情页、用户资料 新闻推荐、热搜榜单 - 每次访问时,
3.4 volatile-ttl 策略
原理:从设置了过期时间的 Key 中,优先淘汰 TTL(剩余存活时间)最短的 Key。
适用场景:
- 缓存数据有明确的业务过期时间(如验证码 5 分钟、Token 30 分钟);
- 希望优先释放即将过期的数据,减少内存浪费。
注意:如果所有 Key 都没有设置过期时间,
volatile-ttl不会淘汰任何 Key,可能导致内存溢出。
4. 生产环境配置建议
4.1 过期策略配置
场景 建议 大量 Key 同时过期 分散过期时间( EXPIRE key seconds + random(0, 60)),避免雪崩内存敏感 监控 expired_keys指标,确保定期删除效率长冷数据 设置合理的过期时间,避免长期占用内存 4.2 淘汰策略配置
业务特征 推荐策略 理由 通用缓存(读多写少) allkeys-lru保留热点数据,淘汰冷数据 热点数据变化频繁 allkeys-lfu频率优先,适应热点转移 需保留永久 Key volatile-lru只淘汰有过期时间的 Key 数据有明确生命周期 volatile-ttl优先释放即将过期数据 数据无热点 allkeys-random简单随机,避免 LRU 误差 4.3 关键配置参数
# redis.conf maxmemory 4gb # 最大内存上限 maxmemory-policy allkeys-lru # 淘汰策略 maxmemory-samples 10 # LRU/LFU 采样数量(提高精度) hz 10 # serverCron 执行频率(默认 10)
5. 生产环境避坑指南
5.1 大量 Key 同时过期的内存飙升如果 100 万个 Key 同时设置 1 小时过期,1 小时后定期删除的采样速度(每 100ms 采样 20 个,每秒 200 个)远跟不上过期速度,内存会短暂飙升。解决方案:
- 分散过期时间:
EXPIRE key 3600 + random(0, 300); - 监控
used_memory和expired_keys,设置告警。
- 分散过期时间:
5.2
noeviction策略的生产风险默认策略(Redis 3.0 前)是noeviction,内存满后写入直接报错。生产环境必须显式设置淘汰策略。5.3 LFU 的
logc饱和问题logc最大值为 255,如果某个 Key 被频繁访问达到饱和,后续访问不再增加计数器。Redis 通过非线性增长缓解,但极端场景下仍需监控。5.4 大 Key 的淘汰风险如果某个大 Key(如 100MB 的 Hash)被 LRU 淘汰,瞬间释放大量内存,可能导致 Redis 阻塞(删除大 Key 是同步操作)。建议:
- 避免大 Key,拆分为多个小 Key;
- 使用
UNLINK(Redis 4.0+,异步删除)替代DEL。
5.5 监控指标
expired_keys:每秒过期 Key 数量,监控定期删除效率;evicted_keys:每秒淘汰 Key 数量,监控内存压力;used_memory:当前内存使用量;used_memory_rss:操作系统视角的内存占用(含内存碎片)。
6. 面试官追问与高分回答模板
追问 1:“Redis 的过期策略有哪些?为什么不用定时过期?”
低分回答:“有定时过期、惰性过期和定期过期。定时过期 CPU 开销大,所以不用。”(没有源码级细节)
高分回答:
"Redis 的过期策略分三种:
- 定时过期:为每个 Key 创建定时器,到期立即删除。优点是内存友好,但 Redis 是单线程模型,大量定时器触发会阻塞主线程,CPU 开销巨大,因此 Redis未采用纯定时过期。
- 惰性过期:访问 Key 时检查是否过期,过期则删除。优点是 CPU 友好,但长期不访问的过期 Key 会残留内存,导致’内存泄漏’。
- 定期过期:每 100ms(
server.hz控制)执行activeExpireCycle,随机采样 20 个 Key 删除过期项。如果过期比例 > 25% 且执行时间 < 25ms,继续采样。优点是平衡 CPU 和内存,但清理是概率性的,不及时。
Redis 实际采用‘定期过期为主 + 惰性过期为辅’的组合策略。"
追问 2:“定期过期是怎么实现的?采样数量是多少?”
高分回答:
"定期过期由
serverCron每 100ms 触发一次activeExpireCycle函数实现:- 每次从过期字典中随机采样 20 个 Key(
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP); - 删除其中已过期的 Key;
- 如果过期 Key 比例> 25%,继续采样;否则结束本次循环;
- 如果执行时间> 25ms,强制结束,防止阻塞主线程。
这个设计的核心思想是’自适应采样’:过期 Key 多时多删,少时少删,通过时间上限保护避免阻塞。但这也意味着如果过期 Key 数量远大于采样速度,部分过期 Key 会短暂残留。"
- 每次从过期字典中随机采样 20 个 Key(
追问 3:“LRU 和 LFU 有什么区别?Redis 是怎么实现的?”
高分回答:
"LRU(Least Recently Used)和 LFU(Least Frequently Used)的核心区别在于淘汰指标:
- LRU:以’最后访问时间’为指标,淘汰最久未访问的 Key。适合访问模式稳定的场景(如商品详情页长期热门)。
- LFU:以’访问频率’为指标,淘汰访问次数最少的 Key。适合访问模式变化大的场景(如新闻推荐、热搜榜单)。
Redis 的实现: - LRU:每个对象的
lru字段(24 位)记录最后访问时间戳。淘汰时随机采样 N 个 Key(默认 5 个),选择lru最小的淘汰。这是近似 LRU,不是精确的,但性能更高。 - LFU(Redis 4.0+):复用
lru字段,高 16 位为logc(访问计数器,非线性增长),低 8 位为ldt(最后访问时间)。每隔lfu-decay-time分钟,logc衰减一次。淘汰时选择logc最小的 Key。
生产环境建议:通用场景用allkeys-lru,热点变化频繁的场景用allkeys-lfu。"
追问 4:“如果 Redis 内存满了,写入会报错吗?”
高分回答:
"取决于
maxmemory-policy配置:noeviction(默认):内存满后写入直接返回错误(OOM command not allowed),读操作正常。allkeys-lru/allkeys-lfu/allkeys-random:从所有 Key 中按策略淘汰,释放内存后写入成功。volatile-lru/volatile-lfu/volatile-random/volatile-ttl:只从设置了过期时间的 Key 中淘汰。
生产环境严禁使用noeviction,必须显式配置淘汰策略。如果业务需要保留永久 Key,使用volatile-xxx策略;否则使用allkeys-lru或allkeys-lfu。"
追问 5:“大量 Key 同时过期会导致什么问题?怎么解决?”
高分回答:
"大量 Key 同时过期会导致两个问题:
- 内存飙升:定期删除的采样速度(每秒 200 个)跟不上过期速度,过期 Key 短暂残留,内存占用突增;
- CPU 飙升:
activeExpireCycle检测到高过期比例后持续采样,占用大量 CPU,阻塞主线程。
解决方案: - 分散过期时间:设置过期时间时增加随机偏移量,如
EXPIRE key 3600 + random(0, 300),将过期分散到 5 分钟内; - 监控告警:监控
expired_keys和used_memory,突增时触发告警; - 懒加载:避免批量写入时统一设置过期时间,改为按需设置。"
追问 6:“Redis 的大 Key 删除会阻塞吗?怎么优化?”
高分回答:
"Redis 单线程模型下,
DEL大 Key(如 100MB 的 Hash 或 List)是同步操作,会阻塞主线程,导致其他命令延迟飙升。
优化方案:- 避免大 Key:设计时将大 Key 拆分为多个小 Key(如将大 List 拆分为每 1000 个元素一个 Key);
- 异步删除:Redis 4.0+ 提供
UNLINK命令,将删除操作放入后台线程异步执行,不阻塞主线程; - 渐进式删除:对 Hash/Set/ZSet 使用
HSCAN/SSCAN/ZSCAN分批删除元素,最后删除 Key; - 内存淘汰时的大 Key 风险:如果大 Key 被 LRU/LFU 淘汰,同步删除会阻塞。建议配合
UNLINK或避免大 Key。"
7. 方案选型速查表
| 业务场景 | 推荐淘汰策略 | 过期时间设置 | 注意事项 |
|---|---|---|---|
| 通用缓存(商品详情、用户资料) | allkeys-lru | 固定 TTL + 随机偏移 | 监控内存使用率 |
| 热点变化频繁(新闻、热搜) | allkeys-lfu | 固定 TTL + 随机偏移 | 关注logc衰减 |
| 需保留永久配置数据 | volatile-lru | 业务数据设 TTL | 永久 Key 不设置 TTL |
| 数据生命周期明确(验证码、Token) | volatile-ttl | 精确 TTL | 避免全部同时过期 |
| 内存极度敏感 | allkeys-lru+maxmemory-samples 10 | 分散过期 | 提高 LRU 精度 |
| 大数据量、低访问频率 | allkeys-random | 固定 TTL | 简单随机,避免 LRU 误差 |
💡面试官想要的满分总结:
Redis 的过期策略和内存淘汰策略,本质是在内存占用和 CPU 开销之间的工程化权衡。
过期策略采用"定期过期为主 + 惰性过期为辅":每 100ms 的
activeExpireCycle随机采样 20 个 Key,过期比例 > 25% 且时间 < 25ms 时继续采样;访问时惰性检查删除。这种设计避免了定时过期的 CPU 灾难,也缓解了惰性过期的内存泄漏。内存淘汰策略中,
allkeys-lru是通用首选(近似 LRU,采样 5 个 Key),热点变化频繁的场景用allkeys-lfu(16 位计数器 + 衰减机制)。生产环境严禁noeviction,必须显式配置淘汰策略。生产避坑要点:分散过期时间防止雪崩、避免大 Key 同步删除(用
UNLINK)、监控expired_keys和evicted_keys、根据业务特征调整maxmemory-samples。最后记住:Redis 的过期删除是概率性的,不是实时的。设计缓存架构时,不能假设 Key 过期后立即被删除,必须考虑短暂的残留窗口。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯