Redis内存淘汰策略深度解析
Redis内存淘汰策略深度解析
Redis作为一款高性能的内存数据库,当内存使用达到预设阈值时,其内存淘汰策略决定了如何释放空间以维持系统正常运行。本文将深入解析Redis 7.0版本的内存淘汰机制,从源码层面揭示其工作原理。
Redis将数据存储在内存中,并设置了一个内存使用阈值。当内存使用超过这个阈值时,Redis会根据预设的内存淘汰策略来删除一些键,从而释放内存空间,确保系统能够继续处理写操作。本文将参考Redis 7.0源码,详细解析当内存使用超过限制时,Redis是如何进行键淘汰的。
1. 源码入口
在源码中,内存淘汰的主要处理函数是performEvictions
,这个函数在处理每条命令时被调用。其调用链如下:
如果Redis设置了maxmemory
,同时Redis没有处在超时执行的Lua脚本,则会进行内存淘汰检查和执行对应的淘汰策略。
2. 相关配置
在启动配置文件中,有几个配置项与内存淘汰密切相关:
maxmemory <bytes>
:Redis最大内存限制,如果没有配置,默认值在32位系统下为3G,64位系统无限制。maxmemory-policy
:8种内存淘汰策略中的一种。maxmemory-samples
:LRU、LFU、TTL采用的是近似算法,维护了一个大小为16的数组,每次采样maxmemory-samples
个键更新这个数组。lazyfree-lazy-eviction
:是否启动惰性淘汰,开启的情况下会尝试异步删除。
3. 淘汰策略
淘汰策略由配置项maxmemory-policy
决定,共有8种策略,从预选键的范围来看可分为3类:
noeviction
:表示不进行淘汰。allkeys
:在所有键上进行具体的策略。volatile
:只在设置了过期时间的键上进行具体的策略。
而具体的淘汰算法实际是4种。
3.1 随机淘汰
遍历每个DB,从中随机选择一个键。注意如果上次选定了DB_A,则下次会从DB_A的下一个DB开始选择。随机选择的策略在源码中对应dictGetRandomKey(dict)
函数。该函数包含两个步骤:首先是随机选择一个哈希表中的槽,其次在槽的链表上选择一个键。选择槽的时候会考虑rehash,不会选择刚刚rehash结束的键。
3.2 LRU(Least Recently Used)
LRU/LFU/TTL策略在同一个条件分支下。首先是遍历每个DB,调用evictionPoolPopulate
函数,采样若干个键放入pool中,由于pool中的键是按照idle从小到大排序的,然后从后往前遍历pool获取一个键,即为候选键bestKey。不同的策略实际是idle值计算方式不同。
LRU策略优先淘汰最近一段时间没有被访问到的键。通过estimateObjectIdleTime
函数获取键的idle值。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
每个键对象包含了一个lru字段,该字段在LRU淘汰策略下表示上一次被访问时的lru时钟时间,差值即为该键的空闲时间。考虑到lru为24位,只能保存一段时间内的值,可能存在值的套圈,所以有了else的条件判断。
3.3 LFU(Least Frequently Used)
LFU策略考虑访问频率,idle = 255-LFUDecrAndReturn(o)
。在LFU策略下,键对象的lru字段低8位表示访问频次,高16位表示上次访问的时间。255-访问频次作为idle的值,访问频率越低idle值越大。
3.4 TTL
直接获取键的过期时间,选择最近的过期键。
4. 候选键删除
得到候选键和对应的DB后,Redis的删除过程如下:
- 获取当前内存值delta
- 执行删除,根据配置项决定是同步删除还是异步删除
- 重新获取内存值,计算本次删除的内存大小delta(此时异步删除可能还没有执行)
- 删除可能持续很久,每释放16次键,同步删除信号给副本;异步删除情况下,检查内存是否已经满足要求
- 检查当前删除耗时是否大于配置值,如果超出时间约束,启动后台任务继续执行删除
4.1 同步和异步删除
当async=0
时,表示同步删除;async=1
表示异步删除。
删除键所在DB如果有设置过期的键集合,则首先尝试删除该键的过期时间,然后调用dictUnLink
将键从字典中移除,但是此时并没有释放键和节点的内存。如果是异步删除则调用freeObjAsync
异步删除,同时设置该节点对应的值为nullptr。最后调用dictFreeUnlinkedEntry
,对于同步的情况,val不为空,会直接删除,而异步的情况,val为空,不执行删除val操作,而是在异步任务中真正的删除。
而异步删除freeObjAsync
实际上还会检查对象值是否大于设置阈值以及当前是否没有被共享,两个条件都满足时才会启动后台删除线程。这个值的计算遵循:string类型为 1,list类型返回存储元素数,set(hashtable实现)、zset(跳表实现)、hash(hashtable)返回包含元素数。
5. 总结
Redis在处理线上命令时会检测内存是否超出阈值,当超出阈值时,计算本次需要释放多少内存。然后根据淘汰策略选择删除候选键,随机选择是随机选择字典的槽和链表上的节点,lru/lfu/ttl 都是采用的近似算法,维护一个固定大小的数组,按照键的idle值从小到大排序,每次选择idle最大的键,每次也会采样指定的键来更新这个数组。选定候选键后根据配置执行同步或者异步删除。为了防止阻塞线上请求,删除不能执行太久,所以每16次都会检查内存是否已经满足要求,满足则直接退出,检查当前删除动作是否已经超时,超时则转化到后台执行。