Using Redis as an LRU cache

Posted by BY morningcat on April 1, 2020

Using Redis as an LRU cache

LRU Least Recently Used最近最少使用

将Redis用作缓存时,通常很方便的做法是在添加新数据时自动将旧数据逐出。此行为在开发人员社区中是众所周知的,因为它是流行的内存缓存系统的默认行为

LRU 实际上只是支持的驱逐方法之一。此页面涵盖了 Redis maxmemory 指令更加一般的主题,该指令用于将内存使用限制为固定量,并且还深入介绍了Redis 使用的 LRU 算法,实际上是精确的LRU的近似值。从Redis版本 4.0 开始,引入了新的 LFU(最不常用)驱逐策略。本文档的单独部分对此进行了介绍。

Maxmemory 配置指令

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

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

maxmemory 100mb

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

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

Eviction policies 驱逐策略

使用 maxmemory-policy 配置指令配置当达到最大内存限制时,会采用哪种驱逐策略。

可以使用以下策略:

  • noeviction

当达到内存限制并且客户端尝试执行可能导致使用更多内存的命令时,将返回错误(大多数写入命令,但DEL和一些其他例外)。

  • allkeys-lru

通过尝试先删除最近使用较少的(LRU)键来移出键,以便为添加的新数据腾出空间。

  • volatile-lru

通过尝试先删除最近使用较少的(LRU)密钥(但仅限于已设置过期的密钥)来移出密钥,以便为添加的新数据腾出空间。 通过尝试先删除最近使用较少的且设置了过期时间的(LRU)键来移出键,以便为添加的新数据腾出空间。

  • allkeys-random

随机删除键,以便为添加的新数据腾出空间。

  • volatile-random

随机删除设置了过期时间的键,以便为添加的新数据腾出空间。

  • volatile-ttl

删除设置了到期时间的键,并尝试首先删除具有较短生存时间(TTL)的键,以便为添加的新数据腾出空间。


如果没有与先决条件匹配的键可供删除,则volatile-lruvolatile-randomvolatile-ttl策略的行为类似于noeviction

选择正确的驱逐策略很重要,具体取决于应用程序的访问模式,但是可以在应用程序运行时在运行时重新配置该策略,并使用Redis INFO输出监视缓存未命中和命中的次数,以调整设置。

一般而言,根据经验:

  • 当您希望请求的受欢迎程度呈幂律分布时,请使用 allkeys-lru 策略,也就是说,您希望访问元素的子集比其他元素访问更多。如果不确定,这是一个不错的选择。

  • 如果您具有对所有键进行连续扫描的周期性访问,或者当您期望分布是统一的(所有元素以相同的概率被访问)时,请使用allkeys-random

  • 如果您希望能够在创建缓存对象时通过使用不同的TTL值向Redis提供有关哪些是到期的最佳候选者的提示,请使用volatile-ttl

  • 当您要使用单个实例进行缓存并拥有一组持久键时,volatile-lruvolatile-random策略主要有用。但是,通常最好运行两个Redis实例来解决此问题。

  • 还值得注意的是,将键设置为过期会消耗内存,因此使用 allkeys-lru 之类的策略会提高内存效率,因为无需为在内存压力下驱逐密钥设置过期。

驱逐过程如何进行

重要的是要了解驱逐过程的工作方式如下:

  • 客户端运行新命令,从而添加更多数据。

  • Redis会检查内存使用情况,如果大于最大内存限制,则会根据策略逐出密钥。

  • 执行新命令,依此类推。

因此,我们不断越过内存限制,然后越过该限制,然后逐出按键以在限制范围内返回,从而不断跨越该限制。

如果某条命令导致一段时间内使用了大量内存(例如将较大的交集存储到新键中),则内存限制可能会超出明显数量。

近似 LRU 算法

Redis LRU 算法不是确切的实现。这意味着 Redis 无法选择最好的驱逐候选者(即key),即过去访问最多的访问者。取而代之的是,它将尝试对LRU算法进行近似计算,方法是对少量密钥进行采样,然后从采样的密钥中驱出最好的(访问时间最长)密钥。

但是,自Redis 3.0起,对该算法进行了改进,使其还可以收集一批优秀的驱逐对象。这改善了算法的性能,使其能够更接近地逼真的LRU算法的行为。

Redis LRU算法的重要意义在于,您可以通过更改样本数量来检查每次逐出,从而调整算法的精度。此参数由以下配置指令控制:

maxmemory-samples 5

Redis之所以不使用真正的LRU实现,是因为它占用更多内存。但是,近似值实际上与使用Redis的应用程序等效。以下是Redis使用的LRU近似与真实LRU比较的图形比较。

http://redis.io/images/redisdoc/lru_comparison.png

生成上述图形的测试用给定数量的密钥填充了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 近似之间的差异很小或不存在。

但是,您可以以一些额外的CPU 使用为代价将样本大小增加到 10,以接近真实的 LRU,并检查这是否会导致缓存未命中率有所不同。

通过使用 CONFIG SET maxmemory-samples <count> 命令以不同的样本大小值进行生产实验非常简单。

The new LFU mode

从 Redis 4.0 开始,可以使用新的“最少使用” 驱逐策略。在某些情况下,此模式可能会更好地工作(提供更好的命中率/未命中率),因为使用 LFU Redis 会尝试跟踪物品的访问频率,因此,极少使用的物品会被驱逐,而经常使用的物品则有较高的机会保留在内存中。

如果您认为在LRU,最近访问过但实际上几乎从未请求过的项目不会过期,因此风险在于驱逐将来有更高机会请求的钥匙。LFU没有这个问题,通常应该更好地适应不同的访问模式。

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

  • volatile-lfu

使用近似的 LFU 删除任何设置了过期时间的键

  • allkeys-lfu

使用近似的 LFU 删除任何键。


LFU 近似于 LRU:它使用一个概率计数器(称为莫里斯计数器),以便仅使用每个对象几个位来估计对象访问频率,并结合一个衰减周期,从而使计数器随时间而减少:在某个时候,我们甚至不再希望将密钥视为过去经常访问的密钥,因此该算法可以适应访问模式的转变。

这些信息的采样方式与 LRU 发生的情况类似(如本文档前面的部分所述),以便选择驱逐候选人。

但是,与 LRU 不同的是,LFU 具有某些可调参数:例如,如果频繁访问的项不再被访问,应该将其降低多快?还可以调整 Morris 计数器范围,以使算法更好地适应特定的用例。

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

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

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

可以在源代码发行版的示例 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 文件自我记录注释中获得。由于 LFU 是一项新功能,因此与 LRU 相比,我们将很感谢您提供有关其在用例中的性能的反馈。