Redis内存管理
Redis是一个基于内存的键值数据库,内存大小有限,随着要缓存的数据量越来越大,有限的内存空间不可避免地会被写满,此时应该怎么办呢?放心,Redis内部帮助我们实现了内存管理。
Redis回收内存大致有两个机制:过期策略和内存淘汰策略。一个是删除到达过期时间的键值对象,一个是当Redis已使用内存达到设置的maxmemory大小时,将触发内存淘汰策略,强制删除选择出来的键值对象。
一、过期策略
1、定期删除
Redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的key。
优点:节约内存,每隔一段时间就清除过期的key,快速释放掉不必要的内存占用。
缺点:清除key这个操作需要用到CPU资源,相当于每隔一段时间就要占用CPU,CPU压力过大,会影响Redis服务器的响应时间和指令吞吐量。
总结:用处理器性能换取存储空间(拿时间换空间)
2、惰性删除
只有当访问一个key时,才会判断该key是否已过期,过期则清除。
优点:节约CPU性能,只有访问key时,才会使用CPU。
缺点:内存压力过大,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
总结:用存储空间换取处理器性能(拿空间换时间)
二、内存淘汰策略
1、既然Redis已经存在过期策略,为什么还需要淘汰策略呢?
因为不管是定期删除还是惰性删除都不是一种完全精准的删除,还是会存在key没有被删除掉的场景,所以就需要内存淘汰策略进行补充。
2、简单介绍
当Redis已使用内存达到设置的maxmemory大小时,将触发内存淘汰策略,来清理内存。
3、maxmemory参数
(1).maxmemory如何设置?
maxmemory参数可以在redis.conf中配置,可以在运行时使用命令CONFIG SET动态设置。
maxmemory 1GB
CONFIG SET maxmemory 1GB
(2).maxmemory的大小应该设置成多大?
Redis 缓存使用内存来保存数据,避免业务应用从后端数据库中读取数据,可以提升应用的响应速度。那么是否可以将所有数据都放在Redis内存中呢?
这样做是不可行的。原因有二:
1、内存的价格相对于磁盘要贵的多,这样做虽然可以提高程序性能,但性价比太低。假设有1TB数据,1TB 内存的价格大约是 3.5 万元,而 1TB 磁盘的价格大约是 1000 元,所以最好是将全部数据存储到MySQL中做数据的留底,一部分经常访问的数据放到Redis中。
2、数据访问都是有局部性的,也就是我们通常所说的“八二原理”,80% 的请求实际只访问了 20% 的数据。所以将所有数据都存储到内存中,并没有必要。
“八二原理”只能说是一个经验,不一定在所有场景都生效,因为 20% 的数据不一定能贡献 80% 的访问量,我们不能简单地按照“总数据量的 20%”来设置缓存最大空间容量。在实践过程中,我看到过的缓存容量占总数据量的比例,从 5% 到 40% 的都有。这个容量规划不能一概而论,是需要结合应用数据实际访问特征和成本开销来综合考虑的。
总结:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,我会建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。
4、内存淘汰过程
1、客户端执行一个新指令添加数据。
2、Redis检查内存使用量,如果大于maxmemory限制,就通过内存淘汰策略清理内存。
3、执行新命令,重复上述过程。
5、八种内存淘汰策略
Redis 4.0之前一共实现了6种内存淘汰策略,在4.0之后,又增加了2种策略。我把这 8 种策略的分类,画到了一张图里:
Redis内存淘汰策略分类1.noeviction:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,就返回error。这是默认的淘汰策略。
2.volatile-lru:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置了过期时间的key,淘汰一些最近未使用的key。
3.volatile-random:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置了过期时间的key,随机淘汰一些key。
4.volatile-ttl:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置了过期时间的key,淘汰一些即将过期的key。
5.volatile-lfu:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置了过期时间的key,淘汰一些使用频率最低的key。
6.allkeys-lru:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,就会扫描所有的key,淘汰一些最近最少使用的key。
7.allkeys-random:添加数据时,如果Redis判断该操作会导致占用内存大小超过内存限制,就会扫描所有的key,随机淘汰一些key。
8.allkeys-lfu:添加数据时,如果redis判断该操作会导致占用内存大小超过内存限制,扫描那些设置了过期时间的key,淘汰一些最近最少使用的key。
6、内存淘汰策略的选择
(1).优先使用 allkeys-lru 策略,这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提高Redis的命中率,提升应用的访问性能。
(2).当Redis中的数据有一部分是我们经常访问的,有一部分是几乎访问不到的,即业务数据有明显的冷热数据区分,建议使用 allkeys-lru 策略,将最近最少使用的数据删除(冷数据),保留经常使用的数据(热数据)。
(3).当Redis中的数据基本都会被访问到,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
(4).当我们想精准淘汰一些超时数据(时间>=key设置的超时时间)时,建议使用volatile-ttl。
(5).如果你的业务有置顶的需求,比如置顶新闻、置顶视频,这些置顶的数据是永久保留的。此时的做法是,不给这些置顶数据设置过期时间,同时使用 volatile-lru 策略,这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
三、浅谈LRU算法
1、LRU算法简单介绍
LRU 算法的全称是 Least Recently Used,从名字上就可以看出,LRU算法是按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来,而最近频繁使用的数据会留在缓存中。
那么Redis是怎么进行数据筛选的呢?LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。
LRU算法演示图
链表初始数据有6、3、9、20、5。如果数据 20 和 3 被先后访问,它们都会从现有的链表位置移到 MRU 端,而链表中在它们之前的数据则相应地往后移一位。因为,LRU 算法选择删除数据时,都是从 LRU 端开始,所以把刚刚被访问的数据移到 MRU 端,就可以让它们尽可能地留在缓存中。
现在有一个新数据 15 要被写入缓存,但此时Redis中已经没有缓存空间了,也就是链表没有空余位置了,那么,LRU 算法做两件事:一是,数据 15 是刚被访问的,所以它会被放到 MRU 端。二是,算法把 LRU 端的数据 5 从缓存中删除,相应的链表中就没有数据 5 的记录了。
所以,LRU 算法的思想就是:它认为刚刚被访问的数据,肯定还会被再次访问,所以就把它放在 MRU 端;长久不访问的数据,肯定就不会再被访问了,所以就让它逐渐后移到 LRU 端,在缓存满时,就优先删除它。
2、LRU算法的缺陷
LRU 算法在实际实现时,需要用链表管理所有的缓存数据,这会带来额外的空间开销。
当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
刚刚使用的数据移动到表头,后面的数据都要向后移动,如果链表很长很长,那么就要移动好久好久,是个很耗时的操作。
3、Redis对LRU算法的改进
所以,在 Redis 中,LRU 算法被做了简化,以减轻数据淘汰对缓存性能的影响。具体来说,Redis 默认会记录每个数据的最近一次访问的时间戳(键值对数据结构 RedisObject 中的 lru 字段)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。这样,就将一个超级大的链表分割成了无数个小链表,分治的思想,提高了淘汰的效率。
Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数 N。例如,我们执行如下命令,可以让 Redis 选出 100 个数据作为候选数据集:
CONFIG SET maxmemory-samples 100
当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。
这样一来,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时要移动好长的链表项,提升了缓存的性能。
四、浅谈LFU算法
前面提到的LRU算法看似已经是比较好的算法了,但实际上存在一些缺陷,因为存在某些key,在最近被访问过后就不再访问了,而有些key是很久以前访问过,但是以后很可能也要访问的,如果根据LRU算法,就会导致前一类key保留下来,反而后一类真正需要的key就被淘汰了,因此LFU算法就是该问题的解决方式。
LFU算法在Redis中是通过一个计数器来实现的,每个key都有一个计数器,访问频度越高,计数器的值就越大,Redis就根据计数器的值来淘汰key,当然计数器的值也是会随着时间减少的。
使用LFU算法时,有两个可配置参数:lfu-log-factor和lfu-decay-time。
lfu-log-factor:默认值是10。使用它可以调整计数器计数值的增长速度,lfu-log-factor越大,计数值增长的越慢,因此就要求key的访问频度较高才能避免被淘汰。
lfu-decay-time:衰减时间默认是1,它表示隔多久将计数值减1,使用它可以调整计数值的衰减速度。长时间不读取key的话,其计数值是需要进行衰减的。
下面是不同的lfu-log-factor情况下的计数值的增长情况,key的计数值不是每访问一次key都会增加1,它是和lfu-log-factor有关系的,计数值的最大值是255,lfu-log-factor越大,计数器的值增长就越难,如下图。
lfu-log-factor示意图文章借鉴内容:极客时间《Redis核心技术与实战》