过期删除策略
有关过期时间的设置和查询
查看某个 key 剩余的存活时间,可以使用 TTL 命令(单位是秒)。
取消 key 的过期时间,则可以使用 PERSIST 命令。
# 取消 key1 的过期时间
> persist key1
(integer) 1
# 使用完 persist 命令之后,
# 查下 key1 的存活时间结果是 -1,表明 key1 永不过期
> ttl key1
(integer) -1
过期字典
过期字典就是一个哈希表,和保存键值对的字典(包含了两个哈希表,一个是真正保存键值对的,一个是在rehash时使用的)不同,它保存的是键和键对应的过期时间
typedef struct redisDb {
dict *dict; /* 数据库键空间,存放着所有的键值对 */
dict *expires; /* 键的过期时间 */
....
} redisDb;
通过查询这个过期字典,就可以知道某个key是否有过期时间。当我们查询一个key时,Redis会先去过期字典中查询,如果不存在,则直接在另一个字典(保存键值对的字段)中查询出对应的value返回;如果这个key存在于过期字典中,就查询对应的过期时间和当前系统时间进行对比,如果小于当前系统时间,就说明过期了
过期删除策略有哪些?
Redis的过期删除策略有三种,分别是“定时删除”、“惰性删除”、“定期删除”
定时删除
定时删除是设置key的过期时间时,为当前的key设置一个定时事件,当这个key过期时,就会触发定时处理器处理这个定时事件,对key进行删除
- 优点:对内存友好的。可以尽快地删除一些键值对,释放内存空间
- 缺点:对CPU不友好的。在过期的key很多的时候,删除大量的key会占有相当一部分的CPU时间,如果这时内存不紧张但是CPU比较紧张的话,就会对客户端的响应和吞吐量造成一定的影响
惰性删除
设置key的过期时间可能到了,但是不会主动删除,而是在每次数据库访问key的时候,顺便看看这个key是不是过期了,发现过期了再删除
- 优点:对CPU友好的。因为在访问到这个key的时候发现过期再删除,只会占有一小部分CPU资源。
- 缺点:对内存不友好。因为有些key可能已经过期了,但是一直没有被访问而一直驻留在内存当中,浪费了内存空间。
功能伪代码:
int expireIfNeeded(redisDb *db, robj *key) {
// 判断 key 是否过期
if (!keyIsExpired(db,key)) return 0;
....
/* 删除过期键 */
....
// 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除;
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
定期删除
每隔一段时间随机地从数据库中取出一部分key进行检查是否过期,过期了再删除。是一种介于定时删除和惰性删除之间的策略
- 优点:一方面限制了执行删除操作的时长和频率,减少删除操作对CPU的影响;同时也能够随机地删除一部分过期的键值对,减少内存的浪费
- 缺点:
- 在内存清理方面没有定时删除策略效果好,在CPU资源的使用上也没有惰性删除策略来的好
- 难以控制删除操作执行的时长和频率。如果执行太频繁,就会变得和定时删除策略一样,对CPU不友好;如果执行频率过低,就会变得和惰性删除一样,有些内存来不及释放,对内存不友好
功能伪代码:
循环中每次从过期字典中抽取20个key检查是否过期,如果过期则删除,同时记录本轮20个key中过期的key的数量expired,循环检查完20个key后判断expired是否操作检查的key的25%,超过则进入下一检查。为了避免不定地检查,会设置一个timelimit_exit限制检查是否检查时间超时
do {
//已过期的数量
expired = 0;
//随机抽取的数量
num = 20;
while (num--) {
//1. 从过期字典中随机抽取 1 个 key
//2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++
}
// 超过时间限制则退出
if (timelimit_exit) return;
/* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */
} while (expired > 20/4);
Redis采用的过期删除策略
Redis采用的是定期删除和惰性删除两种策略配合使用,兼顾合理使用CPU资源和避免内存的浪费
内存淘汰策略
过期删除策略是在大小键值对过期的时候对键值对进行删除的一种策略,而内存淘汰策略,是在内存超过了Redis设置的最大内存时使用内存淘汰策略删除符合条件的键值对
在配置文件 redis.conf 中,可以通过参数 maxmemory 来设定最大运行内存。不同位数的操作系统的默认值是不同的。对于64位的操作系统,默认值为0,也就是没有限制Redis的运行内存,就算实例因内存空间不足而奔溃也置之不理。对于32为的操作系统,默认值为3GB,因为32位操作系统的最大运行内存是4GB,设置3GB是合理的,可以避免Redis内存不足时奔溃。
Redis内存淘汰策略有哪些?
Redis的内存淘汰策略分为“不进行数据淘汰的策略”(Redis3.0之后默认的内存淘汰策略)和“进行数据淘汰的策略”
不进行数据淘汰的策略
Redis运行内存超过了设置的最大运行内存时,只是不允许数据写入,但是查询和删除操作还是可以进行的
进行数据淘汰的策略
进行数据淘汰的策略分为“在设置了过期时间的数据中淘汰”和“在所有数据范围内淘汰”
- 在设置了过期时间的数据中淘汰
注意Redis3.0之前是从过期的数据中采用LRU算法进行淘汰
- 在所有数据范围内淘汰
设置内存淘汰策略
可以通过命令config set maxmemory-policy <策略>
设置,会立即生效,但是Redis重启后就失效了;也可以修改Redis配置文件中的maxmemory-policy <策略>
,然后重启Redis生效
Redis中的LRU算法
LRU算法是最近最久未使用算法,Redis4.0后,内存淘汰策略中,对于“过期数据淘汰”和“所有数据进行淘汰”的策略中,都新增了LFU的内存淘汰策略,可见LRU算法的缺陷
- 传统LRU算法存在的问题
- 使用链表维护所有缓存数据,会带来额外的空间开销
- 当有数据被访问时,就要移动到链表表头,如果访问的数据量很大,就会带来很多的链表节点移动,很耗时,降低Redis的性能
Redis不是采用传统的LRU算法,而是在每个对象结构体中包含了一个额外字段,记录了这个对象的最后一次访问时间,这样,在进行内存淘汰的时候,直接随机抽取5(默认)个对象,然后淘汰最久没有使用的那个
- 优点:不需要维护一个链表,没有额外的内存开销;另外,也不需要每次数据访问的时候都移动链表节点,提升了Redis的性能。
- 缺点:没有办法解决“缓存污染”的问题。如果一个应用一次读取了大量的数据,而且这些数据之后被读取一次的话,那么就可能长期留存在Redis当中,造成缓存污染
Redis中的LFU算法
LRU算法下Redis对象结构体中的lru字段存储的是对象最后一次访问的时间戳,而LFU算法下,Redis对象结构体的lru字段存储的则是对象的访问信息(包括最后一次访问的时间戳 + 访问频次)
last decr time是用来记录key最后一次访问的时间戳
logc的初始值是5,它代表的是key的访问频率,而不是具体的访问次数,会随着时间推移而衰减。数值越小越可能被淘汰。具体而言,每次key被访问的时候,它会根据上次访问距离当前的时长,来对logc进行衰减,距离越长,衰减值越大。然后,再按照一定的概率对logc进行增加,logc值越大的key,越难再增加。
可以通过调整配置文件中的lfu-decay-time
和lfu-log-factor
来调整logc的衰减速度和增长速度