Redis——过期键删除、内存淘汰、LRU/LFU实现
一、核心概念前置区分(90%使用者的混淆点)
| 特性 | 过期键删除策略 | 内存淘汰策略 |
|---|---|---|
| 核心目标 | 清理已到过期时间的键,释放无效内存 | 当Redis内存达到maxmemory上限时,主动选键删除,保障写入可用 |
| 触发条件 | 键设置了过期时间且已到期,与内存使用率无强制关联 | Redis已用内存 ≥ 配置的maxmemory上限,与键是否过期无强制关联 |
| 执行优先级 | 前置执行,是内存管控的第一道防线 | 后置兜底,过期键清理后内存仍超限才会触发 |
| 处理范围 | 仅处理设置了过期时间的键 | 可配置为处理全量键/仅过期键 |
二、Redis 过期键删除策略
2.1 过期键的底层存储
Redis在每个数据库redisDb结构中,通过expires字典专门存储键的过期时间:
- 字典key:指向数据库键的指针(无额外内存冗余)
- 字典value:键的过期时间戳(毫秒级Unix时间戳)
- 优势:判断键是否过期、获取过期时间的时间复杂度为O(1)
2.2 行业通用的3种过期删除策略
| 策略 | 核心逻辑 | 优点 | 核心缺陷 |
|---|---|---|---|
| 定时删除 | 设置过期时间时同步创建定时器,到期立即删除键 | 内存极致友好,到期立即释放内存 | CPU极不友好,大量过期键会占用主线程CPU,严重影响Redis响应性能 |
| 惰性删除 | 键到期不主动删除,仅当客户端访问该键时,检查并删除过期键 | CPU极致友好,仅访问时才做处理,无额外CPU开销 | 内存极不友好,大量冷过期键永不被访问,造成内存泄漏 |
| 定期删除 | 每隔固定时间,抽取部分过期键检查,删除其中已到期的键 | 折中CPU与内存,可通过配置调整执行频率和力度 | 难以精准平衡,频率过高耗CPU,频率过低内存泄漏 |
2.3 Redis 实际采用的组合策略(惰性删除 + 定期删除)
Redis单线程架构决定了无法使用定时删除,最终采用惰性删除为基础、定期删除为兜底的组合方案,平衡CPU与内存开销。
2.3.1 惰性删除实现细节
- 触发时机:所有访问键的命令(
GET/MGET/HGET等)执行前,都会调用expireIfNeeded函数 - 核心执行逻辑:
- 检查键是否存在于
expires字典,无过期时间直接放行 - 检查键是否已过期,未过期直接放行
- 已过期:主库同步/异步删除键(4.0+
lazyfree机制对大键异步删除,避免主线程阻塞),并向AOF/从库追加DEL命令,返回nil
- 检查键是否存在于
- 主从一致性约束:从库永远不会主动执行惰性删除,即使键已过期,访问时也仅返回过期状态,必须等待主库同步
DEL命令后才会删除(3.2+版本从库访问过期键会返回nil,但仍不执行删除)
2.3.2 定期删除实现细节
- 触发频率:由
redis.conf的hz参数控制,默认10,即每秒执行10次,单次间隔100ms;取值范围1-500,值越高清理越及时,CPU开销越大 - 自适应执行逻辑:
- 遍历0-15号数据库,逐个库从
expires字典随机抽取20个带过期时间的键 - 检查并删除抽取结果中已过期的键
- 若本次抽取的键中,过期占比超过25%,立即重复抽取-检查-删除流程
- 单次执行设置硬时间上限:不超过当前
hz周期的25%(如hz=10时,单次最多执行25ms),避免阻塞主线程 - 内存紧张时自动进入慢模式,提升清理频率和时长
- 遍历0-15号数据库,逐个库从
2.4 特殊场景下的过期键处理
- RDB持久化:生成RDB文件时,会跳过所有已过期的键;载入RDB时,主库跳过过期键,从库全量载入,等待主库同步
DEL命令 - AOF持久化:键过期被删除时,会向AOF文件追加一条
DEL命令;AOF重写时,会跳过所有已过期的键 - 集群模式:仅主节点执行过期键删除,从节点完全同步主节点的
DEL命令,保障集群数据一致性
三、Redis 内存淘汰8种策略全解
3.1 内存淘汰的触发条件与核心配置
- 触发条件:客户端发送写入类命令时,Redis检查已用内存,若已用内存 ≥
maxmemory配置上限,则触发内存淘汰流程 - 核心配置:
maxmemory:Redis最大可用内存上限,单位字节,生产环境建议设置为物理内存的50%-70%,避免使用swap导致性能暴跌maxmemory-policy:内存淘汰策略配置,默认noevictionmaxmemory-samples:淘汰算法采样数,默认5,值越大淘汰越精准,CPU开销越高
3.2 8种淘汰策略的结构化分类与详解
8种策略可分为1种不淘汰策略 + 7种淘汰策略,淘汰策略按「淘汰范围」+「淘汰算法」两个维度组合划分。
3.2.1 不淘汰类策略(1种)
noeviction(默认策略)
- 核心逻辑:内存达到
maxmemory时,所有会导致内存增加的写入命令(SET/INCR/LPUSH等)全部返回OOM错误,仅只读命令(GET/INFO等)可正常执行 - 适用场景:绝对不允许数据丢失、有独立的降级容灾策略、仅提供只读服务的场景
3.2.2 全键空间淘汰类策略(3种)
淘汰范围:数据库中所有键(无论是否设置过期时间、是否已过期),优先级最高,无键可淘汰时降级为noeviction。
| 策略 | 核心逻辑 | 适用场景 |
|---|---|---|
allkeys-lru |
优先淘汰最近最少使用的键(近似LRU算法) | 业务有明显冷热数据区分,80%请求集中在20%热点数据,生产环境最常用的策略 |
allkeys-lfu |
优先淘汰访问频率最低的键(近似LFU算法,4.0+新增) | 业务数据访问频率差异极大,如电商爆款、热点新闻,需精准保留高频访问数据,避免冷数据污染缓存 |
allkeys-random |
完全随机淘汰任意键 | 所有键访问概率完全均等的特殊场景,生产环境极少使用 |
3.2.3 过期键空间淘汰类策略(4种)
淘汰范围:仅针对设置了过期时间的键(无论是否已到期),若当前库无带过期时间的键,直接降级为noeviction(高频踩坑点)。
| 策略 | 核心逻辑 | 适用场景 |
|---|---|---|
volatile-lru |
仅在过期键中,优先淘汰最近最少使用的键 | 需要永久保留核心数据(如配置、用户基础信息),仅允许淘汰带过期时间的缓存数据 |
volatile-lfu |
仅在过期键中,优先淘汰访问频率最低的键(4.0+新增) | 既要保留永久核心数据,又需精准淘汰低频访问的缓存数据 |
volatile-random |
仅在过期键中,完全随机淘汰键 | 极少使用,仅适用于过期键访问概率完全均等的场景 |
volatile-ttl |
仅在过期键中,优先淘汰剩余TTL最短的键 | 业务有明确的过期优先级,希望优先淘汰即将到期的键 |
3.3 高频踩坑点与策略选型建议
- 高频踩坑点
- 使用
volatile-xxx系列策略时,若库中无带过期时间的键,内存超限后会直接拒绝所有写入 - 从库不会主动执行内存淘汰,必须与主库配置相同的
maxmemory,否则会出现从库OOM - 未配置
maxmemory时,32位Redis默认最大内存3GB,64位Redis无上限,会耗尽服务器内存触发OOM
- 使用
- 选型优先级建议
- 首选:
allkeys-lru,绝大多数有冷热区分的业务场景最优解 - 次选:
allkeys-lfu,热点数据集中、访问频率差异大的场景 - 特殊场景:需保留永久键时,选择
volatile-lru/volatile-lfu - 非特殊需求,禁止使用
random/ttl/noeviction策略
- 首选:
四、Redis LRU/LFU 算法深度实现
4.1 核心载体:RedisObject 结构
Redis中每个键值对的value都封装为redisObject,其中24位的lru字段是LRU/LFU算法的核心载体,无需额外数据结构,极致节省内存:
typedef struct redisObject {
unsigned type:4; // 数据类型
unsigned encoding:4; // 编码格式
unsigned lru:24; // LRU时间戳 / LFU的ldt+logc
int refcount; // 引用计数
void *ptr; // 指向实际数据的指针
} robj;
4.2 LRU(Least Recently Used,最近最少使用)算法实现
4.2.1 标准LRU算法原理与缺陷
- 核心原理:最近被访问的数据,未来被访问的概率更高;内存不足时,淘汰最久未被访问的数据
- 标准实现:哈希表 + 双向链表,哈希表记录键与链表节点的映射,链表按访问时间排序,访问时将节点移至表头,淘汰时删除表尾节点
- 核心缺陷:
- 额外内存开销大,需维护双向链表
- 频繁的链表节点移动操作,单线程下CPU开销高
- 缓存污染:一次性批量访问的冷数据,会挤出长期热点数据,导致缓存命中率暴跌
4.2.2 Redis 近似LRU实现原理
Redis放弃了严格LRU,采用随机采样+候选池的近似LRU方案,在性能和命中率之间做到极致平衡。
- 核心基础:
redisObject的24位lru字段,存储键最后一次被访问的秒级Unix时间戳(24位可覆盖194天,循环使用) - 执行流程(3.0+优化版):
- 内存超限触发淘汰时,按
maxmemory-samples配置随机采样对应数量的键 - 维护一个大小为16的候选池,池内按键的
lru时间升序排序(最久未访问的在前) - 采样的键仅当
lru时间早于池内最小时间,或池未满时,才会被加入候选池 - 从候选池中选出
lru时间最早的键,执行淘汰 - 重复流程,直到内存降至
maxmemory以下
- 内存超限触发淘汰时,按
- 核心优势:无需额外数据结构,内存开销极低;无链表操作,CPU开销极小;
maxmemory-samples=10时,命中率已与标准LRU几乎一致
4.2.3 核心配置与优化细节
maxmemory-samples:默认5,生产环境追求命中率可调整至10,不建议超过10,避免CPU开销激增- 访问优化:键的
lru时间与当前时间差小于1秒时,不会更新lru字段,避免频繁更新带来的CPU开销 - 3.0+候选池优化:解决了旧版本单次采样的局限性,实现跨轮次的键对比,大幅提升淘汰精准度,缓解缓存污染问题
4.3 LFU(Least Frequently Used,最不经常使用)算法实现
4.3.1 标准LFU算法原理与缺陷
- 核心原理:访问频率越高的数据,未来被访问的概率越高;内存不足时,淘汰访问次数最少的键
- 标准实现:为每个键维护访问计数器,访问时计数+1,淘汰时删除计数最小的键
- 核心缺陷:
- 内存开销大,需为每个键维护计数器
- 无法适配访问模式变化:历史热点数据长期不访问,计数仍居高不下,难以被淘汰
- 缓存预热问题:新的热点数据刚进入时计数低,极易被淘汰
- 计数排序开销大,单线程下性能瓶颈明显
4.3.2 Redis 近似LFU实现原理(4.0+新增)
Redis复用redisObject的24位lru字段,拆分为两部分实现LFU,同时通过对数计数器+周期衰减机制解决标准LFU的核心缺陷。
- 24位字段拆分:
- 高16位(ldt):Last Decrement Time,单位分钟,记录上一次计数器衰减的时间戳
- 低8位(logc):Logistic Counter,访问频率计数器,最大值255
- 核心机制1:对数增长计数器
- 解决8位计数器无法区分高频访问的问题,采用基于概率的对数增长逻辑,而非线性增长
- 核心规则:计数器值越小,增长越快;值越大,增长越慢。
lfu_log_factor越大,计数器增长越慢,对访问频率的区分度越高 - 源码级增长逻辑:每次访问时生成0-1的随机数,仅当随机数小于
1/(counter * lfu_log_factor + 1)时,计数器才+1;默认lfu_log_factor=10时,计数器达到255需要百万次以上访问
- 核心机制2:周期衰减机制
- 解决历史热点数据无法淘汰的问题,计数器会随时间周期衰减
- 核心规则:每次访问/采样时,计算当前时间与
ldt的差值,除以lfu_decay_time得到衰减次数,计数器max(counter - 衰减次数, 0),同时更新ldt为当前时间 - 默认
lfu_decay_time=1,即每分钟衰减1次,10分钟未访问的键,计数器至少衰减10
- 新键友好优化:新创建的键,计数器初始值为
5(LFU_INIT_VAL),而非0,避免新键刚进入就因计数过低被淘汰,解决缓存预热问题 - 淘汰执行流程:
- 内存超限触发淘汰时,按
maxmemory-samples随机采样对应数量的键 - 对采样键执行计数器衰减,更新
logc和ldt - 将采样键加入候选池,按计数器值升序排序
- 从候选池中选出计数器值最小的键,执行淘汰
- 重复流程,直到内存降至
maxmemory以下
- 内存超限触发淘汰时,按
4.3.3 核心配置与优化细节
lfu-log-factor:默认10,取值范围0-100,高频访问场景可适当调大,提升频率区分度lfu-decay-time:默认1,单位分钟,访问模式变化快的场景可适当调小,加快衰减速度,快速适配新的热点数据
4.4 LRU vs LFU 核心对比与选型
| 维度 | 近似LRU | 近似LFU |
|---|---|---|
| 核心判断依据 | 最后一次访问的时间 | 历史访问频率 + 时间衰减 |
| 缓存污染抗性 | 弱,批量冷数据易挤出热点数据 | 强,单次访问几乎不会提升计数器,冷数据难以污染缓存 |
| 新数据友好度 | 高,新访问数据直接更新lru时间,不易被淘汰 | 中,初始值5,需积累访问次数才能避免被淘汰 |
| 访问模式适配 | 适合冷热区分明显、访问模式稳定的场景 | 适合访问频率差异大、访问模式频繁变化的场景 |
| Redis版本支持 | 全版本支持 | 4.0及以上版本支持 |
五、全体系实战最佳实践
- 基础配置必做:必须设置
maxmemory,生产环境不超过物理内存的70%;hz参数默认10,高并发场景可调至20-50,禁止超过100 - 淘汰策略选型:绝大多数场景首选
allkeys-lru;热点集中、访问频率差异大的场景选allkeys-lfu;需保留永久键的场景选volatile-lru/volatile-lfu - 性能优化:
maxmemory-samples默认5,追求命中率可调至10;开启lazyfree-lazy-eviction/lazyfree-lazy-expire,避免大键删除阻塞主线程 - 主从集群规范:主从节点
maxmemory、淘汰策略、过期相关配置必须完全一致;禁止在从节点执行大量写入,避免从库OOM - 过期键规范:避免大量键设置相同的过期时间,导致定期删除集中执行阻塞主线程,建议给过期时间增加随机偏移量
六、高频面试考点总结
- 请区分Redis过期键删除策略和内存淘汰策略的核心差异
- Redis过期键删除为什么采用惰性+定期的组合策略,而不用定时删除?
- Redis的8种内存淘汰策略分别是什么,生产环境如何选型?
- Redis为什么不实现严格的LRU,而是用近似LRU?3.0+对LRU做了什么优化?
- Redis的LFU是如何实现的?如何解决标准LFU的历史热点和缓存预热问题?
- 主从架构下,Redis的过期键删除和内存淘汰有什么特殊约束?
- 使用volatile-lru策略时,内存满了但没有淘汰任何键,反而拒绝写入,是什么原因?