likes
comments
collection
share

Redis(7) 淘汰策略

作者站长头像
站长
· 阅读数 2

Redis(7)  淘汰策略

Redis淘汰策略

Redis是键值对内存存储,平时在工作中,用到的java比较多,java对于自己的内存有这一套完整的垃圾回收机制,为了避免内存过大。对于Redis这样的内存数据库,自然对自己的内存也有对应的策略。

从2个方面来看,Redis如何处理内存过高的问题,一是减少数据,通过淘汰键值对来减少内存,二是通过优化内存碎片来提高内存的利用率,三是通过硬件来完成扩展空间

键值对淘汰

Redis对键值对的淘汰,提供了8种策略 Redis(7)  淘汰策略

其中novection 策略就是不淘汰任何数据,所以一般不推荐配置这个

想要切换淘汰策略就在redis.conf文件中配置

maxmemory-policy volatile-lru

就可以配置对应的淘汰策略

过期时间的数据中淘汰

有4种淘汰策略,会选择在具有过期时间的数据中淘汰。

volatile-ttl

volatile-ttl 针对的是设置了过期时间的数据,把这些数据中剩余存活时间最短的筛选出来并淘汰掉。但是这样的方式,并不能保证我们淘汰的是一个非热点的数据。

volatile-random

volatile-random 针对的是设置了过期时间的数据,从中随机选取一个淘汰,这样的方式,具有不确定性,万一在高峰期淘汰了一个热点数据,那对我们的整体系统的性能会产生波动。

volatile-LRU

采用了LRU算法,来选择淘汰数据,也就是最近最少使用。在java中,有一个LinkedHashMap就实现了LRU的算法,也就是首先淘汰最长时间未被使用的数据。具体在Redis中的实现,是用了保存在RedisObject中一个lru的信息,这里保存了数据最近一次被访问的时间,于是在淘汰时,淘汰一个lru最小的。

这里是截取网上的一张图,形象的说明了整个过程 Redis(7)  淘汰策略

当然这里Redis没有说,去所有的数据中比较出一个最小值来,这样的做法明显不够Redis。因为知道LinkedHashMap的都知道,在实现LRU中,需要维持一个链表的结构,当有数据访问时,需要移动数据在链表中的位置,当有数据淘汰时,移除队尾的数据。在Redis中,我们把所有的数据都用链表维持起来,不太现实,而且会频繁的移动。所以Redis会取N个数据,来维护一个链表结构,当需要淘汰数据时,就会从N个数据中,采用LRU的方式去淘汰。当后续再需要淘汰时,会从所有的数据中选取比当前数据池中最小lru小的数据进入候选池。

N的数量可以通过maxmemory-samples修改

Redis的整体风格有一种以小见大的味道,后面的LFU和针对时间到期删除的数据都有体现。

volatile-LFU

采用了LFU算法来淘汰数据,最近最不常用。对每一个数据记录一个使用次数和使用的时间,优先淘汰使用次数最少的元素,假如使用次数一样的多,那在比较时间,使用时间最早的淘汰。

这里是截取网上的一张图,形象的说明了整个过程 Redis(7)  淘汰策略

Redis基于LRU的基础上改造,在RedisObject中只维护了一个lru的信息,并没有再额外维护一个count的信息来记录,所以这里把一个lru分成2部分来使用, ldt 值:lru 字段的前 16bit,表示数据的访问时间戳; counter 值:lru 字段的后 8bit,表示数据的访问次数。

这样的做法在编程中非常的场景,如java中的ReentrantReadWriteLock、线程池等,等有过类似的设计,把一个变量,分成2部分来表示。

当有了访问次数和访问时间后,那就可以完成LRU,当然也是基于N个候选区去淘汰的。但是依旧存在2个问题,一个是counter只有8位,最高可以到255,另一个是counter需要一个衰减机制,不然对于一个瞬时的热点数据来说,可能热点已经过去了不在需要这个数据了,但是由于他的counter比较高,所以他不会淘汰。

counter增加方案

对于redis来说,一个数据的访问次数是有可能非常高的,255次明显无法区分出来热点数据,所以Redis的做法还是以小见大,每当数据被访问一次时,首先,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。这样只要lfu_log_factor控制得当,数据需要访问很多很多次才能被+1,这样的情况下255可以代表一个非常大的访问量。并在初期赋予一个初始值5,防止因为刚创建次数少而立即被淘汰。下面是网上的一个测试数据

Redis(7)  淘汰策略

counter衰减方案

LFU策略会计算这一次访问的时间和上一次访问的时间的差值,并且差值除以 lfu_decay_time配置项,获得counter需要衰减的值,隔得越久,counter需要衰减的越多。

这个方案来看似乎是没有问题的,但是实际上,衰减的方案需要再次访问,才能触发,如果一个数据一开始的访问量很高,后面再也没有人访问,那么在业务上,我们是可以优先放弃这个数据的缓存的,可是在Redis淘汰时,并不是优先选择这个数据。

没有过期时间的数据中淘汰

allkeys-random

与volatile-ranom相似,变的只是,选取数据的范围变了,变成了所有key中选取

allkeys-LRU

与volatile-LRU相似,变的只是,选取数据的范围变了,变成了所有key中选取

allkeys-LFU

与volatile-LFU相似,变的只是,选取数据的范围变了,变成了所有key中选取

淘汰数据

上面这些是淘汰的策略,基于空间满的淘汰策略,Redis淘汰数据的时机有3种 1、内存满了

2、定时扫描,淘汰到期的数据

3、获取操作时,发现数据过期了,淘汰

其中1是我们上面讲的,2和3是Redis的过期策略,主动过期和被动过期。

主动过期

主动过期为访问时,发现过期了,回收key

被动过期

采用定时任务的方式,默认情况下,Redis 每 100 毫秒会删除一些过期 key,采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP(默认20个) 个数的 key,并将其中过期的 key 全部删除,如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。删除的操作是阻塞操作,也可以配合Redis4.0以后的惰性删除来减少影响。但是当大批量的key同时过期是,Redis是会影响主线程的调用,从而阻塞客户端的调用,造成Redis波动。当然这个操作是可以进行配置的,比如最高会占用多少时间等。

在这里Redis还是采用了以小见大的手法,采用N个key的过期情况来预估整个Redis中过期key的情况。

内存空间优化

除了当空间不够用的时候会进行数据淘汰,Redis也会尽量整理内存,减少内存的碎片,尽量的提高自己的利用率。

可以使用INFO memory命令来查看内存的使用情况,其中有一个mem_fragmentation_ratio 的指标,内存利用率,一般情况下在1<x<1.5之间是正常水平,大于1.5就说明我们的内存碎片较多了。

4.0-RC3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法,当然清理内存碎片是有代价的,类似于拼图,我们需要注意移动数据的顺序,在数据拷贝时,会阻塞,这就导致 Redis 无法及时处理请求,性能就会降低。当然也提供了几种给我们,可以灵活配置碎片的处理:

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理
  • active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展
  • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高

内存扩展

如果 mem_fragmentation_ratio 小于 1 了,mem_fragmentation_ratio小于1,说明used_memory_rss小于了used_memory,这意味着操作系统分配给Redis进程的物理内存,要小于Redis实际存储数据的内存,也就是说Redis没有足够的物理内存可以使用了,这会导致Redis一部分内存数据会被换到Swap中,之后当Redis访问Swap中的数据时,延迟会变大,性能下降。当内存一下子满了,Redis还来不及操作淘汰数据时,此时会借用swap来缓解一下压力。当然速度会相对变慢,对于swap技术可以了解一下这里先关链接