Redis的LRU缓存用法详解

如果你将Redis作为一种缓存使用,那么当你添加新的数据时,有时候可以让Redis自动释放旧数据占用的内存,这是一个非常方便的功能。这种行为在开发者社区中是众所周知的,因为广为流行的memcached系统的默认行为也是这样的。

LRU实际上只是受支持的内存回收方法之一。本文主要涵盖Redis的maxmemory配置指令的使用方法,这个配置指令能够将Redis的内存使用量限制为一个固定的总量。除此之外,本文还较为深入地介绍了Redis使用的LRU算法,这个算法实际上是理想LRU的一种近似算法。

一、maxmemory配置指令

如果需要限制Redis用于存储数据集的内存总量,那么可以使用maxmemory配置指令对Redis进行配置。可以在redis.conf文件中设置配置指令,或者稍后可以在运行时使用CONFIG SET命令进行配置。

例如,如果需要将Redis可用的最大内存限制为100 MB,那么可以在redis.conf文件中使用以下配置指令:

  1. maxmemory 100mb

如果将maxmemory设置为0,那么就表示Redis没有内存使用限制。这是64位系统的默认行为,而3位系统则具有最多3 GB内存的隐含限制。

当达到指定的内存总量时,可以在几种不同的行为中选择一个执行,这些行为被称为内存回收策略。当某个命令导致内存使用量超出限制时,Redis可以仅仅返回错误信息。或者,Redis还可以回收某些旧数据占用的内存,当添加新数据时,便可以使用这些被释放的内存。

二、内存回收策略

当内存使用量达到maxmemory的限制时,你可以使用maxmemory-policy配置指令决定Redis此时的具体行为。

Redis会使用以下策略:

  • noeviction:当达到内存使用量的限制时,Redis会返回错误信息,而客户端仍然会尝试执行可能导致内存使用量超限的命令(包括大多数的写入命令,但是DEL之类的命令除外)。

  • allkeys-lru:为了给新添加的数据腾出内存空间,Redis会首先删除最近最少使用(LRU,Least Recently Used)的键。

  • volatile-lru:为了给新添加的数据腾出内存空间,Redis会首先删除最近最少使用的键,并且这些键必须具有过期时间

  • allkeys-random:为了给新添加的数据腾出内存空间,Redis会随机删除键。

  • volatile-random:为了给新添加的数据腾出内存空间,Redis会随机删除键,并且这些键必须具有过期时间

  • volatile-ttl:为了给新添加的数据腾出内存空间,Redis只会删除具有过期时间的键,特别是生存时间(TTL,Time To Live)较短的键。

如果没有满足条件的键可以删除,那么volatile-lruvolatile-randomvolatile-ttl策略的行为会类似于noeviction策略。

根据你的应用程序访问Redis的方式,选用合适的内存回收策略是非常重要的。然而,当应用程序正在运行时,你也可以在运行时重新配置Redis,然后可以使用Redis的INFO命令,监控这个命令输出信息中的缓存命中和未命中的次数,这样便能不断调优你的设置。

通常,你可以参考以下的经验法则:

  • 当你的访问请求基本服从幂律分布(Power-Law Distribution)时(也就是说,缓存的某个元素子集的访问几率要比其余元素的访问几率高得多),你可以使用allkeys-lru策略。如果你不确定的话,那么使用allkeys-lru策略也是一个很好的选择。

  • 当你的访问请求会循环访问Redis缓存时(此时,应用程序会不断扫描所有的键),或者当你的访问请求基本服从均匀分布(Uniform Distribution)时,你可以使用allkeys-random策略。

  • 如果你在创建各个缓存对象时使用了不同的TTL(生存时间)值,并且你想要让Redis根据这些TTL值来决定回收哪些键的内存,那么你可以使用volatile-ttl策略。

当你想要将一个Redis实例既用于缓存数据,又用于持久化保存一些键的数据时,大多数情况下可以使用volatile-lruvolatile-random策略。但是,在上述使用场景中,通常最好运行两个Redis实例,分别实现缓存和持久化的功能。

还有一点也值得注意,为某个键设置过期时间也是会消耗内存的。因此,如果使用allkeys-lru策略,那么内存的使用效率会更高。因为,在比较高的内存使用压力之下,没有必要为待回收内存的键设置过期时间。

三、如何进行内存回收

理解回收内存的工作流程非常重要,如下所示:

  • 某个客户端运行一条新命令,导致添加更多的数据。
  • Redis会检查内存使用率,如果超过maxmemory的限制,那么它便会根据内存回收策略,释放相应键的内存。
  • 又运行一条新命令,以此类推。

因此,我们通过检查Redis的工作状态会发现,Redis会不断地跨越内存限制的边界,然后再不断回收相应键的内存,这样便能使内存使用量回落至maxmemory的限制之下。

某个命令可能会导致占用大量的内存(例如,某条SET命令可能会将一个大型数据集存储在一个新键之中)。有时候,Redis的内存使用量可能会明显超出maxmemory的限制

四、近似LRU算法

Redis的LRU算法并不是一种精确的实现。这就意味着,Redis并不能选出能够回收内存的最佳候选者,也就是说,Redis会释放过去一段时间内访问量最少的键。相反,Redis会尝试运行LRU算法的一种近似实现,这种实现方法会采样少量的键,然后回收这些采样中最符合要求(访问时间最老)的键的内存。

但是,从3.0版本以来,Redis已经改善了这个算法,现在已经可以批量选择等待回收内存的候选键了。现在,这个算法的性能已经得到了提高,Redis回收内存的行为更加近似于真实的LRU算法。

Redis的LRU算法有一个很重要的特性,你只要修改每次回收内存时需要的采样数量,便能够调整LRU算法的精度。这个参数是由下面的配置指令进行控制的:

  1. maxmemory-samples 5

为什么Redis没有使用真正的LRU算法呢?这是因为它会消耗更多的内存。然而,这种近似方法实际上等效于使用Redis的应用程序。Redis使用的近似的LRU算法和真正的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算法和Redis的近似LRU算法之间的差异非常小,甚至不存在差异。

你也可以将采样数量增大至10,虽然会消耗一些额外的CPU性能,但是能够更加接近于真正的LRU算法的性能。然后,你可以检查缓存命中率和未命中率是否有明显的差异。

如果想要在生产环境中尝试配置不同的采样数量,那么你可以使用CONFIG SET maxmemory-samples <count>命令,这种方法非常简单。