DK's Notes DK's Notes
首页
导航站
  • Java-Se

    • Java基础
  • Java-Se进阶-多线程

    • 多线程
  • Java-Se进阶-java8新特性

    • java8新特性
  • Java-ee

    • JavaWeb
  • Java虚拟机

    • JVM
  • SQL 数据库

    • MySQL
  • NoSQL 数据库

    • Redis
    • ElasticSearch
    • MongoDB
  • ORM

    • MyBatis
    • MyBatis-Plus
  • Spring

    • Spring
  • SpringMVC

    • SpringMVC1
    • SpringMVC2
  • SpringCloud

    • SpringCloud
  • 中间件

    • RabbitMQ
    • Dubbo
  • 秒杀项目
  • Git
  • Linux
  • Docker
  • JWT
  • 面试
  • 刷题
开发问题😈
设计模式
关于💕
归档🕛
GitHub (opens new window)

风

摸鱼
首页
导航站
  • Java-Se

    • Java基础
  • Java-Se进阶-多线程

    • 多线程
  • Java-Se进阶-java8新特性

    • java8新特性
  • Java-ee

    • JavaWeb
  • Java虚拟机

    • JVM
  • SQL 数据库

    • MySQL
  • NoSQL 数据库

    • Redis
    • ElasticSearch
    • MongoDB
  • ORM

    • MyBatis
    • MyBatis-Plus
  • Spring

    • Spring
  • SpringMVC

    • SpringMVC1
    • SpringMVC2
  • SpringCloud

    • SpringCloud
  • 中间件

    • RabbitMQ
    • Dubbo
  • 秒杀项目
  • Git
  • Linux
  • Docker
  • JWT
  • 面试
  • 刷题
开发问题😈
设计模式
关于💕
归档🕛
GitHub (opens new window)
  • MySQL

  • Redis

    • Redis - 知识体系
    • Redis 全笔记
    • Redis - 概念和基础
    • Redis - 五大数据类型
    • Redis - 三种特殊数据类型
    • Redis - 事务
    • Redis - Java
    • Redis - 高级
    • Redis持久化机制
    • Redis集群:主从模式
    • Redis高可用-哨兵机制
    • Redis事务机制
    • Redis过期清除策略和内存淘汰机制
      • 过期清除策略
        • 如何设置key的过期时间
        • 三种过期删除策略
        • 定时删除
        • 定期删除
        • 惰性删除
        • 定期删除+惰性删除存在的问题
      • 为什么要有淘汰机制
      • 如何获取及设置内存淘汰策略
      • Redis的内存淘汰策略
        • LRU算法
        • LRU的筛选逻辑
        • Redis对LRU算法的实现
        • 小结
        • LFU算法
        • 如何处理被淘汰的数据
      • 缓存污染
        • 如何解决缓存污染问题
        • volatile-random 和 allkeys-random
        • volatile-ttl 策略
        • LRU 策略
        • LFU策略
        • LFU策略的筛选规则
        • LFU 策略具体实现
        • Redis对LFU算法的实现
        • counter 值的衰减机制
        • 使用 LFU 策略会保证缓存不被污染吗?
    • 缓存一致性问题
    • redis和zookeeper分布式锁
  • ElasticSearch

  • MongoDB

  • 数据库
  • Redis
zdk
2022-08-03
目录

Redis过期清除策略和内存淘汰机制

Table of Contents generated with DocToc (opens new window)

  • 过期清除策略
    • 如何设置key的过期时间
    • 三种过期删除策略
      • 定时删除
      • 定期删除
      • 惰性删除
    • 定期删除+惰性删除存在的问题
  • 为什么要有淘汰机制
  • 如何获取及设置内存淘汰策略
  • Redis的内存淘汰策略
    • LRU算法
      • LRU的筛选逻辑
      • Redis对LRU算法的实现
      • 小结
    • LFU算法
    • 如何处理被淘汰的数据
  • 缓存污染
    • 如何解决缓存污染问题
      • volatile-random 和 allkeys-random
      • volatile-ttl 策略
    • LRU 策略
    • LFU策略
      • LFU策略的筛选规则
      • LFU 策略具体实现
      • Redis对LFU算法的实现
      • counter 值的衰减机制
    • 使用 LFU 策略会保证缓存不被污染吗?

# 过期清除策略

如果我们设置了Redis的key-value的过期时间,当缓存中的数据过期之后,Redis就需要将这些数据进行清除,释放占用的内存空间。Redis中主要使用 定期删除 + 惰性删除 两种数据过期清除策略。

# 如何设置key的过期时间

设置key的过期时间:

(1) EXPIRE key seconds // 设置多少秒后过期

(2) EXPIREAT key timestamp //设置 key 过期时间的时间戳(unix timestamp) 以秒计

(3) PEXPIRE key milliseconds // 设置多少毫秒后过期

(4) PEXPIREAT key milliseconds-timestamp // 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计

移除redis的过期时间:

PERSIST key // 移除key的过期时间,key将保持永久

查询剩余生存时间:

TTL key // 以秒为单位,返回给定 key 的剩余生存时间

PTTL key // 以毫秒为单位返回 key 的剩余的过期时间

当key过期后,该key保存的数据还是会占据内存的,因为每当我们设置一个键的过期时间时,Redis会将该键带上过期时间存放到一个过期字典中。当key过期后,如果没有触发redis的删除策略的话,过期后的数据依然会保存在内存中的,这时候即便这个key已经过期,我们还是能够获取到这个key的数据。

# 三种过期删除策略

# 定时删除

在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。

缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。

# 定期删除

每隔一段时间,我们就对一些key进行检查,删除里面过期的key。Redis默认每隔100ms就随机抽取部分设置了过期时间的key,检测这些key是否过期,如果过期了就将其删除。

这里有两点需要注意下:

  • 默认的每隔100ms是在Redis的配置文件redis.conf中有一个属性"hz",默认为10,表示1s执行10次定期删除,即每隔100ms执行一次,可以修改这个配置的值来设置默认的间隔时间。

image.png

  • 随机抽取部分,而不是全部key。因为如果Redis里面有大量key都设置了过期时间,全部都去检测一遍的话CPU负载就会很高,会浪费大量的时间在检测上面,甚至直接导致redis挂掉。所有只会抽取一部分而不会全部检查。

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键长时间占用的内存没有及时释放的话,当我们再次获取这个过期的key时,依然会返回这个key的值,就相当于这个过期时间是无效的了。

# 惰性删除

设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。

缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放,从而造成内存泄漏。所以redis还引入了另一种内存淘汰机制。

Redis默认采用的过期策略是定期删除与惰性删除结合

# 定期删除+惰性删除存在的问题

如果某个key过期后,定期删除没删除成功,然后也没再次去请求key,也就是说惰性删除也没生效。这时,如果大量过期的key堆积在内存中,redis的内存会越来越高,导致redis的内存块耗尽。那么此时就应该采用内存淘汰机制。

# 为什么要有淘汰机制

Redis 缓存使用内存保存数据,避免了系统直接从后台数据库读取数据,提高了响应速度。由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来。Redis 定义了「淘汰机制」用来解决内存被写满的问题。 缓存淘汰机制,也叫缓存替换机制,它需要解决两个问题:

  • 决定淘汰哪些数据;
  • 如何处理那些被淘汰的数据。

# 如何获取及设置内存淘汰策略

1、获取当前内存淘汰策略:

127.0.0.1:6379> config get maxmemory-policy
1

image.png

可以看到当前使用的默认的noeviction策略 2、获取Redis能使用的最大内存大小

127.0.0.1:6379> config get maxmemory
1

image.png

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的可用内存

3、设置淘汰策略 通过配置文件设置淘汰策略(修改redis.conf文件):

maxmemory-policy allkeys-lru
1

通过命令修改淘汰策略:

127.0.0.1:6379> config set maxmemory-policy allkeys-lru
1

4、设置Redis最大占用内存大小

#设置Redis最大占用内存大小为100M
127.0.0.1:6379> config set maxmemory 100mb
1
2

# Redis的内存淘汰策略

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
# 

redis的缓存淘汰策略

# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
# 省略
#
# The default is:
#
# maxmemory-policy noeviction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略。截止目前,Redis定义了「8种内存淘汰策略」用来处理 redis 内存满的情况:

  • noeviction: 不会淘汰任何数据,当使用的内存空间超过 maxmemory 值时,返回错误;
  • volatile-ttl:筛选设置了过期时间的键值对,越早过期的越先被删除;
  • volatile-random:筛选设置了过期时间的键值对,随机删除;
  • volatile-lru:使用 LRU 算法筛选设置了过期时间的键值对,移除最近最少使用的键值对;
  • volatile-lfu:使用 LFU 算法选择设置了过期时间的键值对,移除最近最不频繁使用的键值对;
  • allkeys-random:在所有键值对中,随机选择并删除数据;
  • allkeys-lru:使用 LRU 算法在所有数据中进行筛选,移除最近最少使用的键值对;
  • allkeys-lfu:,使用 LFU 算法在所有数据中进行筛选,移除最近最不频繁使用的键值对。

上面是内存不足时的key「淘汰策略」,还有一种是过期键的删除策略,两者是不同的

1)、 noeviction 策略,也是 Redis 的默认策略,它要求 Redis 在使用的内存空间超过 maxmemory 值时,也不进行数据淘汰。一旦缓存被写满了,再有写请求来的时候,Redis 会直接返回错误。 我们实际项目中,一般不会使用这种策略。因为我们业务数据量通常会超过缓存容量的,而这个策略不淘汰数据,导致有些热点数据保存不到缓存中,失去了使用缓存的初衷。

2)、 我们再分析下 volatile-random、volatile-ttl、volatile-lru、volatile-lfu 这四种淘汰策略。它们淘汰数据的时候,只会筛选设置了过期时间的键值对上。

比如,我们使用 EXPIRE 命令对一批键值对设置了过期时间,那么会有两种情况会对这些数据进行清理:

  • 一种是过期时间到期了,会被删除;
  • 一种是 Redis 的内存使用量达到了 maxmemory 阈值,Redis 会根据 volatile-random、volatile-ttl、volatile-lru、volatile-lfu 这四种淘汰策略,具体的规则进行淘汰;这也就是说,如果一个键值对被删除策略选中了,即使它的过期时间还没到,也需要被删除。

3)、 allkeys-random,allkeys-lru,allkeys-lfu 这三种策略跟上述四种策略的区别是:淘汰时数据筛选的数据范围是所有键值对。 我们按照是否会进行数据淘汰,以及根据淘汰数据集的筛选的范围进行总结,如下图:

image.png

通过我们上面的分析,volatile-random、volatile-ttl以及allkeys-random 的筛选规则比较简单,而 volatile-lru,volatile-lfu,allkeys-lru,allkeys-lfu 分别用到了LRU 和 LFU 算法,我们继续分析。

# LRU算法

LRU 算法全称 Least Recently Used,一种常见的页面置换算法。按照「最近最少使用」的原则来筛选数据,筛选出最不常用的数据,而最近频繁使用的数据会留在缓存中。

# LRU的筛选逻辑

LRU会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表「最近最常使用」的数据和「最近最不常用」的数据。 如下图

image.png

我们现在有数据 6、3、9、20、5。 数据 20 和 3 被先后访问,它们都会从现有的链表位置移到 MRU 端,而链表中在它们之前的数据则相应地往后移一位。 因为,LRU 算法选择删除数据时,都是从 LRU 端开始,所以把刚刚被访问的数据移到 MRU 端,就可以让它们尽可能地留在缓存中。 如果有一个新数据 15 要被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么,LRU 算法会做两件事:

  1. 因为数据 15 是刚被访问的,所以它会被放到 MRU 端;
  2. 算法把 LRU 端的数据 5 从缓存中删除,相应的链表中就没有数据 5 的记录了。

其实,LRU 的算法逻辑十分简单:它认为,刚刚被访问的数据,肯定还会被再次访问,所以就把它放在 MRU端;LRU 端的数据被认为是长久不访问的数据,在缓存满时,就优先删除它。

我们可以把它理解为手机的后台应用窗口。它总是会把最近常用的窗口放在最前边,而不常用的应用窗口,就排列在后边了,如果再加上只能放置 N 个应用窗口的限制,淘汰最近最少用的应用窗口,那就是一个活生生的 LRU 了。

不过,LRU 算法在实际实现时,需要用「链表」管理所有的缓存数据,这会带来两个问题:

  1. 额外的空间开销;
  2. 当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而降低 Redis 缓存性能。

所以,在 Redis 中,LRU 算法被做了简化,「以减轻数据淘汰对缓存性能的影响」。

# Redis对LRU算法的实现

简单来说,Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。 然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。 接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。

Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的备选数据个数。

例如,我们执行如下命令,可以让 Redis 选出 100 个数据作为备选数据集:

config set maxmemory-samples 100
1

当需要再次淘汰数据时,Redis 需要挑选数据进入「第一次淘汰时创建的候选集合」。

挑选的标准是:能进入候选集合的数据的 lru 字段值必须小于「候选集合中最小的 lru 值」。

当有新数据进入备选数据集后,如果备选数据集中的数据个数达到了设置的阈值时。Redis 就把备选数据集中 lru 字段值最小的数据淘汰出去。

这样,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。

# 小结

  • 通常情况下推荐优先使用 allkeys-lru 策略。这样可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。
  • 如果业务数据中「有明显的冷热数据区分」,建议使用 allkeys-lru策略。这样,可以充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。
  • 如果业务应用中的「数据访问频率相差不大」,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据。
  • 如果业务中有「置顶」的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
  • 如果没有设置过期时间的键值对,那么 volatile-lru,volatile-lfu,volatile-random 和 volatile-ttl 策略的行为, 和 noeviction 基本上一致。

# LFU算法

LFU(Least Frequently Used),表示最近最少使用,它和key的使用次数有关,其思想是:根据key最近被访问的频率进行淘汰,比较少访问的key优先淘汰,反之则保留。

相比LRU算法,LFU增加了访问频率的这样一个维度来统计数据的热点情况,LFU主要使用了两个双向链表去形成一个二维的双向链表,一个用来保存访问频率,另一个用来访问频率相同的所有元素,其内部按照访问时间排序。

  • 当添加元素的时候访问频次默认为1,于是找到相同频次的节点,然后添加到相同频率节点对应的双向链表的头部,
  • 当元素被访问的时候就会增加对应key的访问频率,并且把访问的节点移动到下一个频次的节点。

LFU算法通过使用频率和上次访问时间来标记数据的这样一个热度,如果某个数据有读和写那么就增加访问的频率,如果一段时间内这个数据没有读写,那么就减少访问频率。

所以通过LFU算法改进之后,就可以真正达到非热点数据的淘汰,当然LFU也有缺点,相比LRU算法,LFU增加了访问频次的一个维护,以及实现的复杂度比LRU更高。

image.png

# 如何处理被淘汰的数据

一般来说,一旦被淘汰的数据选定后,如果这个数据是干净的,那么我们就直接删除;如果这个数据是脏数据,我们需要把它写回数据库。

干净数据和脏数据的区别就在于,和最初从后端数据库里读取时的值相比,有没有被修改过。 干净数据一直没有被修改,所以后端数据库里的数据也是最新值。在替换时,它可以被直接删除。而脏数据则相反。

但是,对于 Redis 来说,它决定了被淘汰的数据后,会把它们直接删除。即使淘汰的数据是脏数据,Redis 也不会把它们写回数据库。 所以,我们在使用 Redis 缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。 否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据了。

分析完 LRU 我们继续分析 LFU 算法。在这之前先了解一个问题:缓存污染。

# 缓存污染

在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用内存空间。这种情况,就是**「缓存污染」**。 缓存污染一旦变得严重,就会有大量不再访问的数据滞留在缓存中。如果这些数据占满了缓存空间,我们再往缓存中写入新数据时,就需要先把这些数据逐步淘汰出缓存,这就会引入额外的内存消耗,进而会影响应用的性能。

# 如何解决缓存污染问题

解决方案,那就是得把不会再被访问的数据筛选出来并淘汰掉。这样就不用等到缓存被写满以后,再逐一淘汰旧数据之后,才能写入新数据了。 至于哪些淘汰哪些数据,是由缓存的淘汰策略决定的。 上面分析了 Redis 的 8 种淘汰策略,下面我们一一分析,这些策略对于解决缓存污染问题,是否都有效呢?

# volatile-random 和 allkeys-random

这两种策略,它们都是随机选择内存的数据进行淘汰。既然是随机挑选,那么 Redis 就不会根据「数据的访问情况」来筛选数据。 而且如果被淘汰的数据再次被访问了,就会发生缓存缺失。应用需要到后端数据库中访问这些数据,降低了应用的请求响应速度。 如图:

image.png

所以,volatile-random 和 allkeys-random 策略,在避免缓存污染这个问题上的效果非常有限。

# volatile-ttl 策略

volatile-ttl 筛选的是设置了过期时间的数据,把这些数据中剩余存活时间最短的筛选出来并淘汰。 虽然 volatile-ttl 策略不再是随机选择淘汰数据了,但是剩余存活时间并不能直接反映数据再次访问的情况。 所以,按照 volatile-ttl 策略淘汰数据,和按随机方式淘汰数据类似,也可能出现数据被淘汰后,被再次访问导致的缓存缺失问题。

有一种情况例外:业务应用在给数据设置过期时间的时候,明确知道数据被再次访问的情况,并根据访问情况设置过期时间。 此时,Redis 按照数据的剩余最短存活时间进行筛选,是可以把不会再被访问的数据筛选出来的,进而避免缓存污染。

小结:

  • 在明确知道数据被再次访问的情况下,volatile-ttl 可以有效避免缓存污染;
  • 在其他情况下,volatile-random、allkeys-random、volatile-ttl 这三种策略不能应对缓存污染问题。

接下来,我们再分别分析 LRU 策略,以及 Redis 4.0 后实现的 LFU 策略。LRU 策略会按照数据访问的时效性,来筛选即将被淘汰的数据,应用非常广泛。

# LRU 策略

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

Redis 中的 LRU 策略,会在每个数据对应的 RedisObject 结构体中设置一个 lru 字段,用来记录数据的访问时间戳。

在进行数据淘汰时,LRU 策略会在候选数据集中淘汰掉 lru 字段值最小的数据,也就是最久不被访问的数据。

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

但是,也正是因为 只看数据的访问时间,使用 LRU 策略在处理「扫描式单次查询」操作时,无法解决缓存污染。

所谓的「扫描式单次查询操作」,就是指应用对大量的数据进行一次全体读取,每个数据都会被读取,而且只会被读取一次。 此时,因为这些被查询的数据刚刚被访问过,所以 lru 字段值都很大。

在使用 LRU 策略淘汰数据时,这些数据会留存在缓存中很长一段时间,造成缓存污染。

如果查询的数据量很大,这些数据占满了缓存空间,却又不会服务新的缓存请求。

此时,再有新数据要写入缓存的话,需要先把这些旧数据淘汰掉才行,这会影响缓存的性能。

举个简单例子。 如下图,数据 6 被访问后,被写入 Redis 缓存。但是,在此之后,数据 6 一直没有被再次访问,这就导致数据 6 滞留在缓存中,造成了污染。

image.png

所以,对于采用了 LRU 策略的 Redis 缓存来说,「扫描式单次查询」会造成缓存污染。 为了应对这类缓存污染问题,Redis 从 4.0 版本开始增加了 LFU 淘汰策略。

与 LRU 策略相比,LFU 策略中会从两个维度来筛选并淘汰数据:

  • 一是,数据访问的时效性(访问时间离当前时间的远近);
  • 二是,数据的被访问次数。

# LFU策略

LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个「计数器」,来统计这个数据的访问次数。

# LFU策略的筛选规则

  • 当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。
  • 如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。

# LFU 策略具体实现

我们上面提到,为了避免操作链表的开销,Redis 在实现 LRU 策略时使用了两个近似方法:

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

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

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

举个简单例子。 假设第一个数据 A 的累计访问次数是 256,访问时间戳是 202010010909,所以它的 counter 值为 255。

Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255。

而第二个数据 B 的累计访问次数是 1024,访问时间戳是 202010010810。

如果 counter 值只能记录到 255,那么数据 B 的 counter 值也是 255。

此时,缓存写满了,Redis 使用 LFU 策略进行淘汰。

由于数据 A 和 B 的 counter 值都是 255,LFU 策略会继续比较 A 和 B 的访问时间戳。发现数据 B 的上一次访问时间早于 A,就会把 B 淘汰掉。

但其实数据 B 的访问次数远大于数据 A,很可能会被再次访问。这样一来,使用 LFU 策略来淘汰数据就不合适了。

Redis 对此也进行了优化:在实现 LFU 策略时,Redis 并没有采用数据每被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规则。

# Redis对LFU算法的实现

Redis 实现 LFU 策略的计数规则:

  • 每当数据被访问一次时,先用「计数器当前的值」乘以「配置项 」lfu_log_factor ,再加 1;取其倒数,得到一个 p 值;
  • 然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。

下面是Redis 的部分源码,其中,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 了。 Redis 官网上提供的一张表,进一步说明 LFU 策略计数器递增的效果。它记录了当 lfu_log_factor 取不同值时,在不同的实际访问次数情况下,计数器值的变化情况。

image.png

可以看到,当 lfu_log_factor 取值为 1 时,实际访问次数为 100K 后,counter 值就达到 255 了,无法再区分实际访问次数更多的数据了。而当 lfu_log_factor 取值为 100 时,当实际访问次数为 10M 时,counter 值才达到 255。

正是因为使用了非线性递增的计数器方法,即使缓存数据的访问次数成千上万,LFU 策略也可以有效的区分不同的访问次数,从而合理的进行数据筛选。

从刚才的表中,我们可以看到,当 lfu_log_factor 取值为 10 时,百、千、十万级别的访问次数对应的 counter 值已经有明显的区分了。所以,我们在应用 LFU 策略时,一般可以将 lfu_log_factor 取值为 10。

前面我们也提到了,应用负载的情况是很复杂的。比如某些业务场景,有些数据在「短时间内被大量访问后就不会再被访问了」。那么再按照访问次数来筛选的话,这些数据会被留存在缓存中,但不会提升缓存命中率。

为此,Redis 在实现 LFU 策略时,还设计了一个「 counter 值的衰减机制」。

# counter 值的衰减机制

简单来说,LFU 策略使用「衰减因子配置项」 lfu_decay_time 来控制访问次数的衰减。

  1. LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。
  2. 然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。

简单举个例子,假设 lfu_decay_time 取值为 1,如果数据在 N 分钟内没有被访问,那么它的访问次数就要减 N。

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

# 使用 LFU 策略会保证缓存不被污染吗?

在一些极端情况下,LFU 策略使用的计数器可能会在短时间内达到一个很大值,而计数器的「衰减配置项」 lfu_decay_time 设置得较大,导致计数器值衰减很慢,在这种情况下,数据就可能在缓存中长期驻留。所以不能一定保证缓存不会被污染。

在 GitHub 上编辑此页 (opens new window)
#Redis#Redis内存淘汰机制#Redis过期清除策略
最后更新: 2022/10/04, 8:10:00
Redis事务机制
缓存一致性问题

← Redis事务机制 缓存一致性问题→

Theme by Vdoing | Copyright © 2022-2023 zdk | notes
湘ICP备2022001117号-1
川公网安备 51142102511562号
提供加速服务
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式