键驱逐
Redis键淘汰策略概述(LRU、LFU等)
Redis 通常用作缓存,以加快对较慢服务器或数据库的读取访问。由于缓存条目是持久存储数据的副本,当缓存内存不足时,通常可以安全地驱逐它们(如果需要,将来可以再次缓存)。
Redis 允许您指定一个驱逐策略,当缓存的大小超过设定的内存限制时自动驱逐键。每当客户端运行一个向缓存添加更多数据的新命令时,Redis 会检查内存使用情况。如果内存使用量超过限制,Redis 会根据所选的驱逐策略驱逐键,直到使用的总内存回到限制以下。
请注意,当一个命令向缓存中添加大量数据时(例如,一个大的集合交集存储到一个新键中),这可能会暂时超出限制,且超出量可能很大。
以下部分解释了如何配置缓存的内存限制,并描述了可用的驱逐策略以及何时使用它们。
使用maxmemory
配置指令
maxmemory
配置指令指定用于缓存数据的最大内存量。您可以在启动时使用redis.conf
文件设置maxmemory
。例如,要配置100兆字节的内存限制,您可以在redis.conf
中使用以下指令:
maxmemory 100mb
你也可以使用CONFIG SET
来在运行时通过redis-cli
设置maxmemory
:
> CONFIG SET maxmemory 100mb
将maxmemory
设置为零以指定您不想限制数据集的内存。这是64位系统的默认行为,而32位系统使用3GB的隐式内存限制。
当你的缓存大小超过maxmemory
设置的限制时,Redis将执行你选择的eviction policy以防止缓存进一步增长。
为复制或持久化实例设置maxmemory
如果您为服务器使用
复制
或持久化,
Redis将使用一些RAM作为缓冲区来存储等待写入副本或AOF文件的更新集。
此缓冲区使用的内存不包括在与maxmemory
进行比较以查看是否需要驱逐的总内存中。
这是因为键的驱逐本身会生成必须添加到缓冲区的更新。如果这些更新被计入已用内存,那么在某些情况下,通过驱逐键节省的内存会立即被添加到缓冲区的更新数据所占用。这反过来会触发更多的驱逐,由此产生的反馈循环可能会不必要地从缓存中驱逐许多项目。
如果您正在使用复制或持久化,我们建议您设置maxmemory
以留出一些空闲的RAM来存储缓冲区。请注意,这对于noeviction
策略来说是不必要的(有关驱逐策略的更多信息,请参见下面的部分)。
INFO
命令在 memory
部分返回一个 mem_not_counted_for_evict
值(你可以使用 INFO memory
选项来仅查看此部分)。这是当前由缓冲区使用的内存量。尽管确切的数量会有所不同,但你可以使用它来估计在设置 maxmemory
之前应从总可用 RAM 中减去多少。
驱逐策略
使用maxmemory-policy
配置指令来选择当达到maxmemory
设置的限制时你想要使用的驱逐策略。
以下策略可用:
noeviction
: 不会驱逐键,但当你尝试执行缓存新数据的命令时,服务器将返回错误。如果你的数据库使用复制,那么这种情况仅适用于主数据库。请注意,仅读取现有数据的命令仍然正常工作。allkeys-lru
: 淘汰最近最少使用 (LRU) 的键。allkeys-lfu
: 淘汰最少使用 (LFU) 的键。allkeys-random
: 随机淘汰键。volatile-lru
: 驱逐最近最少使用的键,这些键的expire
字段设置为true
。volatile-lfu
: 驱逐设置了expire
字段为true
的最不常用的键。volatile-random
: 仅在键设置了expire
字段为true
时,随机驱逐键。volatile-ttl
: 驱逐设置了expire
字段为true
且剩余生存时间(TTL)值最短的键。
如果没有键将expire
字段设置为true,或者对于volatile-ttl
,如果没有键设置了生存时间值,则volatile-xxx
策略的行为类似于noeviction
。
您应该选择一个适合您应用程序访问键的方式的驱逐策略。您可能能够提前预测访问模式,但您也可以在运行时使用INFO
命令中的信息来检查或改进您的策略选择(有关更多信息,请参见下面的使用INFO
命令)。
作为经验法则:
- 当您预计一部分元素会被访问得比其他元素频繁得多时,使用
allkeys-lru
。根据帕累托原则,这是一种非常常见的情况,因此如果您没有理由偏好其他选项,allkeys-lru
是一个很好的默认选择。 - 当您预期所有键的访问频率大致相同时,请使用
allkeys-random
。例如,当您的应用程序以重复周期读取数据项时。 - 如果你的代码可以估计哪些键是驱逐的好候选者,并为它们分配较短的TTL,请使用
volatile-ttl
。还要注意,如果你很好地利用了键的过期功能,那么你不太可能遇到缓存内存限制,因为键通常会在需要被驱逐之前过期。
volatile-lru
和 volatile-random
策略主要在你想要使用单个 Redis 实例同时进行缓存和持久化键集时非常有用。然而,在这种情况下,如果可能的话,你应该考虑运行两个独立的 Redis 实例。
还要注意,为键设置expire
值会消耗内存,因此像allkeys-lru
这样的策略在内存效率上更高,因为它不需要expire
值来操作。
使用 INFO
命令
INFO
命令提供了几项数据,这些数据对于检查缓存的性能非常有用。特别是,INFO stats
部分包括两个重要的条目,keyspace_hits
(在缓存中成功找到键的次数)和 keyspace_misses
(请求键但未在缓存中的次数)。下面的计算给出了从缓存中满足的尝试访问的百分比:
keyspace_hits / (keyspace_hits + keyspace_misses) * 100
检查这是否大致符合您对应用程序的预期 (自然,更高的百分比表示更好的缓存性能)。
EXISTS
命令报告一个键不存在时,这将被计为键空间未命中。如果命中率低于预期,这可能意味着你没有使用最佳的淘汰策略。例如,如果你认为一小部分“热”数据(可以轻松放入缓存)应该占访问量的75%左右,你可以合理地预期键空间命中率大约为75%。如果实际百分比较低,请检查evicted_keys
的值(也由INFO stats
返回)。高比例的淘汰可能表明你选择的策略过于频繁地淘汰了错误的键(因此allkeys-lru
可能是一个不错的选择)。如果evicted_keys
的值较低并且你使用了键过期,请检查expired_keys
以查看有多少键已过期。如果这个数字很高,你可能使用了过低的TTL,或者你选择了错误的键来过期,这导致键在应该消失之前就从缓存中消失了。
由INFO
返回的其他有用信息包括:
used_memory_dataset
: (memory
部分) 用于缓存数据的内存量。如果这个值大于maxmemory
,那么差值就是超过maxmemory
的量。current_eviction_exceeded_time
: (stats
部分) 自缓存上次开始超过maxmemory
以来的时间。commandstats
部分:除其他外,这部分报告了每个发给服务器的命令被拒绝的次数。如果你正在使用noeviction
或其中一个volatile_xxx
策略,你可以使用这个来找出哪些命令被maxmemory
限制阻止以及这种情况发生的频率。
近似LRU算法
Redis的LRU算法使用最近最少使用键的近似值,而不是精确计算它们。它随机抽取少量键,然后淘汰自上次访问以来时间最长的键。
从 Redis 3.0 开始,该算法还跟踪一个用于驱逐的良好候选池。这提高了算法的性能,使其接近真正的 LRU 算法。
您可以通过更改每次驱逐前检查的样本数量来调整算法的性能,使用maxmemory-samples
配置指令:
maxmemory-samples 5
Redis不使用真正的LRU实现的原因是因为它会消耗更多的内存。然而,对于使用Redis的应用程序来说,近似值实际上是等价的。此图比较了Redis使用的LRU近似值与真正的LRU。
生成上述图表的测试用给定数量的键填充了一个Redis服务器。这些键从第一个到最后一个被访问。使用LRU算法时,前几个键是最佳的淘汰候选。随后添加了超过50%的键,以强制淘汰一半的旧键。
你可以在图中看到三种点,形成了三个不同的带。
- 浅灰色带是被驱逐的对象。
- 灰色带表示未被驱逐的对象。
- 绿色带表示已添加的对象。
在理论上的LRU实现中,我们预期在旧键中,前一半将会过期。而Redis的LRU算法只会概率性地使旧键过期。
正如你所看到的,与Redis 2.8相比,Redis 3.0在5个样本的情况下表现更好,然而大多数最近访问的对象仍然被Redis 2.8保留。在Redis 3.0中使用10个样本大小,近似值非常接近Redis 3.0的理论性能。
请注意,LRU只是一个预测给定键在未来被访问的可能性的模型。此外,如果你的数据访问模式非常接近幂律分布,大多数访问将集中在LRU近似算法能够很好处理的键集合中。
在模拟中我们发现,使用幂律访问模式时,真正的LRU和Redis近似之间的差异很小或不存在。
然而,你可以将样本大小增加到10,代价是一些额外的CPU使用,以接近真正的LRU,并检查这是否会影响你的缓存未命中率。
要在生产环境中使用不同的样本大小值进行实验,使用CONFIG SET maxmemory-samples
命令非常简单。
LFU 淘汰策略
从Redis 4.0开始,最少使用淘汰模式可用。在某些情况下,这种模式可能工作得更好(提供更好的命中/未命中比率)。在LFU模式下,Redis会尝试跟踪项目的访问频率,因此很少使用的项目会被淘汰。这意味着经常使用的键有更高的机会保留在内存中。
要配置LFU模式,以下策略可用:
volatile-lfu
在设置了过期时间的键中使用近似的LFU算法进行淘汰。allkeys-lfu
使用近似的LFU算法淘汰任何键。
LFU(最近最少使用)算法类似于LRU(最近最少使用)算法:它使用一个称为Morris计数器的概率计数器,通过每个对象仅使用几位来估计对象的访问频率,并结合一个衰减周期,以便计数器随时间减少。在某些时候,我们不再希望将键视为频繁访问的,即使它们在过去是频繁访问的,这样算法可以适应访问模式的变化。
该信息的采样方式与LRU类似(如本文档前一节所述),用于选择要淘汰的候选对象。
然而与LRU不同,LFU具有某些可调参数:例如,如果一个频繁访问的项目不再被访问,它的排名应该以多快的速度下降?也可以调整Morris计数器的范围,以更好地使算法适应特定的使用场景。
默认情况下,Redis 配置为:
- 将计数器饱和在大约一百万次请求。
- 每分钟衰减计数器。
这些应该是合理的值,并且已经通过实验测试,但用户可能希望调整这些配置设置以选择最佳值。
关于如何调整这些参数的说明可以在源代码分发的示例redis.conf
文件中找到。简而言之,它们是:
lfu-log-factor 10
lfu-decay-time 1
衰减时间是显而易见的,它是计数器在被采样并发现比该值更旧时应衰减的分钟数。特殊值0
表示:我们永远不会衰减计数器。
计数器对数因子改变了使频率计数器饱和所需的命中次数,该计数器的范围仅为0-255。因子越高,达到最大值所需的访问次数越多。因子越低,对于低访问量,计数器的分辨率越好,如下表所示:
+--------+------------+------------+------------+------------+------------+
| factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
+--------+------------+------------+------------+------------+------------+
| 0 | 104 | 255 | 255 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 1 | 18 | 49 | 255 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 10 | 10 | 18 | 142 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 100 | 8 | 11 | 49 | 143 | 255 |
+--------+------------+------------+------------+------------+------------+
所以基本上这个因素是在更好地区分访问量低的项目和区分访问量高的项目之间进行权衡。更多信息可以在示例redis.conf
文件中找到。