由于redis是一个内存数据库,所有键值对都保存在内存中,而内存的数量毕竟不是无限的,因此当redis中保存的键值对超过一定数量时,就需要淘汰掉一些键值对,减少redis所占的内存空间。redis有两种内存淘汰策略,一种称为主动淘汰,一种称为被动淘汰。主动淘汰即为redis的键值对设置过期时间,当时间到期之后,对应的键值对就会从内存中删除。而被动淘汰则是给redis的内存设置一个最大的限制,当内存使用超过这个限制时,就会采用一定的策略算法被动的删掉一部分键值对,接下来我们分别进行介绍。
1. 主动淘汰
1.1 键的生存周期
通过EXPIRE命令,客户端可以以秒或者毫秒的精度为数据库的某个键设置生存周期,经过指定的秒或者毫秒之后,服务器就会自动删除生存周期为0的键。redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典。
typedef struct redisDb {
// 过期字典,保存者键的过期时间
dict *expires;
} redisDb;
redis过期键的删除策略主要有三种,分别是:
- 定时删除:设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。定时删除对CPU不友好。
- 惰性删除:放任过期键不管,但是每次从键空间获取键时,都检查键是否过期,过期则删除,否则则返回。惰性删除对内存不友好。
- 定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,检查多少数据库,由算法决定。定期删除是定时删除和惰性删除的折中。
1.2 过期键删除策略
redis 服务器实际上采用了惰性删除和定期删除两种策略相结合的方式:
(1)惰性删除的实现:
惰性删除由expireIfNeeded函数实现,所有读写数据库的redis命令在执行前都会调用这个函数对输入的键进行检查,如果输出的键已经过期,则将键从数据库中删除。
(2)定期删除的实现:
定期删除由activeExpireCycle函数实现,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字段中随机检查一部分过期时间,并删除其中的过期键。该函数的有两种,分别是快速模式(ACTIVE_EXPIRE_CYCLE_FAST)和正常模式(ACTIVE_EXPIRE_CYCLE_SLOW):
- 在快速模式下,执行的时间不会长过 1000(EXPIRE_FAST_CYCLE_DURATION )微秒,并且在 1000(EXPIRE_FAST_CYCLE_DURATION )微秒之内不会再重新执行。快速模式会在事件循环每次循环的beforeSleep函数中调用。
- 在正常模式下,执行的时间不会超过CPU时间的25%,即100ms * 25%,为25ms,这里的100ms表示serverCron函数1s调用10次。正常模式会在时间事件的serverCron函数中调用。
2. 被动淘汰
2.1 被动淘汰策略
Redis的所占用的最大内存可以在配置文件的maxmemory中设置,而Redis的内存淘汰策略则可以通过maxmemory-policy和maxmemory-samples来设置。当redis所占的内存大于配置文件所设置的内存时,redis需要从所有键中选择一个键进行淘汰。Redis有6种内存淘汰策略,分别是:
/* Redis maxmemory strategies */
#define REDIS_MAXMEMORY_VOLATILE_LRU 0
#define REDIS_MAXMEMORY_VOLATILE_TTL 1
#define REDIS_MAXMEMORY_VOLATILE_RANDOM 2
#define REDIS_MAXMEMORY_ALLKEYS_LRU 3
#define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4
#define REDIS_MAXMEMORY_NO_EVICTION 5
#define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION
- volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key,对应宏定义REDIS_MAXMEMORY_VOLATILE_LRU
- volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。对应宏定义REDIS_MAXMEMORY_VOLATILE_TTL
- volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。对应宏定义REDIS_MAXMEMORY_VOLATILE_RANDOM
- allkeys-lru: 所有key通用,优先删除最近最少使用(less recently used ,LRU) 的 key。对应宏定义REDIS_MAXMEMORY_ALLKEYS_LRU
- allkeys-random: 所有key通用; 随机删除一部分 key。对应宏定义REDIS_MAXMEMORY_ALLKEYS_RANDOM
- noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息,这是默认的淘汰策略。对应宏定义REDIS_MAXMEMORY_NO_EVICTION
注意:对于LRU淘汰策略,redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取maxmeory-samples个键,删除这些键中空转时间最大的键。此外,Redis还使用了一个缓冲池来保存历次淘汰时那些空转时长最大的键,进行新一轮淘汰时,会将这随机选择的maxmeory-samples个键和缓冲池中的键进行比较,然后选出空转时长最大的那个。采用缓冲池策略,把一个全局排序问题转化成为了局部的比较问题。缓冲池大小由宏REDIS_EVICTION_POOL_SIZE定义,大小为16。
2.2 被动淘汰策略的实现
主要由freeMemoryIfNeeded函数实现内存淘汰,每次执行客户端的命令时都会调用这个函数,该函数的整个计算流程如下图所示。
(1)计算Redis目前所占的内存总数时,有两个方面的内存不会计算在内,分别是:
- 所有从服务器slave的输出缓冲区的内存
- AOF 缓冲区的内存server.aof_buf和AOF重写缓冲区的缓存
(2)循环中每次删除时,只选择一个键进行删除,删除流程为:
-
如果策略是 allkeys-lru 或者 allkeys-random,那么淘汰的目标为所有数据库键;如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl,那么淘汰的目标为带过期时间的数据库键;
-
如果使用的是随机策略(volatile-random、allkeys-random policy),那么从目标字典中随机选出键;
-
如果使用的是 LRU 策略(volatile-lru、allkeys-lru policy),那么从一集 sample 键中选出 IDLE 时间最长的那个键,IDEL时间为当前时间减去每个键所保存的lru时间
-
如果策略为TTI(volatile-ttl) ,从一集 sample 键中选出过期时间距离当前时间最接近的键;