Redis的过期删除策略与淘汰策略

  • Post author:
  • Post category:其他




过期策略

Redis的所有数据结构都可以设置过期时间,Redis将设置了过期时间的Key放入一个独立的字典里,然后定时遍历这个字典来删除到期的Key。除此定时遍历外还会采用惰性策略来删除Key,当一个Key被访问时,先检查这个key的过期时间,如果已经过期,就立即删除。

Redis的定时扫描策略每秒会进行10次扫描,但是并不是每一次都扫描字段中的所有Key,而是采用一种简单的贪心策略:

  1. 从过期字典中随机选择20个key
  2. 删除这20个key中的过期key
  3. 如果删除的key比率超过1/4,重复步骤1

同时为了避免key过期时间一致导致循环时间过长,还增加了一次扫描不超过25ms的限制,但是这个限制仅仅在过期key不那么多或TPS较低时有效,如果过期key很庞大同时TPS很高,那么这个限制就不那么有效了,比如同时过期一大批Key,此时有101个指令打了过来,Redis是单线程的,那么每个指令都需要卡顿25ms,依次下去,等到该101个指令执行时已经过去2500ms了。因此在涉及大批量key的过期时间比如一些促销活动持续到哪天,那后设置促销数据的过期时间,一定要在目标过期时间的基础上加一个随机值,避免同时过期。

从库不会进行过期扫描,从库对过期key的处理时被动的,主库过期删除后会同步给从库一个del指令,从库根据这条指令删除相应数据。因为Redis主从同步的异步特性,所以如果出现故障master节点宕机主从切换,del指令没有来得及同步从库,就会出现已经过期的数据在新的master节点上还在的情况。



淘汰策略

当Redis使用的内存超过物理内存限制时,内存数据就会频繁和磁盘交换,磁盘的读写速度会让Redis的性能急剧下降,基本上等于不可用。为了避免这种情况,Redis提供了配置参数maxmemory来限制内存超过期望大小。

当实际内存超过maxmemory时,Redis提供了几种策略(maxmemory-policy)来让用户选择采取何种方案应对:

  • noeviction:拒绝写请求,读请求正常访问。这样保证不会丢失数据,但是可能会导致业务不能继续进行。也是默认的淘汰策略
  • volatile-lru: 尝试淘汰设置了过期时间的key中距离上次使用间隔最久的key,不淘汰没设置过期时间的key,这样可以保证持久化数据不丢失。
  • volatile-ttl :尝试淘汰设置了过期时间的key中剩余生命最小的key。
  • volatile-random :从设置了过期时间的key中随机选择尝试淘汰
  • allkeys-lru :尝试淘汰所有key中距离上次使用间隔最久的key。
  • allkeys-random:从所有key中随机选择尝试淘汰.
  • volatile-lfu:redis4.0新增,尝试淘汰设置了过期时间的key中最近最少使用的key。
  • allkeys-lfu:redis4.0新增,尝试淘汰所有key中最近最少使用的key。

可以看到,volatile是针对设置了过期时间的key的,allkeys是针对所有key的,三种策略lru是根据活跃度排序,ttt是根据存活时间排序,random是随机。因为allkeys可能没有过期时间,所以没有ttl策略。剩下的组合加上默认的noeviction组成所有的淘汰策略。

可以根据是否使用Redis的持久化结合实际场景来选择使用哪种策略。

LRU(Least Recently Used)算法:维护一个链表,当字典中的某个元素被访问时,就把这个元素对应的key挪到链表头部,链表的顺序就是活跃度的顺序。空间满了时从尾部开始剔除。

Redis的lru采用的是一种近似LRU算法,不直接采用LRU算法是因为单独唯一个lru链表需要付出大量的额外内存,实时维护也会影响Redis的处理效率。Redis为每个key增加了一个24bit的额外小字段用来存储最近访问的时间戳。LRU淘汰是懒惰处理方式,只有写操作后内存大于maxmemory时才会执行LRU淘汰,此时会根据策略从设置了过期时间的key或者所有key中随机采样5(可配置)个key,然后淘汰掉最旧的key,如果还超过maxmemory,那么就继续采样淘汰。设置的采样样本量越大,就越接近严格的LRU算法。在Redis3.0中还新增了淘汰池,进一步提升近似LRU算法,淘汰池是一个数组,大小是maxmemory_samples,每一次采样淘汰时,先将选出来的key与淘汰池中的key结合,淘汰掉最旧的的一个key,然后将剩下的key整合到淘汰池等待下一个循环。

Redis的LFU一方面采用了LFUDA的理念,加入了衰减机制避免瞬时高峰数据无法被清除,另一个方面则是加入了权重机制,分为全局权重和单例权重两个,单例权重下一个key访问频率越大,那么统计次数的新增越慢。全局权重则是对于整个Redis来说,访问流量越大,统计次数的增加也越慢。



Redis的异步删除

首先要了解一个事情:Redis4.0修改了原来的单线程模型,变成了多线程。但是这个多线程模型仅仅是新增了几个处理后台任务的异步线程。提供服务执行指令的依然是单线程,被称为主线程。所以还称得上是单线程服务。这个修改的目的就是避免那些那些耗时较久且与服务无关的任务影响Redis对外提供的服务。另外提一下Redis6.0在为了应对网络的限制在网络IO上也变成了多线程,但是提供服务的线程也依然是单线程。

比如回收内存时使用del指令直接释放内存,一般情况下这个指令非常快,没有明显的延迟,但是如果是删除的是一个大key,删除时间就会提升,导致Redis卡顿。

在异步处理的基础上Redis4.0引用unlink指令,将删除操作的回收内存阶段变为异步处理的方式。

>unlink key
OK

unlink指令执行后,key将不可达,主线程无法访问避免了并发问题。之后内存的回收由异步线程进行回收。

unlink时主线程将目标key改为不可达后,就创建一个异步的内存回收任务到异步队列中去,后台线程从异步队列里面取任务进行处理。因为异步队列主线程和后台线程都会操作,所以是一个线程安全的队列(主要是取,因此塞的时间就一个线程)。但是并不是所有的unlink都会异步处理,如果key是一个小key,Redis会在unlink时直接删除,和del一样。

除此之外对于清空数据库的flushdb和flushall指令,Redis4.0也增加了异步处理的功能,只需在指令后增加async参数:

>flushall async
OK

还有AOF持久化时将缓存中的数据刷入磁盘的fsync操作也很消耗时间,影响服务,所以Redis4.0也一起将其改为异步的模式。而且AOF fsync有专属的异步线程和任务队列,不和上面提到惰性删除共用。

Redis4.0虽然提供多个不影响服务操作的异步机制,但是并不是默认开启的,如果要使用需要通过配置启用相应的异步机制:

  • slave-lazy-flush:从库全量同步时接收完rdb文件后立即进行的flush操作是否异步
  • lazyfree-lazy-eviction:内存到达maxmemory后的淘汰操作
  • lazyfree-lazy-expire key :过期删除操作
  • lazyfree-lazy-server-del rename:删除指令和rename指令


PS:



【JAVA核心知识】系列导航 [持续更新中…]


关联导航:

Redis应用篇


关联导航:

Redis基础篇


关联导航:

Redis拓展篇


关联导航:

Redis集群篇


欢迎关注…


参考资料:


《Redis深度历险》



版权声明:本文为yue_hu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。