对于过期键的处理,Redis一共提供了两种过期策略,不同的策略也会影响Redis的性能

下面我就来具体讲讲这两种过期策略

定期扫描策略

Redis会将每个设置了过期时间的key放入一个独立的字典中,之后会定时遍历这个字典来删除到期的key

Redis默认每秒进行10次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略,如下:

(1) 从过期字典中随机选择出 20 个key
(2) 删除这 20 个key中已经过期的key
(3) 如果过期的key的比例超过1/4,那就重复步骤 (1)

这种随机从字典里选择删除key在一定程度下可以保证主线程在处理过期key的时候不会占用太多的时间

但是如果Redis实例中所有的key都在同一时间过期,那么Redis会持续扫描过期字典集合,直到过期字典中过期的key比例低于 1/4,Redis才会停止扫描,这就会导致Redis会在过期key清理上花费很多的时间,从而导致其他的命令无法执行

因此Redis为了解决这一问题,Redis也给扫描时间设置了一个上限,默认不会超过 25ms,但是这种方案还是会有缺陷

如果客户端发送了一个命令,此时服务器正在进行过期扫描,那么客户端的这一请求至少需要等待 25ms 后才能进行处理,如果客户端的超时时间设置得要比 25ms 低,那么就会出现大量得链接超时,从而造成业务端的异常

我们在做业务处理的时候,给大量的key不要设置为同一过期时间,应当给过期key设置一个随机的时间

惰性策略

定时扫描策略由于是随机的从过期字典里选取key再进行删除,因此也会有"漏网之鱼"

为了避免这种情况,Redis在定时扫描策略的基础上还加了一个惰性策略

惰性策略就是在客户端访问这个key的时候,如果这个过期key还存在,那么还会对key的过期时间进行检查,如果过期了就立即删除并且不会把值返回给客户端

惰性策略是直接对定时扫描策略的增强,弥补了定时扫描策略的不足

从节点的过期策略

使用Redis就难免会使用到集群,从节点不会主动的进行过期扫描,任然还是以数据同步的方式来对过期数据进行处理

主节点在key到期时,会在AOF文件里增加一条del指令,然后再同步到所有的从节点,从节点执行这条 del 指令来删除过期的 key

由于主从同步数据是异步进行的,如果主节点过期的key的del指令没有及时同步到从节点的话,就会出现主从数据不一致

Redis对于超过内存限制的处理

上面我们就已经提到Redis的过期key处理,但是想一下如果Redis中所有的key都没有设置过期时间又或者key的过期时间非常的长,只要key还没过期,所有的key都会存在于Redis中,只要Redis没有宕机,久而久之Redis的内存容量就会超出物理内存限制,此时如果再有数据传入,那么Redis就会将数据存放直接保存到磁盘中

这种与磁盘频繁的交互会让Redis的性能急剧下降,此时Redis就是去了原本存在的意义

为了防止Redis发生与磁盘交互的行为,Redis提供了最大内存的使用限制,我们可以在 conf 配置参数 maxmemory 来限制Redis的使用内存

redis hset 过期 redis过期key如何处理_数据库


当然这里只配置了内存使用限制还不行,还需要配置处理策略(当内存使用超过了内存使用限制时该怎么做),同样也需要在 conf 文件中配置 maxmemory-policy 参数

redis hset 过期 redis过期key如何处理_redis_02

Redis 提供了几种可选策略来进行处理,这些策略在 4.0 之前四种,4.0之后又新增了两种

noeviction: 不会继续服务写请求,读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续运行。这也是默认的处理策略
volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先被淘汰。没有设置过期时间的key不会被淘汰,这样可以保证需要持久化的数据不会突然丢失
volatile-lru: 与 volatile-lru 不同的是,它会使用 LFU 算法淘汰设置了过期时间的key
volatile-ttl:跟上面几乎一样,不过淘汰的策略不是LRU,而是比较key的剩余寿命ttl的值,ttl越小越优先被淘汰
volatile-random:跟上面几乎一样,不过淘汰的key是过期key集合中随机的key
allkeys-lru:区别于volatile-lru,这个策略要淘汰的key对象是全体的key集合,而不只是过期的key集合。这意味着一些没有设置过期时间的key也会被淘汰
allkeys-lfu:与allkeys-lru算法不同的是,算法淘汰设置了过期时间的key
allkeys-random:跟上面几乎一样,不过淘汰的key是随机的key。

上面的 volatile-xxx 策略只会针对已经设置了过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的key 进行淘汰

如果客户端不会设置key的过期时间,那么可以选择 allkeys-xxx 策略,如果需要保证数据的持久化,那么就可以选择 volatile-xxx 策略,因为这种策略只针对于设置了过期时间的key,没有设置过期时间的key不会被LRU算法淘汰

浅谈LRU与LFU

上面的淘汰策略里使用到了一种LRU和LFU的淘汰算法机制,我们再来看看这两种机制的区别

LRU全称 Least Recently Used,即最近最少使用,意思是当数据最近被访问了,那么在未来被访问的几率会比较大

我们可以把LRU理解为是一个双向链表结构,所有的数据都在这条链表中,当访问了链表中某个元素时,会把被访问元素移动到链表的头部,在淘汰的时候会把靠近链表末尾的元素给淘汰掉

假设现在有A、B、C、D、E四个元素,对应在链表中

redis hset 过期 redis过期key如何处理_nosql_03


当访问了 D 元素的时候,D 元素会被移动到链表头部

redis hset 过期 redis过期key如何处理_nosql_04


越靠近链表头部的位置就表示最近被访问到,越靠近链表末尾的位置就表示不会被访问到,这样在链表末尾的 C、E 就有可能会被淘汰掉

Redis 使用的时一种近似LRU算法,它跟LRU算法不太一样,之所以不完全使用LRU算法,是因为其需要消耗大量的额外内存,需要对现有的数据结构进行较大的改造

Redis为了实现近似LRU算法,给每个key增加了一个额外的小字段,这个字段的长度是 24 个bit,也就是最后一次被访问的时间戳

这个算法也很简单,就是随机采样出 5 (可以通过 maxmemory_samples 参数设置) 个key, 然后进行淘汰掉,如果淘汰后还是超出最大的内存限制,那就继续随机采样淘汰,直到内存低于最大内存限制为止

这里要注意的是,这个采样是根据淘汰策略来的,如果是 allkeys-xxx 策略,那么就是从所有的 key 字段中随机采样,如果是 volatile-xxx 策略,就从带过期时间的key字典中随机采样

LFU全称 Least frequently used ,使用频次最少的,即为不经常使用的 ,意思是当数据访问的频率或次数很低,那么在未来访问的几率也很低

与LRU不同的是,LFU主要关注数据访问的频率或次数,而LRU关注的是数据的最近有没有被访问

在数据被访问的时候,LFU 会把访问的频率或次数记录下来,当需要淘汰的时候会把访问频率或次数低的数据给淘汰掉,相对于LRU来说,LFU没有那么复杂

我们还是以A、B、C、D、E 五个元素为例,假设它们对应的访问频次为 10、4、9、2、6

redis hset 过期 redis过期key如何处理_Redis_05


假设 C 元素被访问了一次,那么 C 的频次就会加 1 ,从 9 变成 10 ,那么当需要对数据进行淘汰的时候,其中的 B 和 D 元素的访问频率是最低的,就有可能会被淘汰

LFU 在Redis 4.0 之后才被引入进来,具体要使用那种淘汰策略,还需要依据具体的场景来选择