(译)将Redis作为LRU缓存使用

本文翻译自Using Redis as an LRU cache

当把Redis当做一个缓存使用时,常见的操作是当你向缓存增加新数据时缓存会自动淘汰旧数据。这种缓存行为对于开发者来可以说是非常熟悉了,因为这是一个常见缓存系统的基本行为。

LRU实际上只支持一种淘汰数据的模式。本文涵盖了Redis中maxmemory指令的基本话题,maxmemory指令用来限制Redis的最大内存使用量,并深度涵盖了Redis中使用的近似LRU的算法。

从Redis 4.0开始,引入了一个一个新的LFU(最不经常使用)淘汰策略。这在本文档的一个独立小节中被涵盖。

maxmemory配置指令

maxmemory配置指令用来配置Redis的最大内存使用量。可以在redis.conf文件中配置该指令,也可以在运行时使用CONFIG SET命令进行配置。

例如为了将最大内存使用量限制在100mb,可以在redis.conf文件中使用如下指令进行配置。

maxmemory 100mb

将maxmemory设置为0将禁用内存限制。在64位系统上默认禁用内存限制,但在32位系统上内存限制被隐形地定为3GB。

当Redis的内存使用量达到指定的限制值时,有多种可选的策略。Redis可以直接对客户端发送的命令返回错误,这会导致更多的内存使用,或者Redis可以在加入新数据时淘汰一些旧数据,以便内存使用量降低到限制值以内。

淘汰策略

当Redis的内存使用量达到限制值时,将遵循由maxmemory-policy配置指令指定的几种策略。

有如下几种策略可用:

  • noeviction:当Redis达到最大内存使用量限制时返回错误,此时客户端继续尝试执行命令会导致更多的内存使用(大多数写命令会,除了DEL和一些异常)。
  • allkeys-lru:使用最近最少使用算法(LRU)淘汰键来为新增的数据腾出空间。
  • volatile-lru:仅对那些设置了过期时间的键使用最近最少使用算法(LRU)进行淘汰。
  • allkeys-random:对所有键进行随机淘汰来为新增的数据腾出空间。
  • volatile-random:仅对那些设置了过期时间的键进行随机淘汰。
  • volatile-ttl:对那些设置了过期时间的键进行淘汰,尝试优先淘汰那些生存时间(TTL)较短的键来为新增的数据腾出空间。

当键的条件不匹配时,和noeviction类似,volatile-lruvolatile-randomvolatile-ttl这几个策略都不会淘汰键。

根据你应用的访问模式选择正确的键淘汰策略非常重要,然而你可以在运行时重新配置键淘汰策略,并使用Redis的INFO命令监控的缓存未命中和命中数量以此来调整配置。

一般来说作为一个经验法则:

  • 当预计你的请求时一个幂律分布时,也就是说,当你预计其中所有元素的一个子集比其他元素更经常被访问时,可以使用allkeys-lru策略。如果你不确定的话,这是一个不错的选择。
  • 如果你在循环中对所有键进行连续扫描,或者你预计你的访问在键空间分布均匀时(所有键被访问的可能性相同),请使用allkeys-random策略。
  • 如果你想使用设置了过期时间的对象的TTL来提示Redis如何淘汰键,可以使用volatile-ttl策略。

当你想使用一个单一Redis实例来做缓存和持久地保存键值时,volatile-lruvolatile-random策略都很有用。然而,一般来说比较好的做法是运行两个Redis实例来解决这个问题。

还需要注意的是,对一个键设置过期时间需要消耗内存,所以使用像allkeys-lru这样的策略会有比较高的内存效率,因为在内存压力下,不需要为了淘汰键而对键设置过期时间。

键淘汰过程是如何运作的?

了解键淘汰的运作过程非常重要,如下:

  • 一个客户端执行一个新命令,导致更多的数据被添加进Redis。
  • Redis检查内存使用量,如果大于最大内存使用量限制,遵循配置的策略对键进行淘汰。
  • 接着继续执行新命令,以此类推。

所以我们不断地在跨域内存限制的边界,有时超过这个限制,然后又通过键淘汰使内存使用量回到限制值以内。

如果一个命令导致内存被大量占用(比如计算一个大集合的交集并将其存储到一个新键中)一段时间,那么实际的内存使用量会显著地超过内存限制值。

近似LRU算法

Redis的LRU算法并不是一个准确的LRU实现。这意味着Redis无法选出最合适被淘汰的键。相反,Redis会运行一个近似的LRU算法,通过采样少量的键,并淘汰这些采样键中最符合(即距离上一次被访问的时间最长的)淘汰条件的键。

然而从Redis 3.0开始,该算法会选择一批符合被淘汰条件地键进行淘汰。这提升了算法性能,也使其更加接近一个真正的LRU算法的行为。

关于Redis的LRU算法的一个很重要的事情是你可以通过改变每次键淘汰时采样键的数量来调整算法的精度。这个参数由下面的配置指令控制:

maxmemory-samples 5

Redis不使用一个真正的LRU算法实现的原因是它需要消耗更多内存。然而,对于使用Redis的应用来说近似LRU算法和真正的LRU算法几乎是等价的。下面是Redis使用的近似LRU算法和真正的LRU算法的对比图。

redis使用的近似LRU算法和真正的LRU算法的对比

使用一个有给定数量的键填充的Redis服务器来进行测试并生成上面的图。键被从头到尾地访问,因此使用LRU算法将淘汰那些首先被访问的键。之后又有50%的键被加入其中,迫使一半的旧键被淘汰。

你在图中可以看到三种类型的点,形成了三个明显的带。

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

理论上我们对LRU实现的期望是,在旧键中,前一半将被淘汰。取而代之的是Redis的LRU算法将只能概率性地淘汰旧键。

如你所见,对于采样键数为5的情况Redis 3.0的表现要好于Redis 2.8,然而Redis 2.8还是会保留大部分最近访问时间较新的键。在Redis 3.0中将采样数设为10时,近似LRU的表现已经非常接近于理论LRU的表现。

注意,LRU算法只是一个预测给定键在将来被访问的可能性的模型。此外,如果你的数据访问模式接近于幂律分布,即大部分的访问都发生在键空间的某个子集内,则近似LRU算法将很好地处理这种情况。

在模拟中我们发现使用幂律访问模式时,真正的LRU算法和Redis的近似LRU算法区别极小或者根本不存在。

然而,你可以将采样键数量提高到10以及一些额外的CPU使用为代价来让近似LRU算法的行为更接近真正LRU算法,并检查这么做是否会影响你的缓存未命中率。

为了在生产环境中对不同的采样键数进行实验,可以使用CONFIG SET maxmemory-samples <count>命令,非常简单。

新的LFU(最不经常使用)模式

从Redis 4.0开始,提供了一个全新的最不经常使用的淘汰策略。在某些情况下这种模式的表现更佳(提供了一个更好的命中/未命中率),因为Redis使用LFU将尝试追踪各个键的访问频率,因此比较少被访问的键将被淘汰,而经常被访问的键将被保留在内存中。

考虑在使用LRU算法的情况下,一个最近被访问过的键有可能实际上几乎从未被访问过,这个键不会被淘汰,所以我们有淘汰掉一个在未来有较高几率被访问到的键的风险。LFU不存在这个问题,且一般来说它对不同访问模式的适应性更好。

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

  • volatile-lfu:使用近似LFU算法来淘汰那些设置了过期时间的键。
  • allkeys-lfu:使用近似LFU算法来淘汰任何键。

LFU是LRU的近似:它使用了一个概率计数器,叫做Morris计数器,以便对每个对象仅使用几个比特位来估计对象的访问频率,使用衰减的方式使计数器的值随着时间的推移而减少:在某些时候,我们不再想考虑那些频繁被访问的键,即使距离上一次被访问时间较长,从而使算法能够适应访问模式的变化。

通过采样键来选择需要淘汰的键的方式和LRU的情况类似(在本文上小节中有描述)。

然而,和LRU不同的是,LFU具有一定的可调参数:比如,如果一个被频繁访问的对象不再被访问了,它的排名下降有多快?还可以调整Morris计数器的范围,以便更好地适应特定用例的算法。

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

  • 使计数器饱和大约需要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自身的文档注释。

由于LFU是一个新特性,我们将非常感谢你对在你的使用场景中将它和LRU进行对比的任何反馈。