使用 Redis 作为 LRU 缓存

https://redis.io/topics/lru-cache

当 Redis 用作缓存时,通常可以很方便地让它在您添加新数据时自动淘汰旧数据。这种行为在开发者社区中是众所周知的,因为它是流行的 memcached 系统的默认行为。

LRU 实际上只是支持的淘汰方法之一。本页涵盖了 Redis maxmemory 指令的更一般主题,该指令用于将内存使用量限制在固定数量,还深入介绍了 Redis 使用的 LRU 算法,这实际上是精确 LRU 的近似值。

从 Redis 版本 4.0 开始,引入了新的 LFU(最不常用)淘汰策略。这在本文档的单独部分中进行了介绍。

Maxmemory 配置指令

maxmemory 配置指令用于配置 Redis 为数据集使用指定数量的内存。可以使用 redis.conf 文件设置配置指令,或者稍后在运行时使用 CONFIG SET 命令。

例如,为了配置 100 M字节的内存限制,可以在 redis.conf 文件中使用以下指令

maxmemory 100mb

将 maxmemory 设置为零会导致没有内存限制。这是 64 位系统的默认行为,而 32 位系统使用 3GB 的隐式内存限制。

当达到指定的内存量时,可以在不同的行为中进行选择,称为策略。 Redis 可以只返回可能导致使用更多内存的命令的错误,或者它可以淘汰一些旧数据以便在每次添加新数据时返回到指定的限制。

淘汰策略

当达到最大内存限制时,Redis 的淘汰策略是使用 maxmemory-policy 配置指令配置的。

可以使用以下策略:

  • noeviction(不淘汰):当达到内存限制并且客户端尝试执行可能导致所有引起申请内存的命令会报错误(大部分的写命令, 删除键 和更多异常)。
  • allkeys-lru:首先通过尝试删除最近较少使用的 (LRU) key来淘汰key,以便为添加的新数据腾出空间。
  • volatile-lru:首先通过尝试删除最近较少使用的(LRU)key来淘汰key,但仅在设置了过期时间的key中,以便为添加的新数据腾出空间。
  • allkeys-random:随机淘汰key
  • volatile-random:随机淘汰key,但仅在设置了过期时间的key中
  • volatile-ttl:对于设置了过期时间的key中,首先尝试淘汰设置的TTL时间较短的key

如果没有匹配先决条件的淘汰键,则策略 volatile-lru、volatile-random 和 volatile-ttl 的行为类似于 noeviction。

根据应用程序的访问模式选择正确的淘汰策略很重要,但是您可以在应用程序运行时在运行时重新配置策略,并使用 Redis INFO 输出监控缓存未命中和命中的数量以调整您的设置.

一般来说,根据经验:

  • 当您期望请求的流行度呈幂律分布时,请使用 allkeys-lru 策略,也就是说,您期望元素子集的访问频率远高于其他元素。如果您不确定,这是一个不错的选择。
  • 如果您有一个循环访问,其中所有键都被连续扫描,或者您希望分布均匀(所有元素可能以相同的概率访问),请使用 allkeys-random。
  • 如果您希望能够通过在创建缓存对象时使用不同的 TTL 值向 Redis 提供关于哪些是好的过期候选者的提示,请使用 volatile-ttl。
  • volatile-lru 和 volatile-random 策略主要在您想使用单个实例进行缓存并拥有一组持久键时非常有用。然而,运行两个 Redis 实例来解决这样的问题通常是一个更好的主意。

还值得注意的是,为key设置过期时间会消耗内存,因此使用 allkeys-lru 之类的策略会提高内存效率,因为无需为在内存压力下被淘汰的密钥设置过期时间。

淘汰key的过程

重要的是要了解淘汰过程的工作方式如下:

  • 客户端运行新命令,导致添加更多数据
  • Redis 检查内存使用情况,如果大于 maxmemory 限制,它会根据策略淘汰key。
  • 执行新的命令,依次类推

所以我们不断地会超过内存限制,然后通过淘汰key保证内存始终在限制之下。

如果某个命令导致在一段时间内使用大量内存(例如有big key的集合),则内存限制可能会被明显超过。

近似 LRU 算法

Redis LRU 算法不是一个精确的实现。这意味着 Redis 无法选择淘汰key的最佳候选key,即过去访问次数最多的访问。相反,它将尝试运行 LRU 算法的近似值,通过对少量key进行采样,并淘汰采样key中最好的(具有最久访问时间)的key。

然而,自 Redis 3.0 以来,该算法得到了改进,也可以将一组好的候选对象用于淘汰。这提高了算法的性能,使其能够更接近真实 LRU 算法的行为。

Redis LRU 算法的重要之处在于,您可以通过更改样本数量来调整算法的精度来进行每次的淘汰。此参数由以下配置指令控制:

maxmemory-samples 5

Redis 之所以不使用真正的 LRU 实现,是因为它需要更多的内存。然而,对于使用 Redis 的应用程序来说,这个近似值几乎是等价的。以下是 Redis 使用的 LRU 近似与真实 LRU 的对比图。

redis生成随机数 redis如何实现lru_redis生成随机数

生成上述图表的测试用给定数量的key填充了 Redis 服务器。key从第一个到最后一个被访问,因此第一个key是使用 LRU 算法淘汰的最佳候选者。后来增加了更多 50% 的密钥,以强制驱逐一半的旧密钥。

您可以在图中看到三种点,形成三个不同的带。

  • 浅灰色带是被淘汰的对象
  • 灰色带是未被淘汰的对象
  • 绿色带是添加的对象。

在理论上的 LRU 实现中,我们预计在之前的key中,前半部分将过期。 Redis LRU 算法只会以概率方式使以前的key过期。

与 Redis 2.8 相比,Redis 3.0 的 5 个样本做得更好,但大多数最新访问的对象仍由 Redis 2.8 保留。在 Redis 3.0 中使用 10 的样本大小,该近似值非常接近 Redis 3.0 的理论性能。

请注意,LRU 只是一个模型,用于预测给定key在未来被访问的可能性。此外,如果您的数据访问模式与幂律非常相似,那么大多数访问将在 LRU 近似算法能够很好地处理的key集中。

在模拟中,我们发现使用幂律访问模式,真正的 LRU 和 Redis 近似之间的差异很小或不存在。

但是,您可以以一些额外的 CPU 使用为代价将样本大小提高到 10,以接近真实的 LRU,并检查这是否会影响您的缓存未命中率。

使用如下命令在生产环境中试验不同的样本大小值非常简单。

CONFIG SET maxmemory-samples <count>

比如:

config set maxmemory-samples 10

新的LFU模式

从 Redis 4.0 开始,提供了一种新的最不常用的淘汰模式。这种模式在某些情况下可能会更好(提供更好的命中/未命中率),因为使用 LFU Redis 会尝试跟踪项目的访问频率,因此很少使用的会被淘汰,而经常使用的有更高的概率会留在内存中。

如果您认为在 LRU,最近访问但实际上几乎从未请求过的key不会过期,因此风险是淘汰未来更有可能被请求的key。 LFU没有这个问题,一般来说应该更好地适应不同的访问模式。

要配置 LFU 模式,可以使用以下策略:

  • volatile-lfu 在具有过期的key中使用近似 LFU 算法去淘汰key。
  • allkeys-lfu 使用近似的 LFU 淘汰所有key。

LFU 类似于 LRU:它使用概率计数器,称为 Morris 计数器,以便使用每个对象的几个bit 来估计对象访问频率,并结合衰减周期,以便计数器随着时间的推移而减少:在某些时候,我们不再希望将key视为频繁访问,即使它们是以前的,以便算法可以适应访问模式的转变。

这些信息的采样与 LRU 发生的情况类似(如本文档上一节所述),以便选择淘汰的key。

然而,与 LRU 不同的是,LFU 具有某些可调参数:例如,如果不再访问排名较低的频繁项,它应该怎么处理更快呢?也可以调整Morris 计数器范围,以便更好地使算法适应特定的用例。

默认情况下,Redis 4.0 配置为:

  • 在大约 100 万个请求时使计数器饱和。
  • 每隔一分钟计数器减1。

这些应该是合理的值,并且经过了实验性测试,但用户可能希望使用这些配置设置来选择最佳值。

有关如何调整这些参数的说明可以在源代码分发中的示例 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 文件注释中获得。