redis 过期策略
问题:
- Redis会自己回收清理不用的数据吗?
- 如果能,那如何配置?
-
设置key过期时间
Redis对存储值的过期处理实际上是针对该值的键(key)处理的,即时间的设置也是设置key的有效时间。Expires字典保存了所有键的过期时间,Expires也被称为过期字段
expire key time(以秒为单位)—这是最常用的方式
- setex(String key, int seconds, String value)—字符串独有的方式
注: 1、除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠expire方法来设置时间 2、注意:如果没有设置时间,那缓存就是永不过期 3、如果设置了过期时间,之后又想让缓存永不过期,使用persist key
设置过期时间常用方式
一般主要包括4种处理过期方,其中expire都是以秒为单位,pexpire都是以毫秒为单位的
EXPIRE key seconds //将key的生存时间设置为ttl秒
PEXPIRE key milliseconds //将key的生成时间设置为ttl毫秒
EXPIREAT key timestamp //将key的过期时间设置为timestamp所代表的的秒数的时间戳
PEXPIREAT key milliseconds-timestamp //将key的过期时间设置为timestamp所代表的的毫秒数的时间戳
PS:
- timestamp为unix时间戳(例如:timestamp=1650038400 表示将在2022-04-16过期)
- 1、2两种方式是设置一个过期的时间段,就是咱们处理验证码最常用的策略,设置三分钟或五分钟后失效,把分钟数转换成秒或毫秒存储到Redis中。
- 3、4两种方式是指定一个过期的时间 ,比如优惠券的过期时间是某年某月某日,只是单位不一样。
EXPIREAT 命令用法:
EXPIREAT 语法:
expireat key timestamp
返回值:
一个整数值1或0,如下:
- 如果成功地为该键设置了超时时间,返回 1
- 如果键不存在或无法设置超时时间,返回 0
示例:
127.0.0.1:6379> set k1 123456 #设置key值 k1->123456
OK
127.0.0.1:6379> get k1
"123456"
127.0.0.1:6379> expireat k1 1650001380 # 设置过期时间
(integer) 1
127.0.0.1:6379> ttl k1 #查询剩余过期时间
(integer) 40
127.0.0.1:6379> ttl k1
(integer) 22
127.0.0.1:6379> ttl k1 #key值已失效
(integer) -2
127.0.0.1:6379> get k1 #无法获取到失效的key值 ,数据已被清理
(nil)
字符串设置过期时间
对字符串特殊处理的方式为SETEX命令,SETEX命令为指定的 key 设置值及其过期时间。如果 key 已经存在, SETEX命令将会替换旧的值。
SETEX 语法:
setex key seconds value
返回值:
OK
示例:
127.0.0.1:6379> setex k1 30 123456789 #设置字符换的k1 以及过期时间 value值
OK
127.0.0.1:6379> get k1
"123456789"
127.0.0.1:6379> ttl k1 # 过期时间
(integer) 16
127.0.0.1:6379> ttl k1
(integer) 8
127.0.0.1:6379> ttl k1 # 已过期
(integer) -2
127.0.0.1:6379> get k1 # 无法获取到过期的key值
(nil)
三种Redis过期策略
- 定时删除策略
- 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
- 优点:保证内存被尽快释放
- 缺点:
- 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
- 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
- 惰性删除策略
- 含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
- 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
- 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
- 定期删除策略
- 含义:每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
- 优点:
- 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用—处理”定时删除”的缺点
- 定期删除过期key—处理”惰性删除”的缺点
- 缺点
- 在内存友好方面,不如”定时删除”
- 在CPU时间友好方面,不如”惰性删除”
- 难点
- 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)【消耗资源】(这个要根据服务器运行情况来定了)
结论:
定时删除和定期删除为主动删除:Redis会定期主动淘汰一批已过去的key
惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除
惰性删除为redis服务器内置策略
定期删除可以通过:
- 第一、配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
- 第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略
- memcached只是用了惰性删除,而Redis同时使用了惰性删除与定期删除,这也是二者的一个不同点(可以看做是redis优于memcached的一点)
对于惰性删除而言,并不是只有获取key的时候才会检查key是否过期,在某些设置key的方法上也会检查(eg.setnx key2 value2:该方法类似于memcached的add方法,如果设置的key2已经存在,那么该方法返回false,什么都不做;如果设置的key2不存在,那么该方法设置缓存key2-value2。假设调用此方法的时候,发现redis中已经存在了key2,但是该key2已经过期了,如果此时不执行删除操作的话,setnx方法将会直接返回false,也就是说此时并没有重新设置key2-value2成功,所以对于一定要在setnx执行之前,对key2进行过期检查)
Redis采用的过期策略
惰性删除+定期删除
惰性删除流程惰性删除实现: 过期键的惰性删除删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeed函数对输入键进行检查:
- 在进行get或setnx等操作时,先检查key是否过期,
- 若过期,删除key,然后执行相应操作;
- 若没过期,直接执行相应操作
- 如果键已经过期,那么expireIfNeeded函数将键删除
- 如果键未过期,那么expireIfNeeded函数不做操作命令调用expireIfNeeded函数过程如下图:
PS:另外因为每个被访问的键都可能被删除,所以每个命令都必须能同时处理键存在以及不存在的情况。 下图表示get命令的执行过程
定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)
- 过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定时间内,分多次遍历服务器中各个数据库
- Redis 默认每秒进行 10 次过期扫描,过期扫描不会遍历过期字典中所有的 key, 而是采用了一种简单的贪心策略,步骤如下:
从内存数据库持久化数据到RDB文件
- 持久化key之前,会检查是否过期,过期的key不进入RDB文件
从RDB文件恢复数据到内存数据库
从内存数据库持久化数据到AOF文件:
- 当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
- 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
- AOF重写
- 重写时,会先判断key是否过期,已过期的key不会重写到aof文件
redis 内存淘汰机制(eviction)
redis 支持缓存淘汰(eviction)并提供相应的了配置项:
1. maxmemory 最大内存
PS: 设置内存使用上限,该值不能设置为小于 1M 的容量,选项的默认值为 0,此时系统会自行计算一个内存上限。
问题:Redis 设置多大的内存容量呢?
解决:根据“八二原理“,即 80% 的请求访问了 20% 的数据,因此如果按照这个原理来配置,将 Redis 内存大小设置为数据总量的 20%,就有可能拦截到 80% 的请求。当然,只是有可能,对于不同的业务场景需要进行不同的配置,一般建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销
2. maxmemory-policy 内存淘汰策略
redis数据库维护了两个字典:
- db.dict:数据库中所有键值对,也被称作数据库的 keyspace
- db.expires:带有生命周期的 key 及其对应的 TTL(存留时间),因此也被称作 expire set
当达到内存使用上限maxmemory时,可指定的清理缓存所使用的策略有:
- noeviction 当达到最大内存时直接返回错误,不覆盖或逐出任何数据,不淘汰数据
- allkeys-lfu 淘汰整个 keyspace 中最不常用的 (LFU) 键 (4.0 或更高版本)
- allkeys-lru 淘汰整个 keyspace 最近最少使用的 (LRU) 键
- allkeys-random 淘汰整个 keyspace 中的随机键
- volatile-ttl 淘汰 expire set 中 TTL 最短的键
- volatile-lfu 淘汰 expire set 中最不常用的键 (4.0 或更高版本)
- volatile-lru 淘汰 expire set 中最近最少使用的 (LRU) 键
- volatile-random 淘汰 expire set 中的随机键
当 expire set 为空时,volatile-* 与 noeviction 行为一致
3. maxmemory-samples 采样数量
为了保证性能,redis 中使用的 LRU 与 LFU 算法是一类近似实现。 简单来说就是:算法选择被淘汰记录时,不会遍历所有记录,而是以 随机采样 的方式选取部分记录进行淘汰。maxmemory-samples 选项控制该过程的采样数量,增大该值会增加 CPU 开销,但算法效果能更逼近实际的 LRU 与 LFU 。
4. lazyfree-lazy-eviction
清理缓存就是为了释放内存,但这一过程会阻塞主线程,影响其他命令的执行。 当删除某个巨型记录(比如:包含数百条记录的 list)时,会引起性能问题,甚至导致系统假死。延迟释放 机制会将巨型记录的内存释放,交由其他线程异步处理,从而提高系统的性能。 开启该选项后,可能出现使用内存超过 maxmemory 上限的情况。
淘汰策略算法
缓存能使用的内存是有限的,当空间不足时,应该优先淘汰那些将来不再被访问的数据,保留那些将来还会频繁访问的数据。因此淘汰算法会围绕 时间局部性 原理进行设计,即:如果一个数据正在被访问,那么在近期很可能会被再次访问。
为了适应缓存读多写少的特点,实际应用中会使用哈希表来实现缓存。当需要实现某种特定的缓存淘汰策略时,需要引入额外的簿记 book keeping 结构。
下面我们 3 种最常见的缓存淘汰策略:
1. FIFO(先进先出)
- 越早进入缓存的数据,其不再被访问的可能性越大
- 因此在淘汰缓存时,应选择在内存中停留时间最长的缓存记录
使用队列即可实现该策略:
PS:
优点:实现简单,适合线性访问的场景
缺点:无法适应特定的访问热点,缓存的命中率差 簿记开销:时间 O(1),空间 O(N)
2. LRU (最近最少使用)
- 一个缓存被访问后,近期再被访问的可能性很大
- 可以记录每个缓存记录的最近访问时间,最近未被访问时间最长的数据会被首先淘汰。
使用链表即可实现该策略:
当更新 LRU 信息时,只需调整指针:
PS:
优点:实现简单,能适应访问热点 缺点:对偶发的访问敏感,影响命中率 簿记开销:时间 O(1),空间 O(N)
3. LRU-K
原始的 LRU 算法缓存的是最近访问了 1 次的数据,因此不能很好地区分频繁和不频繁缓存引用。 这意味着,部分冷门的低频数据也可能进入到缓存,并将原本的热点记录挤出缓存。 为了减少偶发访问对缓存的影响,后续提出的 LRU-K 算法作出了如下改进:
- 在 LRU 簿记的基础上增加一个历史队列 History Queue
- 当记录访问次数小于 K 时,会记录在历史队列中(当历史队列满时,可以使用 FIFO 或 LRU 策略进行淘汰)
- 当记录访问次数大于等于 K 时,会被从历史队列中移出,并记录到 LRU 缓存中
K 值越大,缓存命中率越高,但适应性差,需要经过大量访问才能将过期的热点记录淘汰掉。 综合各种因素后,实践中常用的是 LRU-2 算法:
PS:
优点:减少偶发访问对缓存命中率的影响 缺点:需要额外的簿记开销 簿记开销:时间 O(1),空间 O(N+M)
4. LFU (最不经常使用)
- LFU 全称 Least Frequently Used
- 一个缓存近期内访问频率越高,其再被访问的可能性越大。
- 可以记录每个缓存记录的最近一段时间的访问频率,访问频率低的数据会被首先淘汰。
实现 LFU 的一个简单方式,是在缓存记录设置一个记录访问次数的计数器,然后将其放入一个小顶堆:
为了保证数据的时效性,还要以一定的时间间隔对计数器进行衰减,保证过期的热点数据能够被及时淘汰:
redis 淘汰策略实现
Redis的LRU算法不是一个严格的LRU实现。这意味着Redis不能选择最佳候选键来回收,也就是最久未被访问的那些键。相反,Redis 会尝试执行一个近似的LRU算法,通过采样一小部分键,然后在采样键中回收最适合(拥有最久访问时间)的那个。
然而,从Redis3.0开始,算法被改进为维护一个回收候选键池。这改善了算法的性能,使得更接近于真实的LRU算法的行为。Redis的LRU算法有一点很重要,你可以调整算法的精度,通过改变每次回收时检查的采样数量。
这个参数可以通过如下配置指令:
maxmemory-samples 5
Redis没有使用真实的LRU实现的原因,是因为这会消耗更多的内存,会产生链表移动。然而,近似值对使用Redis的应用来说基本上也是等价的。
Redis 对 LRU 的实现进行了一些改变:
- 记录每个 key 最近一次被访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)
- 在第一次淘汰数据时,会先随机选择 N 个数据作为一个候选集合,然后淘汰 lru 值最小的。(N 可以通过 config set maxmemory-samples 100 命令来配置)
- 后续再淘汰数据时,会挑选数据进入候选集合,进入集合的条件是:它的 lru 小于候选集合中最小的 lru。
- 如果候选集合中数据个数达到了 maxmemory-samples,Redis 就会将 lru 值小的数据淘汰出去。
注意:
在 Redis 4.0 时添加进来。它在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。
前面说到,LRU 使用了 RedisObject 中的 lru 字段记录时间戳,lru 是 24bit 的,LFU 将 lru 拆分为两部分:
- ldt 值:lru 字段的前 16bit,表示数据的访问时间戳
- counter 值:lru 字段的后 8bit,表示数据的访问次数
使用 LFU 策略淘汰缓存时,会把访问次数最低的数据淘汰,如果访问次数相同,再根据访问的时间,将访问时间戳最小的淘汰。
为什么 Redis 有了 LRU 还需要 LFU 呢?
在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。
由于 LRU 是基于访问时间的,如果系统对大量数据进行单次查询,这些数据的 lru 值就很大,使用 LFU 算法就不容易被淘汰。