Nrich's blog Nrich's blog
首页
  • Java
  • Golang
  • 深度学习
  • Git
  • Linux
  • DataStructure
  • CloudNative
  • Redis
  • MySQL
  • 路由劫持
GitHub (opens new window)

Nrich

小聪明
首页
  • Java
  • Golang
  • 深度学习
  • Git
  • Linux
  • DataStructure
  • CloudNative
  • Redis
  • MySQL
  • 路由劫持
GitHub (opens new window)
  • Redis

    • 专栏:Redis 核心技术与实战

      • 基础架构、数据结构与 IO 模型
      • 持久化机制:AOF日志和RDB快照
      • 主从复制与哨兵机制
      • 切片集群
      • Redis 中的数据结构
      • 时间序列数据的保存
      • Redis 的消息队列方案
      • 异步机制、CPU 架构对性能的影响
      • 如何应对变慢的 Redis
      • Redis 的内存碎片、缓冲区
      • Redis 用作缓存
      • Redis 用作缓存之缓存异常
      • Redis 用作缓存之缓存污染
        • 5. 缓存被污染了,该怎么办?
          • 5.1 如何解决缓存污染问题
          • 5.2 LRU 缓存策略
          • 5.3 LFU 缓存策略的优化
          • 5.4 小结
      • Pika:基于SSD实现大容量Redis
      • 无锁的原子操作和分布式锁
      • Redis 的事务机制
      • Redis 主从同步的坑
      • 秒杀场景下的应用
      • 集群的数据倾斜和通信开销问题
  • MySQL

  • 中间件
  • Redis
  • 专栏:Redis 核心技术与实战
Nrich
2023-04-12
目录

Redis 用作缓存之缓存污染

参考:

  • 27 缓存被污染了,该怎么办?| 极客时间 (opens new window)

掌握缓存需要解决四个关键问题:

  • Redis 缓存具体是怎么工作的?(工作原理)
  • Redis 缓存如果满了,该怎么办?(替换策略)
  • 为什么会有缓存一致性、缓存穿透、缓存雪崩、缓存击穿等异常,该如何应对?(异常处理机制)
  • Redis 的内存毕竟有限,如果用快速的固态硬盘来保存数据,可以增加缓存的数据量,那 Redis 缓存可以使用快速固态硬盘吗?(扩展机制)

下面介绍第四个问题。

# 5. 缓存被污染了,该怎么办?

缓存污染:指有些数据很少被访问,这些数据被访问后仍然继续留在缓存中,就只会白白占用缓存空间,这种情况就是缓存污染。

如果缓存污染严重,就会影响 Redis 的性能。这一节就看看如何解决缓存污染问题。

# 5.1 如何解决缓存污染问题

要解决缓存污染,我们也能很容易想到解决方案,那就是得把不会再被访问的数据筛选出来并淘汰掉。这样就不用等到缓存被写满以后,再逐一淘汰旧数据之后,才能写入新数据了。而哪些数据能留存在缓存中,是由缓存的淘汰策略决定的。

我们前面说过缓存淘汰策略有 8 种,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random和allkeys-lfu策略。哪些策略可以解决缓存污染问题呢?我们一一分析下。

noeviction 策略是不会进行数据淘汰的。所以,它肯定不能用来解决缓存污染问题。

首先先看一下 volatile-random 和 allkeys-random 这两个策略,它们都是采用随机挑选被淘汰的数据。因为这两个策略并不根据数据的访问情况来筛选,因此在避免缓存污染这个问题上的效果非常有限。

再看 volatile-ttl 策略,它把数据中剩余存活时间最短的筛选出来并淘汰掉,但剩余存活时间也不能直接反映数据再次访问的情况,因此也无法有效避免缓存污染。除非应用会根据访问情况来设置过期时间。

讲到这里,我们先小结下。除了在明确知道数据被再次访问的情况下,volatile-ttl可以有效避免缓存污染。在其他情况下,volatile-random、allkeys-random、volatile-ttl这三种策略并不能应对缓存污染问题。

下面再看一下 LRU 和 LFU 策略在解决缓存污染问题上的效果。

# 5.2 LRU 缓存策略

LRU 策略的核心思想:如果一个数据刚刚被访问,那么这个数据肯定是热数据,还会被再次访问。

Redis 的 LRU 缓存策略实现方式是在 RedisObject 结构体上设置了一个 lru 字段来记录时间戳,在进行数据淘汰时,LRU 策略会淘汰掉 lru 值最小的数据。

因此在数据被频繁访问的业务场景中,LRU 策略能够有效留存访问时间最近的数据,而且因为这些数据很可能被再次访问,从而可以提升业务应用的访问速度。

但 LRU 这种只看数据访问时间的算法,无法处理“扫描式单次查询操作”导致的缓存污染问题。扫描式单次查询操作指应用对大量数据进行一次全体逐一访问,此时由于被查询的数据都是刚被访问过的,其 lru 值都很大,从而导致很多数据都留在了缓存中产生污染。

所以对于 Redis 的 LRU 策略 而言,扫描式单次查询会造成缓存污染。为了应对这类问题,Redis 4.0 增加了 LFU 淘汰策略,它从时效性和被访问次数两个维度来筛选数据。下面看一下 LFU 策略。

# 5.3 LFU 缓存策略的优化

LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。

和那些被频繁访问的数据相比,扫描式单次查询的数据因为不会被再次访问,所以它们的访问次数不会再增加。因此,LFU 策略会优先把这些访问次数低的数据淘汰出缓存。这样一来,LFU 策略就可以避免这些数据对缓存造成污染了。


LFU策略具体又是如何实现的呢?

既然LFU策略是在LRU策略上做的优化,那它们的实现必定有些关系。再复习下第24讲学习过的LRU策略的实现:

  • Redis是用RedisObject结构来保存数据的,RedisObject结构中设置了一个lru字段,用来记录数据的访问时间戳;
  • Redis并没有为所有的数据维护一个全局的链表,而是通过随机采样方式,选取一定数量(例如10个)的数据放入候选集合,后续在候选集合中根据lru字段值的大小进行筛选。

在此基础上, Redis在实现LFU策略的时候,只是把原来24bit大小的lru字段,又进一步拆分成了两部分。

  1. ldt值:lru字段的前16bit,表示数据的访问时间戳;
  2. counter值:lru字段的后8bit,表示数据的访问次数。

总结一下:当LFU策略筛选数据时,Redis会在候选集合中,根据数据lru字段的后8bit选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据lru字段的前16bit值大小,选择访问时间最久远的数据进行淘汰。

但是, Redis只使用了8bit记录数据的访问次数,而8bit记录的最大值是255,这样可以吗?

Redis 在实现LFU策略时,没有采用数据每被访问一次,就给对应的counter值加1的计数规则,而是采用了一个更优化的计数规则:每当数据被访问一次时,首先,用计数器当前的值乘以配置项lfu_log_factor再加1,再取其倒数,得到一个p值;然后,把这个p值和一个取值范围在(0,1)间的随机数r值比大小,只有p值大于r值时,计数器才加1。.

下面这段Redis的部分源码,显示了LFU策略增加计数器值的计算逻辑。其中,baseval是计数器当前的值。计数器的初始值默认是5(由代码中的LFU_INIT_VAL常量设置),而不是0,这样可以避免数据刚被写入缓存,就因为访问次数少而被立即淘汰。

double r = (double)rand()/RAND_MAX;
...
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
1
2
3
4

使用了这种计算规则后,我们可以通过设置不同的lfu_log_factor配置项,来控制计数器值增加的速度,避免counter值很快就到255了。

下表记录了当lfu_log_factor取不同值时,在不同的实际访问次数情况下,计数器的值是如何变化的。

$uploadName

正是因为使用了非线性递增的计数器方法,即使缓存数据的访问次数成千上万,LFU策略也可以有效地区分不同的访问次数,从而进行合理的数据筛选。我们在应用LFU策略时,一般可以将lfu_log_factor取值为10。

在一些场景下,有些数据在短时间内被大量访问后就不会再被访问了。那么再按照访问次数来筛选的话,这些数据会被留存在缓存中,但不会提升缓存命中率。为此,Redis在实现LFU策略时,还设计了一个counter值的衰减机制:

  • LFU策略使用衰减因子配置项lfu_decay_time来控制访问次数的衰减。LFU策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU策略再把这个差值除以lfu_decay_time值,所得的结果就是数据counter要衰减的值。

简单举个例子,假设lfu_decay_time取值为1,如果数据在N分钟内没有被访问,那么它的访问次数就要减N。如果lfu_decay_time取值更大,那么相应的衰减值会变小,衰减效果也会减弱。所以,如果业务应用中有短时高频访问的数据的话,建议把lfu_decay_time值设置为1,这样一来,LFU策略在它们不再被访问后,会较快地衰减它们的访问次数,尽早把它们从缓存中淘汰出去,避免缓存污染。

# 5.4 小结

在实际业务应用中,LRU 和LFU 两个策略都有应用。LRU 和 LFU 两个策略关注的数据访问特征各有侧重:

  • LRU 策略更加关注数据的时效性;
  • LFU 策略更加关注数据的访问频次。

通常情况下,实际应用的负载具有较好的时间局部性,所以 LRU 策略的应用会更加广泛。但是,在扫描式查询的应用场景中,LFU 策略就可以很好地应对缓存污染问题了,建议你优先使用。

此外,如果业务应用中有短时高频访问的数据,除了LFU策略本身会对数据的访问次数进行自动衰减以外,我再给你个小建议:你可以优先使用volatile-lfu策略,并根据这些数据的访问时限设置它们的过期时间,以免它们留存在缓存中造成污染。

编辑 (opens new window)
上次更新: 2023/04/12, 03:21:35
Redis 用作缓存之缓存异常
Pika:基于SSD实现大容量Redis

← Redis 用作缓存之缓存异常 Pika:基于SSD实现大容量Redis→

最近更新
01
YAML、Pod、Job、CronJob、ConfigMap、Secret
06-06
02
Kubernetes 的安装与基本架构
06-04
03
初识容器
05-30
更多文章>
Theme by Vdoing | Copyright © 2022-2023 Nrich | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式