【大白话说Java面试题 第175题】【07_Redis篇】第11题:Redis 的过期策略和内存淘汰策略

📌PDF:大白话说Java面试题 — 07_Redis篇

第11题:Redis 的过期策略和内存淘汰策略

📚回答:

  • 核心考点Redis 的过期策略内存淘汰策略是 Redis 内存管理的核心机制,大厂面试不会只问"有哪几种",而是深入考察惰性过期与定期过期的协同工作原理(Redis 源码级)、定期删除的随机采样算法activeExpireCycle)、LRU 的近似实现lruclock与 24 位时钟)、LFU 的计数器衰减机制logclfu_decay_time),以及生产环境中如何根据业务特征选型。面试官真正想判断的是:你是否理解 Redis 在"内存"和"CPU"之间的工程化权衡,以及能否针对具体业务场景给出合理的配置建议。

1. Redis 过期策略的本质

Redis 中设置了过期时间的 Key,其过期信息存储在过期字典expires字典)中,Key 指向过期时间戳。Redis 不会立即删除所有过期 Key,而是采用惰性删除 + 定期删除的组合策略,在内存占用和 CPU 开销之间取得平衡。


2. 三种过期策略深度解析
  • 2.1 定时过期(Time-Based Expiration)

    原理:为每个设置了过期时间的 Key 创建一个定时器,到达过期时间时立即删除。

    优点:内存友好,过期 Key 被立即释放,无内存残留。
    缺点

    1. CPU 开销巨大:大量 Key 同时过期时,需要创建大量定时器,触发大量删除操作;
    2. 影响主线程: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;}

    优点

    1. CPU 友好:无需额外的定时扫描任务,删除操作只在访问时触发;
    2. 实现简单:无需维护定时器或扫描线程。

    缺点

    1. 内存不友好:长期未被访问的过期 Key 会一直占用内存("内存泄漏"风险);
    2. 批量过期问题:如果大量 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? │ │ ├─ 是 → 强制结束 │ │ └─ 否 → 继续 │ └─────────────────────────────┘

    优点

    1. 平衡 CPU 和内存:通过随机采样和比例控制,避免一次性删除大量 Key 导致阻塞;
    2. 自适应:过期 Key 多时多删,少时少删,动态调整。

    缺点

    1. 清理不及时:如果过期 Key 数量远大于采样数量,部分过期 Key 可能长期残留;
    2. 内存峰值:大量 Key 同时过期时,定期删除无法及时清理,内存可能短暂飙升。

    适用场景:作为主策略,与惰性删除配合,构成 Redis 的过期策略体系。

  • 2.4 三种策略对比

    策略触发时机CPU 开销内存友好度阻塞风险Redis 采用
    定时过期定时器触发极高⭐⭐⭐⭐⭐高(单线程)❌ 未采用
    惰性过期访问 Key 时⭐⭐✅ 辅助
    定期过期每 100ms 采样⭐⭐⭐⭐低(时间上限保护)✅ 主策略
  • 2.5 Redis 的实际过期策略Redis 采用“定期过期为主 + 惰性过期为辅”的组合策略:

    1. 定期过期:每 100ms 主动采样删除,控制内存增长;
    2. 惰性过期:访问时检查并删除,确保过期 Key 不会返回给客户端。

    重要认知:Redis 的过期删除是概率性的,不是实时的。即使 Key 已过期,如果未被访问且未被定期采样到,仍可能短暂存在。


3. 内存淘汰策略深度解析

当 Redis 内存达到maxmemory上限时,需要淘汰部分 Key 以释放内存。Redis 提供 8 种淘汰策略(Redis 4.0+)。

  • 3.1 策略分类

    策略范围淘汰逻辑适用场景
    noeviction全部 Key不淘汰,直接返回错误禁止淘汰(不推荐生产)
    allkeys-lru全部 KeyLRU 淘汰最近最少使用通用首选
    allkeys-random全部 Key随机淘汰数据无热点
    allkeys-lfu全部 KeyLFU 淘汰最少使用频率热点数据差异大
    volatile-lru有过期时间的 KeyLRU 淘汰需保留永久 Key
    volatile-random有过期时间的 Key随机淘汰数据无热点
    volatile-ttl有过期时间的 Key淘汰 TTL 最短的需优先释放即将过期数据
    volatile-lfu有过期时间的 KeyLFU 淘汰热点数据差异大
  • 3.2 LRU 近似实现

    传统 LRU 的问题:需要维护一个双向链表,每次访问时移动节点,时间复杂度 O(1) 但内存开销大,且 Redis 单线程下频繁移动节点影响性能。

    Redis 的近似 LRU

    1. 每个对象(robj)的lru字段(24 位)记录最后一次访问时间(Redis 全局时钟的低 24 位,精度约 1 分钟);
    2. 淘汰时,随机采样 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 对比

    维度LRULFU
    核心指标最后访问时间访问频率
    适用场景访问模式稳定(热点长期不变)访问模式变化大(热点会转移)
    内存开销24 位时间戳16 位计数器 + 8 位时间
    计算复杂度中(需衰减计算)
    典型业务商品详情页、用户资料新闻推荐、热搜榜单
  • 3.4 volatile-ttl 策略

    原理:从设置了过期时间的 Key 中,优先淘汰 TTL(剩余存活时间)最短的 Key。

    适用场景

    1. 缓存数据有明确的业务过期时间(如验证码 5 分钟、Token 30 分钟);
    2. 希望优先释放即将过期的数据,减少内存浪费。

    注意:如果所有 Key 都没有设置过期时间,volatile-ttl不会淘汰任何 Key,可能导致内存溢出。


4. 生产环境配置建议
  • 4.1 过期策略配置

    场景建议
    大量 Key 同时过期分散过期时间(EXPIRE key seconds + random(0, 60)),避免雪崩
    内存敏感监控expired_keys指标,确保定期删除效率
    长冷数据设置合理的过期时间,避免长期占用内存
  • 4.2 淘汰策略配置

    业务特征推荐策略理由
    通用缓存(读多写少)allkeys-lru保留热点数据,淘汰冷数据
    热点数据变化频繁allkeys-lfu频率优先,适应热点转移
    需保留永久 Keyvolatile-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 个)远跟不上过期速度,内存会短暂飙升。解决方案:

    1. 分散过期时间:EXPIRE key 3600 + random(0, 300)
    2. 监控used_memoryexpired_keys,设置告警。
  • 5.2noeviction策略的生产风险默认策略(Redis 3.0 前)是noeviction,内存满后写入直接报错。生产环境必须显式设置淘汰策略。

  • 5.3 LFU 的logc饱和问题logc最大值为 255,如果某个 Key 被频繁访问达到饱和,后续访问不再增加计数器。Redis 通过非线性增长缓解,但极端场景下仍需监控。

  • 5.4 大 Key 的淘汰风险如果某个大 Key(如 100MB 的 Hash)被 LRU 淘汰,瞬间释放大量内存,可能导致 Redis 阻塞(删除大 Key 是同步操作)。建议:

    1. 避免大 Key,拆分为多个小 Key;
    2. 使用UNLINK(Redis 4.0+,异步删除)替代DEL
  • 5.5 监控指标

    • expired_keys:每秒过期 Key 数量,监控定期删除效率;
    • evicted_keys:每秒淘汰 Key 数量,监控内存压力;
    • used_memory:当前内存使用量;
    • used_memory_rss:操作系统视角的内存占用(含内存碎片)。

6. 面试官追问与高分回答模板
  • 追问 1:“Redis 的过期策略有哪些?为什么不用定时过期?”

    低分回答:“有定时过期、惰性过期和定期过期。定时过期 CPU 开销大,所以不用。”(没有源码级细节)

    高分回答

    "Redis 的过期策略分三种:

    1. 定时过期:为每个 Key 创建定时器,到期立即删除。优点是内存友好,但 Redis 是单线程模型,大量定时器触发会阻塞主线程,CPU 开销巨大,因此 Redis未采用纯定时过期。
    2. 惰性过期:访问 Key 时检查是否过期,过期则删除。优点是 CPU 友好,但长期不访问的过期 Key 会残留内存,导致’内存泄漏’。
    3. 定期过期:每 100ms(server.hz控制)执行activeExpireCycle,随机采样 20 个 Key 删除过期项。如果过期比例 > 25% 且执行时间 < 25ms,继续采样。优点是平衡 CPU 和内存,但清理是概率性的,不及时。
      Redis 实际采用‘定期过期为主 + 惰性过期为辅’的组合策略。"
  • 追问 2:“定期过期是怎么实现的?采样数量是多少?”

    高分回答

    "定期过期由serverCron每 100ms 触发一次activeExpireCycle函数实现:

    1. 每次从过期字典中随机采样 20 个 KeyACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP);
    2. 删除其中已过期的 Key;
    3. 如果过期 Key 比例> 25%,继续采样;否则结束本次循环;
    4. 如果执行时间> 25ms,强制结束,防止阻塞主线程。
      这个设计的核心思想是’自适应采样’:过期 Key 多时多删,少时少删,通过时间上限保护避免阻塞。但这也意味着如果过期 Key 数量远大于采样速度,部分过期 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-lruallkeys-lfu。"
  • 追问 5:“大量 Key 同时过期会导致什么问题?怎么解决?”

    高分回答

    "大量 Key 同时过期会导致两个问题:

    1. 内存飙升:定期删除的采样速度(每秒 200 个)跟不上过期速度,过期 Key 短暂残留,内存占用突增;
    2. CPU 飙升activeExpireCycle检测到高过期比例后持续采样,占用大量 CPU,阻塞主线程。
      解决方案:
    3. 分散过期时间:设置过期时间时增加随机偏移量,如EXPIRE key 3600 + random(0, 300),将过期分散到 5 分钟内;
    4. 监控告警:监控expired_keysused_memory,突增时触发告警;
    5. 懒加载:避免批量写入时统一设置过期时间,改为按需设置。"
  • 追问 6:“Redis 的大 Key 删除会阻塞吗?怎么优化?”

    高分回答

    "Redis 单线程模型下,DEL大 Key(如 100MB 的 Hash 或 List)是同步操作,会阻塞主线程,导致其他命令延迟飙升。
    优化方案:

    1. 避免大 Key:设计时将大 Key 拆分为多个小 Key(如将大 List 拆分为每 1000 个元素一个 Key);
    2. 异步删除:Redis 4.0+ 提供UNLINK命令,将删除操作放入后台线程异步执行,不阻塞主线程;
    3. 渐进式删除:对 Hash/Set/ZSet 使用HSCAN/SSCAN/ZSCAN分批删除元素,最后删除 Key;
    4. 内存淘汰时的大 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_keysevicted_keys、根据业务特征调整maxmemory-samples

最后记住:Redis 的过期删除是概率性的,不是实时的。设计缓存架构时,不能假设 Key 过期后立即被删除,必须考虑短暂的残留窗口。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯