Redis缓存删除驱逐策略的工作方式 - codemancers


Redis是最流行的应用程序缓存存储形式之一。Redis还可以用作具有正确配置类型的持久性数据存储。在此博客中,我们介绍了这些配置在现实情况下如何发挥作用。我们还讨论了在将Redis用于我们的应用程序时如果不仔细考虑此类配置会发生什么情况。
首先,让我们为实验设置环境。为此,我们将使用docker运行一个简单的Redis服务器。除此之外,您还需要编写一些有用的脚本来轻松检查我们的缓存存储区的状态。我将在这里使用Ruby,但是Python也能正常工作!
让我们在docker上启动一个redis容器。

$ docker run -d -p 6379:6379 redis:latest


Redis的maxmemory配置可以定义允许从主机系统使用多少内存。让我们使用redis-cli检查当前配置是什么。默认情况下,redis将maxmemory的值保持为0,并且不会管理或限制内存的使用。但是,在32位系统的情况下,它将仍然受到操作系统的可用RAM或3GB的限制。此外,redis通过maxmemory-policy用于删除数据以管理内存使用情况。让我们使用redis-cli检查当前配置是什么:

127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2)
"noeviction"

默认情况下,redis将maxmemory-policy的值保持为noeviction,并且永远不会自行删除任何数据。这也是一种有意识的配置,可让人们决定如何使用Redis。如上所述,如果某人希望其Redis保留数据,则无需启用逐出功能。
 
那么怎么可能出问题呢?
对于在高并发用户基础生产环境上运行的应用程序,如果不注意此配置,可能会导致Redis停机。
为啥?正如我们已经要求redis服务器不要限制其内存使用,并且不删除缓存策略意味着redis中的数据将继续增长,在某个时候,它达到了临界水平。Redis不能再向其中添加新记录,因为它受到可用内存的限制。
系统由于内存不足而窒息而杀死了Redis,并且由于Redis拒绝连接,我们的应用开始崩溃。
由于redis是内存解决方案,因此我们必须增加系统的RAM,然后重新启动它以使其再次运行。即使从技术上讲增加RAM是可行的解决方案,但在财务上这通常不是可行的解决方案。
  
Redis作为缓存存储
如果有人希望将Redis用作缓存存储该怎么办?首先,我们应该注意什么?
顾名思义,高速缓存存储是一种针对短期数据的解决方案,可以在适当的时间清除这些数据。高速缓存用于需要定期高速访问某些数据集的应用程序中。当应用程序希望将正在进行的任务的中间元数据存储几个小时甚至几天时,也会使用缓存。这表明我们可以要求Redis从其内存中删除不需要的或过时的记录。有两个好处:
  • 更少的RAM需求=>更少的成本障碍
  • 停机时间几乎为零=>现在谁会不喜欢呢?

 
我们应该如何配置它?
为了更好地了解应该如何配置Redis,我们需要查看Redis提供给我们的选项。官方文档明确说明了我们可以使用的配置。简而言之,我们需要更改maxmemory和maxmemory-policy值,以完全控制redis服务器来管理其内存份额。
让我们做一些有趣的实验,看看这些配置如何产生作用。我们将看看三种常见的驱逐政策allkeys-lru,volatile-lru以及volatile-ttl。
首先,我们需要设置两个配置。
127.0.0.1:6379> CONFIG SET maxmemory 1mb
OK

为了模拟低内存的情况,我们将其允许的内存限制为1mb。此时,Redis可能保存的记录数量非常有限。内存已满后,redis kick将启动其概率驱逐算法,以确定可以删除哪些键以为新记录腾出空间。作为用户,我们可以控制如何选择这些键来逐出删除。
 
案例:allkeys-lru,只给我该死的空间!
没错,allkeys-lru无需任何特殊考虑即可从内存中删除最近最少使用的key:

127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
OK

切换到ruby控制台以执行一些脚本来读取和写入数据,以查看哪些键被删除。我们可以使用redis-rb gem连接到redis。
irb> $redis = Redis.new(url: "redis://localhost:6379")
irb> $redis.set(
"constant-1", "foo")
#
"OK"
irb> $redis.set(
"constant-2", "bar")
#
"OK"
irb> $redis.set(
"constant-3", "tar")
#
"OK"
irb> $redis.get(
"constant-2")
#
"bar"
irb> $redis.get(
"constant-1")
#
"foo"

# add many records
irb> (1..500).each { |key| $redis.set(
"loop-#{key}", SecureRandom.uuid)}

# get some of the keys to mimic usage
irb> (3..100).each do |key|
        unless $redis.get(
"loop-#{key}")
          puts
"loop-#{key} evicted"
        else
          puts
"loop-#{key} found"
        end
      end
# loop-3 found
# .
# .
# loop-28 found
# .
# .
# loop-100 found

# now let's flood redis with more data
irb> (501..1500).each { |key| $redis.set(
"loop-#{key}", SecureRandom.uuid)}
irb> $redis.get(
"constant-3")
# nil
irb> $redis.get(
"loop-1")
# nil
irb> $redis.get(
"loop-12")
#
"ea6ef190-05d4-480c-8e47-6785d80ca4d1"
irb> $redis.get(
"loop-100")
#
"fd8ff9e1-6ee2-4e74-aaff-1043b7c24e67"
irb> $redis.get(
"loop-242")
# nil
irb> $redis.get(
"loop-1499")
#
"88f17049-0e11-4cbc-8485-e000df21a2c3"

  • 我们最初添加了三个键,constant-1,2,3并访问了其中两个。
  • 我们在一个循环中添加了500条记录,这消耗了大量的内存。
  • 我们访问了那些键的子集来模仿最近使用的键。IE。我们会使用3到100之间的键。redis中所有键仍然可用。
  • 我们再次开始用1000条记录充斥Redis,Redis达到了内存限制。
  • Redis根据LRU策略选择键并将其删除。
  • 我们随机检查一些键以确认我们在步骤3中查询的键在redis中仍然可用,但是步骤2和4中的大多数键已被清除。
  • 我们还可以注意到,最近插入的键,例如。loop-1499仍然保留

在这种情况下,很明显,redis只关心最近使用过的键,并删除所有其他键,而无需任何进一步的考虑。

127.0.0.1:6379> FLUSHDB
OK

 
案例:volatile-lru,到期或被删除!
与allkeys-lru不同,volatile-lru需要特别注意从内存中删除最近最少使用的键:键必须设置有效期限。
127.0.0.1:6379> CONFIG SET maxmemory-policy volatile-lru
OK

让我们再次切换到ruby控制台并测试这种情况。与最后一种情况的唯一区别是,当我们添加新数据时,我们还将为每条记录设置一个到期时间。
# set some records without any expiry first
irb> $redis.set("constant-1", "foo")
#
"OK"
irb> $redis.set(
"constant-2", "bar")
#
"OK"
irb> $redis.set(
"constant-3", "tar")
#
"OK"
irb> $redis.get(
"constant-1")
#
"foo"

# add many records
# instead of using multi, we can also pass expiry as $redis.set(key, val, ex: 2345)
# but this is not supported by HSET
irb> (1..500).each do |key|
        $redis.multi do |multi|
          multi.set(
"loop-#{key}", SecureRandom.uuid)}
          multi.expire(
"loop-#{key}", key*20)
        end
      end

# get some of the keys to mimic usage
irb> (3..100).each do |key|
        unless $redis.get(
"loop-#{key}")
          puts
"loop-#{key} evicted"
        else
          puts
"loop-#{key} found"
        end
      end
# loop-3 found
# .
# .
# loop-38 found
# .
# .
# loop-100 found

# now let's flood redis with more data
irb> (501..1500).each do |key|
        $redis.multi do |multi|
          multi.set(
"loop-#{key}", SecureRandom.uuid)}
          multi.expire(
"loop-#{key}", key*20)
        end
      end
irb> $redis.get(
"constant-1")
#
"foo"
irb> $redis.get(
"constant-2")
#
"bar"
irb> $redis.get(
"constant-3")
#
"tar"
irb> $redis.get(
"loop-1")
# nil
irb> $redis.get(
"loop-100")
#
"ef32bffc-6b24-4f0d-b2ba-01ef72e94537"
irb> $redis.get(
"loop-101")
# nil
irb> $redis.get(
"loop-1499")
#
"55f1c049-3e14-4cbc-8485-e000df21a2c3"
# .
# .
# after 1499*20(=29980) seconds
irb> $redis.get(
"loop-1499")
# nil

  1. 我们最初添加了三个键,constant-1,2,3并且只访问了其中一个。
  2. 我们在一个循环中添加了500条到期的记录,这消耗了大量的内存。
  3. 我们访问了那些键的子集来模仿最近使用的键。IE。我们访问的键范围是3到100。
  4. 我们再次开始用1000条记录充斥Redis,Redis达到了内存限制。
  5. Redis会根据LRU策略选择到期的键并将其删除。
  6. 我们随机检查一些键以确认我们在步骤3中查询的键仍可在Redis中使用,来自步骤2的所有键以及来自步骤4的大多数键由于它们已过期或被强行删除而已被撤出。
  7. 我们还可以注意到在步骤1中插入的​​键,例如。常数1,2,3仍然保留。这些键没有到期时间,因此算法将忽略它们!

因此,在这种情况下,情况略有偏离,但是我们仍在基于LRU技术删除键。让我们看一下第三种情况如何为它增加一个条件。

 
案例:volatile-ttl,最大限度地减少生存!
在上述情况下,我们看到了redis如何删除过期的键,volatile-ttl是几乎相同,只不过还会删除即将到期的键,而不是选择最近最少使用的记录。另外,请注意,在这种情况下,redis也会忽略没有为记录设置任何过期时间的键。
 
何时/哪个使用?
这个问题的答案归结为我们正在努力实现的目标。

  1. 您是否有一项任务需要X秒钟的记录,而另一些在生存时间长短方面不那么重要?->使用具有可变TTL的volatile-ttl进行记录
  2. 您是否有某些键需要始终在Redis中可用,但还需要进行常规清理?->选择volatile-lru
  3. 您是否只是在很短的时间内将数据转储到Redis中,然后再不再使用它?还是不确定?->坚持使用更安全的allkeys-lru