Redis内存回收:过期策略与淘汰策略
Redis 数据库结构
Redis 作为典型的键值内存存储数据库,所有的键值对(key - value)都存储在之前学习过的 Dict
结构里。在其database
结构体(redisDb
)中,包含多个重要的 Dict
以及其他属性:
dict *dict
:用于存放所有的 key 及对应的 value,这部分也被称为keyspace
。dict *expires
:专门存放每一个设置了 TTL(存活时间)的 key 及其对应的 TTL 存活时间。dict *blocking_keys
:记录有客户端在等待数据(比如通过BLPOP
命令)的 key。dict *ready_keys
:存储接收到PUSH
操作的被阻塞的 key。dict *watched_keys
:为MULTI/EXEC
(事务)以及CAS
(比较并交换)操作所监控的 key。int id
:数据库的 ID,范围是 0 到 15。long long avg_ttl
:用于记录平均 TTL 时长。unsigned long expires_cursor
:在进行 expire 检查时,在dict
中抽样的索引位置。list *defrag_later
:等待进行碎片整理的 key 列表。
过期策略
惰性删除
并非在 TTL(存活时间)到期后就立即删除 key,而是在访问一个 key 的时候,检查该 key 的存活时间,如果已经过期才执行删除操作。
具体执行:
在查找一个 key 执行写操作(lookupKeyWriteWithFlags
函数)和读操作(lookupKeyReadWithFlags
函数)时,都会调用 expireIfNeeded
函数来检查 key 是否过期expireIfNeeded
函数的逻辑是:先判断 key 是否过期,如果未过期直接结束并返回 0;如果过期,则删除过期的 key 并进行相关传播操作,然后返回 1。
周期删除
通过一个定时任务,周期性地抽样部分过期的 key,然后执行删除操作。执行周期有两种。
执行周期:
- Redis 服务初始化函数
initServer()
中设置定时任务:按照server.hz
的频率来执行过期 key 清理,模式为 SLOW。 - Redis 的每个事件循环前调用
beforeSleep()
函数:执行过期 key 清理,模式为 FAST。
具体执行:
- 在
initServer
函数中,会创建定时器,关联回调函数serverCron
,处理周期取决于server.hz
(默认 10)。 serverCron
函数会更新lruclock
到当前时间,为后期的 LRU(最近最少使用)和 LFU(最不经常使用)淘汰策略做准备,还会执行数据库的数据清理,比如过期 key 处理(调用databasesCron
函数)。databasesCron
函数会尝试清理部分过期 key,清理模式默认为 SLOW(调用activeExpireCycle
函数并传入ACTIVE_EXPIRE_CYCLE_SLOW
)。- 在
beforeSleep
函数中,尝试清理部分过期 key,清理模式默认为 FAST(调用activeExpireCycle
函数并传入ACTIVE_EXPIRE_CYCLE_FAST
)。
SLOW 模式
- 执行频率受
server.hz
影响,默认server.hz
为 10,即每秒执行 10 次,每个执行周期 100ms。 - 执行清理耗时不超过一次执行周期的 25%,默认 slow 模式耗时不超过 25ms。
- 逐个遍历数据库(db),逐个遍历 db 中的桶(bucket),抽取 20 个 key 判断是否过期。
- 如果没达到时间上限(25ms)并且过期 key 比例大于 10%,再进行一次抽样,否则结束。
FAST 模式
- 执行频率受
beforeSleep()
调用频率影响,但两次 FAST 模式间隔不低于 2ms。 - 执行清理耗时不超过 1ms。
- 逐个遍历 db,逐个遍历 db 中的 bucket,抽取 20 个 key 判断是否过期。
- 如果没达到时间上限(1ms)并且过期 key 比例大于 10%,再进行一次抽样,否则结束。
淘汰策略
当Redis内存使用达到设置的阈值时,Redis主动挑选部分Key删除
会在处理客户端命令的方法processCommand()中尝试做内存淘汰
支持8种策略来选择要删除的key:
- noeviction:不淘汰任何 key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl:对设置了 TTL 的 key,比较 key 的剩余 TTL 值,TTL 越小越先被淘汰
- allkeys-random:对全体 key,随机进行淘汰。也就是直接从 db->dict 中随机挑选
- volatile-random:对设置了 TTL 的 key,随机进行淘汰。也就是从 db->expires 中随机挑选。
- allkeys-lru:对全体 key,基于 LRU 算法进行淘汰(最少未使用,它认为在最近一段时间内使用频率低或者没有被使用的 key 是最有可能不再被使用的。)
- volatile-lru:对设置了 TTL 的 key,基于 LRU 算法进行淘汰
- allkeys-lfu:对全体 key,基于 LFU 算法进行淘汰(最不经常使用,将访问次数最少的 key 视为最不经常被使用的,从而优先淘汰。)
- volatile-lfu:对设置了 TTL 的 key,基于 LFI 算法进行淘汰
淘汰流程
Redis 内存淘汰流程如下:
- 首先判断内存是否充足,若充足则流程结束。
- 若内存不充足,判断内存策略是否为
NO_EVICTION
,若是则流程结束。 - 若不是
NO_EVICTION
,判断是否为AllKeys
模式:- 若是,从
db->entries
或db->dict
中淘汰。 - 若不是,进入后续内存策略判断。
- 若是,从
- 判断内存策略:
- 若为
LRU
、LFU
或TTL
策略:- 准备一个
eviction_pool
。 - 获取下一个 DB,随机挑选
maxmemory_samples
数量的 key。 - 根据内存策略,分别用
maxTTL - TTL
(TTL
策略)、nowTime - LRU值
(LRU
策略)、255 - LFU计数
(LFU
策略)作为idleTime
。 - 判断是否可以存入
eviction_pool
,若可以则按idleTime
升序存入。 - 若有下一个 DB,重复上述步骤;若没有,倒序从
eviction_pool
中获取一个 key 删除。
- 准备一个
- 若为
RANDOM
策略:遍历 DB,随机挑选一个 key 删除。
- 若为
- 删除 key 后,判断已释放内存是否满足需要释放的内存,若满足则流程结束;若不满足,重复上述淘汰步骤。