Redis回收策略与缓存过期
# Redis回收策略与缓存过期
在探讨Redis的缓存过期与淘汰策略之前,首先需要明确一点:你的Redis是用来干嘛的?是用做缓存还是DB数据库?
因为在配置Redis的内存,以及设置缓存过期与淘汰策略,这些都与你的Redis的用途有关。首先要有以下基本常识:
- 缓存用途:缓存的数据不是全量数据,会随着访问的变化而变化,通常保留的是热数据。
- DB用途:一般要求保持数据的完整性,不能宕机,不能淘汰,支持横向拓展。
基于这些常识,接下来去探讨Redis的maxmemory设置、expire过期的使用以及淘汰策略的选择。
# maxmemory配置
有两种方式可以设置Redis的内存:
在redis.conf中
maxmemory 100mb // 64位的系统是0, 代表没有内存限制,
通过 CONFIG SET (opens new window) 命令设置。
127.0.0.1:6379> config get maxmemory 1) "maxmemory" 2) "0" 127.0.0.1:6379> config set maxmemory xxx
当我们设置了maxmemory后,Redis会分配几乎和maxmemory一样大的内存。那么我们到底改设置多大呢?
- Redis作为DB使用,保证数据的完整性,不能淘汰,可以做集群,横向扩展。这个时候可以不设置。
- Redis作为缓存使用,假如我们不设置,当达到物理内存后,性能可能会持续下降,甚至崩溃(内存与硬盘交换swap虚拟内存,频繁IO)。这个时候需要根据具体的业务去设置。通常情况下,在满足业务运行的情况下,剩下的内存可以都给Redis,此外,如果有slaver从节点,注意也要流出一部分的内存。
当Redis快要达到指定的内存限制大小时,则会根据配置的不同的策略对内存进行回收。
# 回收策略
收了解回收策略前,对LRU和LFU有个基本的理解:
- LRU(Least Recently Used)算法:最近最少使用页面置换算法,淘汰最长时间未被使用的页面。
- LFU(Least Frequently Used)算法:最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页。
区别:LRU关键是看页面最后一次被使用到发生调度的时间长短;而LFU关键是看一定时间段内页面被使用的频率。
再简单得说就是,LRU-多久没有碰它,很久没碰的淘汰;LFU-一段时间碰了多少次,碰的少的淘汰。
Redis的maxmemory-policy
配置指令来进行回收策略的配置,回收策略有以下几种:
- noeviction(默认):禁止驱逐,不淘汰。直接返回错误。
- allkeys-lru:所有的键通过LRU驱逐,即从所有键中淘汰长时间未被使用的键。
- volatile-lru:对在过期集合的键通过LRU驱逐,即从过期键中淘汰长时间未被使用的键。
- allkeys-lfu:所有的键通过LFU驱逐,即从所有键中淘汰一定时间内使用次数最少的键。
- volatile-lfu:对在过期集合的键通过LFU驱逐,即从过期键中一定时间内使用次数最少的键。
- allkeys-random:随机回收所有键。
- volatile-random:随机回收过期集合的键。
- volatile-ttl:回收在过期集合的键,并优先回收存活时间(TTL)较短的键。
从上面可以知道,Redis的回收策略无非就是从两个方面考虑:
- 收集的键的范围:所有键还是过期键
- 使用的算法:是最近最少使用LRU还是最近最不常使用或者是随机。
除了上面的方面考虑,当然还有额外的情况比如不回收以及根据过期键存活时间(volatile-ttl)回收。
# 一般的选择
回收策略选择一般取决于你的应用的访问模式,此外,我们还可以监控缓存命中率和没命中的次数,在运行时进行相关的策略调整,一般来说有一下推荐:
- 使用allkeys-lru策略:如果Redis中部分的数据是属于热点数据,在不确定时一般选这个。
- 使用allkeys-random:如果Redis中所有的数据热点分布比较均衡,选择随机回收。
- 使用volatile-ttl:如果Redis中有大量通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期,那么建议选择volatile-ttl。不过,设置过期时间也是需要消耗内存的,所以有时候使用allkeys-lru这种策略反而更加高效。
# 回收进程工作流程
回收进程工作流:
- 一个客户端运行了新的命令,添加了新的数据。
- Redis检查内存使用情况,如果大于maxmemory的限制,则根据设定好的策略进行回收。
- 执行新的命令等等
也就是说,每次执行一个命令之前,Redis都会对内存进行校验或者会输。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存就会超出限制。
# expire过期
上面我们提到volatile-ttl
,也就是说我们是可以给key赋予过期时间的,主要是用expire
命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除。
# expire的使用
expire命令的使用方法如下:expire key ttl(单位秒)
127.0.0.1:6379> set k1 zhangsan
OK
127.0.0.1:6379> ttl k1 # 没有过期时间,持久化的
(integer) -1
127.0.0.1:6379> expire k1 3
(integer) 1
127.0.0.1:6379> ttl k1
(integer) 2
127.0.0.1:6379> ttl k1 # 过期失效
(integer) -2
127.0.0.1:6379> get k1
(nil)
# 注意事项
- 设置了expire的key,并不会随着访问而延长!
- 如果发生了写数据,那么会剔除设置的expire时间!
127.0.0.1:6379> set k2 lisi
OK
127.0.0.1:6379> expire k2 30
(integer) 1
127.0.0.1:6379> ttl k2
(integer) 26
127.0.0.1:6379> set k2 wangwu
OK
127.0.0.1:6379> ttl k2
(integer) -1
# expire原理
redisDb的结构体源码:
typedef struct redisDb {
int id; // id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; // 存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; // 存储数据库所有的key-value(重点)
dict *expires; // 存储key的过期时间(重点)
dict *blocking_keys; // blpop 存储阻塞key和客户端对象
dict *ready_keys; // 阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
dict *watched_keys; //存储watch监控的的key和客户端对象
} redisDb;
这个结构体定义中除了 id 以外都是指向字典的指针,其中我们只看 dict 和 expires。
- dict 用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对
- expires 则用于维护一个 Redis 数据库中设置了失效时间的键(即key与失效时间的映射)。
当我们使用 expire 命令设置一个key的失效时间时,Redis 首先到 dict 这个字典表中查找要设置的key是否存在,如果存在就将这个key和失效时间添加到 expires 这个字典表。
当我们使用 set 命令向系统插入数据时,Redis 首先将 Key 和 Value 添加到 dict 这个字典表中,然后将 Key 和失效时间添加到 expires 这个字典表中。
简单地总结来说就是,设置了失效时间的key和具体的失效时间全部都维护在 expires 这个字典表中。
此外,这里就知道前面回收策略中allkeys-xx(server.db[i].dict)、volatile-xx(server.db[i].expires)这些key是在那里保存了吧。
# 过期策略
现在讲讲Redis的过期策略(删除策略),包括对底层结构的结构以及LRU的更加详细解读。
等等,刚才不是讲了回收策略,怎么现在又出了个过期策略啊?什么东西??
其实这是站在不同的角度看Redis删除Key的方式而已,本质上还是对内存的回收。先前讲的回收策略是站在Redis对maxmemory兜底时候进行内存回收的策略,这里则主要是看Redis对过期键的回收处理,回收策略的设置当然也会影响到回收处理。
过期键淘汰策略分为定时删除、惰性删除、主动删除(回收策略)。
过期判定原理:
Redis目前采用惰性删除+主动删除的方式,即被动+主动。
# 定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。需要创建定时器,而且消耗CPU,一般不推荐使用。
# 惰性删除
在key被访问时如果发现它已经失效,那么就删除它。
底层原理是调用expireIfNeeded
函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删除它。
这里注意,如果Redis正在从RDB文件中加载数据,是暂时不会处理失效主键的。
int expireIfNeeded(redisDb *db, robj *key) {
// 获取主键的失效时间 get当前时间-创建时间>ttl
long long when = getExpire(db,key);
// 假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
if (when < 0) return 0;
// 假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
if (server.loading) return 0;
...
// 如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
// 还未失效就直接返回0
if (mstime() <= when) return 0;
// 如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失效的信息进行广播,
// 最后将该主键从数据库中删除
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);
}
# 主动删除(回收策略)
仅仅做惰性删除是不够的,因为有些过期的keys,可能永远不会被访问。 无论如何,这些keys应该过期,所以Redis还会定时随机删除:具体就是Redis每秒10次做的事情:
- 测试随机的20个keys进行相关过期检测。
- 删除所有已经过期的keys。
- 如果有多于25%的keys过期,重复步奏1.
这是一个平凡的概率算法,基本上的假设是,在不断重复过期检测中,直到过期的keys的百分百低于25%。这意味着,在任何给定的时刻,最多会清除1/4的过期keys。
使用的策略就是在redis.conf
文件中配置的回收策略,上面的回收策略已经进过,这里不再赘述。
# 总结
对于内存的设置以及回收策略的选择,其实是根据我们Redis的使用场景以及业务场景来运转的,因为内存是有限制的,Redis中应该尽量保存热数据,而对冷数据进行淘汰。
对于过期的键,Redis是同构 被动访问时判定以及周期轮询增量判定的方式进行回收的。