关于Redis的内存淘汰策略详解
作者:长安明月
一、什么是内存淘汰?
如果在做项目时,不计任何后果地把任何数据都往 Redis 写入,使用不合理很容易导致数据超过Redis 的最大内存,这种情况就会导致如下问题。
- Redis 中有很多无效的缓存,这些缓存数据会降低 IO 性能。
- 随着系统的运行,Redis 的数据越来越多,会导致物理内存不足。虽然可以通过使用虚拟内存,将很少访问的数据交换到磁盘上,腾出内存空间的方法来解决物理内存不足的问题,但是由于这部分数据存储在磁盘上,如果在高并发场景中,频繁访问虚拟内存空间会严重降低系统性能。
所以遇到 Redis 内存不足的问题时,我们一般有几种方法:
- 对每个存储到 Redis 中的 key 设置过期时间,这个根据实际业务场景来决定。否则,再大的内存都会随着系统运行被消耗完;
- 增加内存;
- 使用内存淘汰策略。
当内存空间使用达到限制时,Redis 会根据配置策略来选择不同处理方式,要么返回 errors,要么按照不同的策略算法来清除一些旧数据,达到回收内存的目的,这就是 Redis 的内存淘汰,有些文章中,内存淘汰也叫缓存回收。
本文以 Linux 系统安装的 4.0.8 版本的 Redis 为例,对内存淘汰策略进行总结。Redis 的最大内存上限、淘汰算法的配置都在 redis.conf 文件中有说明,现在把 redis.conf 中对内存管理的部分内容摘录如下。
############################## MEMORY MANAGEMENT ################################ # Set a memory usage limit to the specified amount of bytes. # When the memory limit is reached Redis will try to remove keys # according to the eviction policy selected (see maxmemory-policy). # # If Redis can't remove keys according to the policy, or if the policy is # set to 'noeviction', Redis will start to reply with errors to commands # that would use more memory, like SET, LPUSH, and so on, and will continue # to reply to read-only commands like GET. # # This option is usually useful when using Redis as an LRU or LFU cache, or to # set a hard memory limit for an instance (using the 'noeviction' policy). # # WARNING: If you have slaves attached to an instance with maxmemory on, # the size of the output buffers needed to feed the slaves are subtracted # from the used memory count, so that network problems / resyncs will # not trigger a loop where keys are evicted, and in turn the output # buffer of slaves is full with DELs of keys evicted triggering the deletion # of more keys, and so forth until the database is completely emptied. # # In short... if you have slaves attached it is suggested that you set a lower # limit for maxmemory so that there is some free RAM on the system for slave # output buffers (but this is not needed if the policy is 'noeviction'). # # maxmemory <bytes> # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory # is reached. You can select among five behaviors: # # volatile-lru -> Evict using approximated LRU among the keys with an expire set. # allkeys-lru -> Evict any key using approximated LRU. # volatile-lfu -> Evict using approximated LFU among the keys with an expire set. # allkeys-lfu -> Evict any key using approximated LFU. # volatile-random -> Remove a random key among the ones with an expire set. # allkeys-random -> Remove a random key, any key. # volatile-ttl -> Remove the key with the nearest expire time (minor TTL) # noeviction -> Don't evict anything, just return an error on write operations. # # LRU means Least Recently Used # LFU means Least Frequently Used # # Both LRU, LFU and volatile-ttl are implemented using approximated # randomized algorithms. # # Note: with any of the above policies, Redis will return an error on write # operations, when there are no suitable keys for eviction. # # At the date of writing these commands are: set setnx setex append # incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby # zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby # getset mset msetnx exec sort # # The default is: # # maxmemory-policy noeviction # LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated # algorithms (in order to save memory), so you can tune it for speed or # accuracy. For default Redis will check five keys and pick the one that was # used less recently, you can change the sample size using the following # configuration directive. # # The default of 5 produces good enough results. 10 Approximates very closely # true LRU but costs more CPU. 3 is faster but not very accurate. # # maxmemory-samples 5
二、Redis 内存上限
Redis 的最大内存上限可以在配置文件 redis.conf 中配置,redis.conf 配置如下:
# maxmemory <bytes>
设置 maxmemory 为 0 表示没有内存限制。在 64 位系统中,默认是 0 无限制,但是在 32 位系统中默认是 3GB。
三、Redis 内存淘汰策略
Redis 提供了一种内存淘汰策略,当内存不足时,Redis 会根据相应的淘汰规则对 key 数据进行淘汰。 Redis 的内存淘汰策略共有 8 种具体的淘汰策略,默认的策略为 noeviction,当内存使用达到阈值的时候, 所有引起申请内存的命令会报错。8 种淘汰策略如下所示。
- volatile-lru:当内存不足时,从设置了过期时间的 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;
- allkeys-lru:当内存不足时,从所有 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;
- volatile-lfu:当内存不足时,从设置了过期时间的 key 中使用 LFU 算法,选出使用频率最低的数据进行淘汰;
- allkeys-lfu:当内存不足时,从所有 key 中使用 LFU 算法,选出使用频率最低的数据,进行淘汰;
- volatile-random:当内存不足时,从设置了过期时间的 key 中,随机选出数据进行淘汰;
- allkeys-random:当内存不足时,从所有的 key 中,随机选出数据进行淘汰;
- volatile-ttl:当内存不足时,从设置了过期时间的 key 中,选出即将过期的数据(按照过期时间的先后,选出最先过期的数据)进行淘汰;
- noeviction: 当内存不足时,禁止淘汰数据,写入操作报错。这是 Redis 默认的内存淘汰策略。
前缀为 volatile- 和 allkeys- 的区别在于二者选择要清除的键时的字典不同,volatile- 前缀的策略表示从 redisDb 中的过期字典中选择键进行清除;allkeys- 开头的策略代表从 dict 字典中选择键进行清除。
策略中用到的两种算法:
- LRU(Least Recently Used):最近最少使用。优先淘汰使用时间最远的数据。
- LFU(Least Frequently Used):最小频率使用。优先淘汰最不常用的 Key。
四、内存淘汰的具体工作步骤
- 客户端执行一条新命令,导致数据库需要增加数据(比如 set key value);
- Redis 会检查内存使用情况,如果内存使用超过 maxmemory 设置的值,就会按照内存淘汰策略删除一些 key;
- 新的命令执行成功。
五、LRU 算法及在 Redis 中的改进
5.1 LRU 算法
LRU 是 Least Recently Used 的缩写,也就是表示最近最少使用,也可以理解成最久没有使用。也就是说当内存不够的时候,每次添加一条数据,都需要抛弃一条最久时间没有使用的旧数据。标准的 LRU 算法为了降低查找和删除元素的时间复杂度,一般采用 Hash 表和双向链表结合的数据结构,Hash 表可以快速查找到某个 key 是否存在链表中,同时可以快速删除、添加节点,如下图所示。
双向链表的查找时间复杂度是 O(n),删除和插入是 O(1),借助 HashMap 结构,可以使得查找的时间复杂度变成 O(1)。 Hash 表用来查询在链表中的数据位置,链表负责数据的插入,当新数据插入到链表头部时有两种情况。
- 链表满了,把链表尾部的数据丢弃掉,新加入的缓存直接加入到链表头中。
- 当链表中的某个缓存被命中时,直接把数据移到链表头部,原本在头节点的缓存就向链表尾部移动。
这样,经过多次 Cache 操作之后,最近被命中的缓存,都会存在链表头部的方向,没有命中的,都会在链表尾部方向,当需要替换内容时,由于链表尾部是最少被命中的,我们只需要淘汰链表尾部的数据即可。
5.2 Redis 中的 LRU 算法
实际上,Redis 使用的 LRU 算法是一种不可靠的 LRU 算法,它实际淘汰的键并不一定是真正最少使用的数据。它的工作机制是:
- 随机采集淘汰的 key,每次随机选出 5 个key;
- 然后淘汰这 5 个 key 中最少使用的 key。
这 5 个 key 是默认的个数,具体的数值可以在 redis.conf 中配置。
maxmemory-samples 5
当近似 LRU 算法的抽样值越大时就越接近真实的 LRU 算法,因为取值越大获取的数据越完整,淘汰的数据就更加接近最少使用的数据,但是消耗的 CPU 更多。抽样值越小,算法运行更快,但是准确度越低。这里涉及一个权衡问题,如果需要在所有的数据中搜索最符合条件的数据,那么一定会增加系统的开销。Redis 是单线程的,所有耗时的操作都要谨慎一些。为了在一定成本内实现相对的 LRU,早期的 Redis 版本是基于采样的 LRU,也就是放弃了从所有数据中搜索解,改为在采样空间搜索最优解。Redis 3.0 版本之后,Redis 作者对基于采样的 LRU 进行了一些优化:
- Redis 中维护一个大小为 16 的候选池,当第一次随机选取采样数据时,会把数据放入到候选池中,并且候选池中的数据会根据 key 的空闲时间进行排序。
- 当第二次以后选取数据时,只有大于候选池内最小空闲时间的 key 才会被放进候选池(空闲时间越大,代表越久没有被使用,准备淘汰))。
- 当候选池的数据满了之后,那么空闲时间最大的 key 就会被挤出候选池。当执行淘汰时,直接从候选池中选取空闲时间最大的 key 进行淘汰。
如下图所示,首先从目标字典中采集出 maxmemory-samples 个键,缓存在一个 samples 数组中,然后从 samples 数组中一个个取出来,和回收池中的键进行键的空闲时间比较,从而更新回收池。在更新过程中,首先遍历找到每个键的实际插入位置 x,然后根据不同情况进行处理。
- 回收池满了,并且当前插入的 key 的空闲时间最小(也就是回收池中的所有 key 都比当前插入的 key 的空闲时间大),则不作任何操作。
- 回收池未满,并且插入的位置 x 没有键,则直接插入即可。
- 回收池未满,且插入的位置 x 原本已经存在要淘汰的键,则把第 x 个以后的元素都往后挪一个位置,然后再执行插入操作。
- 回收池满了,将当前第 x 个以前的元素往前挪一个位置(实际就是淘汰了),然后执行插入操作。
这样做的目的是能够选出最真实的最少被访问的 key,能够正确选择不常使用的 key。因为在Redis 3.0 之前是随机选取样本,这样的方式很有可能不是真正意义上的最少访问的 key。LRU 算法有一个弊端,假如一个 key 值访问频率很低,但是最近一次被访问到了,那 LRU 会认为它是热点数据,不会被淘汰。同样,经常被访问的数据,最近一段时间没有被访问,这样会导致这些数据被淘汰掉,导致误判而淘汰掉热点数据,于是在 Redis 4.0 中,新加了一种 LFU 算法。
六、LFU
LFU(Least Frequently Used),表示最小频率使用,它和 key 的使用次数有关。其思想是:根据 key 最近被访问的频率进行淘汰,比较少访问的 key 优先淘汰,反之则保留。LFU 的原理是使用计数器来对 key 进行排序,每次 key 被访问时,计数器会增大,当计数器越大,意味着当前 key 的访问越频繁,也就是意味着它是热点数据。 它很好的解决了 LRU 算法的缺陷:一个很久没有被访问的 key,偶尔被访问一次,导致被误认为是热点数据的问题。LFU 的实现原理如下图所示,LFU 维护了两个链表,横向组成的链表用来存储访问频率,每个访问频率的节点下存储另外一个具有相同访问频率的缓存数据。具体的工作原理是:
- 当添加元素时,找到相同访问频次的节点,然后添加到该节点的数据链表的头部。如果该数据链表满了,则移除链表尾部的节点;
- 当获取元素或者修改元素时,都会增加对应 key 的访问频次,并把当前节点移动到下一个频次节点。
添加元素时,访问频率默认为 1,随着访问次数的增加,频率不断递增。而当前被访问的元素也会随着频率增加进行移动。
到此这篇关于关于Redis的内存淘汰策略详解的文章就介绍到这了,更多相关Redis内存淘汰策略内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!