redis做LRU缓存

当Redis用作缓存时,通常可以让它在添加新数据时自动逐出旧数据。 这种行为在开发人员社区中非常有名,因为它是流行的memcached系统的默认行为。

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

从Redis 4.0版开始,引入了新的LFU(最不常用,Least Frequently Used)驱逐策略。 本文档的单独部分对此进行了介绍。

Maxmemory配置指令

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

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

1
maxmemory 100mb

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

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

驱逐策略

使用maxmemory-policy配置指令配置达到maxmemory限制时Redis遵循的确切行为。

可以使用以下策略:

  • noeviction:当达到内存限制并且客户端尝试执行可能导致使用更多内存的命令时返回错误(大多数写命令,但DEL和一些例外)。
  • allkeys-lru:首先尝试删除最近使用较少的(LRU)key,以便为添加的新数据腾出空间。
  • volatile-lru:首先尝试删除最近使用较少的(LRU)key,但仅在具有过期设置的key中删除,以便为添加的新数据腾出空间。
  • allkeys-random:随机逐出key,以便为添加的新数据腾出空间。
  • volatile-random:随机逐出key以便为添加的新数据腾出空间,但只驱逐带有过期集的key。
  • volatile-ttl:用过期集驱逐key,并尝试先用较短的生存时间(TTL)驱逐key,以便为添加的新数据腾出空间。

如果没有用于驱逐匹配先决条件的key,则volatile-lru,volatile-random和volatile-ttl策略的行为类似于noeviction。

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

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

  • 当您期望在请求的流行度中使用幂律分布时,使用allkeys-lru策略,也就是说,您希望访问元素的子集的频率远远高于其他元素。 如果您不确定,这是一个很好的选择。
  • 如果您有连续扫描所有键的循环访问,或者您希望分布均匀(所有元素可能以相同的概率访问),请使用allkeys-random。
  • 如果您希望能够在创建缓存对象时通过使用不同的TTL值向Redis提供有关到期的最佳候选者的提示,请使用volatile-ttl。

当您希望使用单个实例进行缓存并拥有一组持久key时,volatile-lru和volatile-random策略非常有用。 但是,运行两个Redis实例来解决此类问题通常更好。

还值得注意的是,为key设置过期时间也有内存成本,因此使用像allkeys-lru这样的策略可以提高内存效率,因为在内存压力下不需要为key设置过期。

驱逐进程如何运作

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

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

因此,我们不断跨越内存限制的边界,通过检查,然后通过驱逐key返回限制之下。

如果命令导致使用大量内存(如大集合交集然后存储在新key中的)一段时间内,内存限制可以被明显超过。

近似LRU算法

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

然而,自Redis 3.0以来,该算法得到了改进,同时也为一些好的候选人提供了驱逐。 这改善了算法的性能,使其能够更接近地逼近真实LRU算法的行为。

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

1
maxmemory-samples 5

Redis之所以不使用真正的LRU实现,是因为它需要更多的内存。 但是,对于使用Redis的应用程序,近似值几乎相同。 以下是Redis使用的LRU近似与真实LRU的比较的图形比较。

生成上述图表的测试填充了具有给定数量键的Redis服务器。 从第一个到最后一个访问key,因此第一个key是使用LRU算法进行驱逐的最佳候选者。 后来增加了50%的key,以迫使一半的旧key被驱逐。

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

  • 浅灰色带是被驱逐的对象。
  • 灰色带是未被驱逐的对象。
  • 绿色条带是添加的对象。

在理论上的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> 命令对不同的样本量值进行生产实验,非常简单。

新的LFU模式

从Redis 4.0开始,可以使用新的最少使用的逐出模式。 在某些情况下,此模式可能更好(提供更好的命中/未命中率),因为使用LFU Redis将尝试跟踪item的访问频率,以便很少使用的item被驱逐,而使用的item通常具有更高的机会留在内存中。

如果您在LRU中考虑,最近访问但实际上几乎从未请求的item将不会过期,因此风险是驱逐将来有更高机会被请求的key。 LFU没有这个问题,并且通常应该更好地适应不同的访问模式。

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

  • volatile-lfu在具有过期集的key中使用近似LFU来驱逐。
  • allkeys-lfu使用近似的LFU驱逐任何key。

LFU近似于LRU:它使用概率计数器,称为Morris计数器,以便仅使用每个对象的几个位来估计对象访问频率,并结合衰减周期,以便计数器随时间减少:在某些时候我们 不再需要考虑频繁访问的key,即使它们是过去的,因此算法可以适应访问模式的转变。

这些信息的采样与LRU的情况类似(如本文档上一节所述),以便选择驱逐候选者。

然而,与LRU不同,LFU具有某些可调参数:例如,如果频率项不再被访问,它的排名应该有多快? 还可以调整Morris计数器范围,以便更好地使算法适应特定用例。

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

  • 使计数器饱和,大约有一百万个请求。
  • 每隔一分钟腐烂柜台。

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

有关如何调整这些参数的说明可以在源代码发布中的示例redis.conf文件中找到,但简单地说,它们是:

1
2
lfu-log-factor 10
lfu-decay-time 1

衰减时间是显而易见的,它是计数器应该被衰减的分钟数,当被采样并且发现比该值更旧时。 特殊值0表示:每次扫描时总是衰减计数器,并且很少有用。

计数器对数因子改变了为了使频率计数器饱和所需的命中数,该频率计数器仅在0-255的范围内。 因子越高,需要越多的访问才能达到最大值。 根据下表,因子越低,低访问计数器的分辨率越高:

1
2
3
4
5
6
7
8
9
10
11
+--------+------------+------------+------------+------------+------------+
| 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相比的任何反馈。