键值驱逐
Redis 键值驱逐策略概述(LRU、LFU 等)
当 Redis 用作缓存时,在添加新数据时让它自动驱逐旧数据通常很方便。这种行为在开发人员社区中很常见,因为它是在流行的 memcached 系统中的默认行为。
此页面涵盖了 Redis maxmemory
指令的更一般主题,该指令用于将内存使用量限制在固定数量。它还广泛涵盖了 Redis 使用的 LRU 驱逐算法,该算法实际上是精确 LRU 的近似值。
Maxmemory
配置指令
maxmemory
配置指令配置 Redis 使用指定数量的内存来存储数据集。可以使用 redis.conf
文件设置配置指令,或者稍后在运行时使用 CONFIG SET
命令设置。
例如,要配置 100 兆字节的内存限制,可以在 redis.conf
文件中使用以下指令
maxmemory 100mb
将maxmemory
设置为零表示没有内存限制。这是 64 位系统的默认行为,而 32 位系统使用 3GB 的隐式内存限制。
当达到指定的内存量时,如何配置**驱逐策略**将决定默认行为。Redis 可以针对可能导致使用更多内存的命令返回错误,或者它可以在每次添加新数据时驱逐一些旧数据以返回到指定的限制。
驱逐策略
当达到maxmemory
限制时,Redis 遵循的确切行为是使用maxmemory-policy
配置指令配置的。
以下策略可用
- noeviction:当达到内存限制时,不会保存新值。当数据库使用复制时,这适用于主数据库。
- allkeys-lru:保留最近使用的键;移除最久未使用 (LRU) 的键。
- allkeys-lfu:保留经常使用的键;移除最不常使用 (LFU) 的键。
- volatile-lru:移除带有
expire
字段设置为true
的最久未使用键。 - volatile-lfu:移除带有
expire
字段设置为true
的最不常使用键。 - allkeys-random:随机移除键以腾出空间以容纳添加的新数据。
- volatile-random:随机移除带有
expire
字段设置为true
的键。 - volatile-ttl:移除带有
expire
字段设置为true
且剩余生存时间 (TTL) 值最短的键。
如果不存在与先决条件匹配的要驱逐的键,则策略volatile-lru、volatile-lfu、volatile-random和volatile-ttl的行为类似于noeviction。
根据应用程序的访问模式选择正确的驱逐策略很重要,但是您可以在应用程序运行时重新配置策略,并使用 Redis INFO
输出监控缓存未命中次数和命中次数以调整您的设置。
一般来说,作为经验法则
-
当您期望请求流行度存在幂律分布时,使用allkeys-lru策略。也就是说,您期望一部分元素的访问频率远高于其余元素。如果您不确定,这是一个不错的选择。
-
如果您有循环访问,其中所有键都会被连续扫描,或者您期望分布是均匀的,请使用allkeys-random。
-
如果您希望能够通过在创建缓存对象时使用不同的 TTL 值来向 Redis 提供有关哪些是良好到期候选者的提示,请使用volatile-ttl。
volatile-lru和volatile-random策略主要在您希望将单个实例用于缓存以及拥有一组持久键时有用。但是,通常运行两个 Redis 实例来解决此类问题是一个更好的主意。
还值得注意的是,为键设置expire
值会消耗内存,因此使用allkeys-lru之类的策略更节省内存,因为在内存压力下驱逐键不需要为键进行expire
配置。
驱逐过程的工作原理
重要的是要了解驱逐过程的工作原理如下
- 客户端运行新的命令,导致添加更多数据。
- Redis 检查内存使用情况,如果它大于
maxmemory
限制,它将根据策略驱逐键。 - 执行新命令,依此类推。
因此,我们不断地跨越内存限制的边界,通过超过它,然后通过驱逐键返回到限制之下。
如果某个命令导致大量内存被使用(例如,将一个大集合交集存储到一个新的键中)一段时间,则内存限制可能会被明显超过。
近似 LRU 算法
Redis LRU 算法不是精确的实现。这意味着 Redis 无法选择最佳候选者进行驱逐,也就是说,最久未访问的键。相反,它将尝试运行 LRU 算法的近似值,通过对少量键进行采样,并在采样键中驱逐最佳(访问时间最老)的键。
但是,从 Redis 3.0 开始,该算法得到了改进,也包含了一个用于驱逐的良好候选者池。这提高了算法的性能,使其能够更接近地近似于真实 LRU 算法的行为。
关于 Redis LRU 算法重要的是,您可以调整算法的精度,方法是更改每次驱逐时要检查的样本数量。此参数由以下配置指令控制
maxmemory-samples 5
Redis 不使用真正的 LRU 实现的原因是它消耗更多内存。但是,对于使用 Redis 的应用程序来说,近似值实际上是等效的。此图比较了 Redis 使用的 LRU 近似值与真正的 LRU。
生成上述图形的测试用给定数量的键填充了 Redis 服务器。从第一个到最后一个访问这些键。第一个键是使用 LRU 算法进行驱逐的最佳候选者。随后添加了更多 50% 的键,以迫使一半的旧键被驱逐。
您可以在图形中看到三种类型的点,形成三个不同的带。
- 浅灰色带是已驱逐的对象。
- 灰色带是未驱逐的对象。
- 绿色带是已添加的对象。
在理论上的 LRU 实现中,我们期望在旧键中,前一半将被过期。而 Redis LRU 算法只会概率性地使较旧的键过期。
如您所见,Redis 3.0 在 5 个样本中比 Redis 2.8 做得更好,但是大多数在最新访问的键中的对象仍然被 Redis 2.8 保留。在 Redis 3.0 中使用 10 个样本大小,近似值非常接近 Redis 3.0 的理论性能。
请注意,LRU 只是一个模型,用于预测给定键在未来被访问的可能性。此外,如果您的数据访问模式非常类似于幂律,则大多数访问将位于 LRU 近似算法可以很好地处理的键集中。
在模拟中,我们发现使用幂律访问模式,真正的 LRU 与 Redis 近似值之间的差异很小或不存在。
但是,您可以以增加一些 CPU 使用量的代价将样本大小提高到 10 以近似于真正的 LRU,并检查这是否会影响您的缓存未命中率。
使用CONFIG SET maxmemory-samples <count>
命令在生产环境中试验不同的样本大小值非常简单。
新的 LFU 模式
从 Redis 4.0 开始,最不常使用 (LFU) 驱逐模式可用。这种模式在某些情况下可能效果更好(提供更好的命中/未命中率)。在 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
文件。