• Redis 内存淘汰和过期删除策略


    提起使用Redis的优点,大家可以列举出许多,比如:数据存储在内存,读写速度快,性能优异。比如数据持久化,便于数据备份及恢复等等。

    分布式服务系统平台发展至今,Redis活跃在平台的各个领域,比如日志系统、秒杀、限时活动等等。我们通常会将Redis的数据分为永久及过期删除数据。那么到过期时间后,数据是通过什么策略删除的呢?Redis又提供了哪些过期删除策略?

    提到数据的过期删除,又不得不让我们联想到Redis的内存淘汰机制,两者都是删除数据,又有什么不同之处呢?

    带着这些问题,我们来一起探究下Redis的过期删除与内存淘汰机制。

    • 过期删除

    1.Redis Expires  Dict及删除逻辑源码解读

    Redis可以对key设置过期时间,每当我们对一个key设置了过期时间时,Redis会把该key带上过期时间存储到一个过期字典中。过期字典是存储在redisDb这个结构里的,过期字典的键指向Redis数据库中的某个key,过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。结构如下图所示:

    Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:

    • 如果过期,则删除该 key,至于选择异步删除,还是选择同步删除,根据lazyfree_lazy_expire 参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 给客服端;
    • 如果没有过期,不做任何处理,然后返回正常的键值对给客户端;

    过期判断逻辑:

    删除逻辑的底层代码如下:

    2.过期删除策略

    Redis的数据删除有定时删除(立即删除)、惰性删除和定期删除三种方式。

    定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

    惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

    定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

    上面介绍了三种过期删除策略,每一种都有优缺点,仅使用某一个策略都不能满足实际需求,所以Redis选择惰性删除+定期删除这两种策略配合使用,以求在合力使用CPU时间和避免内存浪费之间取得平衡。

    定时删除与惰性删除这两种策略大家都好理解,那么Redis是怎么实现定期删除的呢?

    在Redis中,默认每秒进行10次过期检查,此配置可以通过Redis的配置文件redis.conf进行配置。配置redis.conf的hz选项默认为10(即1秒执行10次,100ms一次,值越大说明刷新频率越快,对redis性能损耗也越大)。

    过期键的定期删除策略由 redis.c/activeExpireCycle 函数实现,调用流程为 serverCron() -> databasesCron() -> activeExpireCycle()。核心代码如下(为了方便查看核心部分,对代码进行了截取):

          这里有两个问题,

          问题1:定期删除时候,每次随机抽取的数量是多少?

           这个是由上图activeExpireCycle函数的ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP定义的,这个值是写死在程序中的,值为20。也就是说,每轮抽查时,会随机选择20key判断是否过期。

    问题2:定期删除是一个循环流程,redis如何保证不会出现循环过度,导致线程卡死现象?

    为此,增加了定时删除循环流程的时间上限,默认不超过25ms

    问题3 redis是单线程的,线程不仅处理定时任务,还处理客户端请求。单线程redis,如何知道要执行定时任务呢?

    Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是接下来处理客户端请求的最大时长,若达到了该时长,则暂时不处理客户端请求而去运行定时任务。

    • 内存淘汰策略

    前面部分我们讨论的是删除已过期的key,而当redis的运行内存达到了我们设置的最大运行内存时,才会触发内存淘汰策略。

    32位和64位操作系统,maxmemory的默认值是不同的。

    64位操作系统:maxmemory的默认值是0,表示没有内存大小限制,那么不管用户存放多少数据到reidsredis也不会对可用内存进行检查,直到redis实例因内存不足而崩溃为止。

    32位操作系统:maxmemory的默认值是3G,因为32位的机器最大只支持4GB的内存,而系统本身就需要一定的内存资源来支持运行,所以32位操作系统限制最大3GB的可用内存是非常合理的,这样可以避免因为内存不足而导致Redis实例崩溃。

    4.0 版本之前 Redis 的内存淘汰策略有以下 6 种。

    策略名称

    说明

    noeviction

    不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,它是 Redis 默认内存淘汰策略

    allkeys-lru

    淘汰整个键值中最久未使用的键值

    allkeys-random

    随机淘汰任意键值

    volatile-lru

    淘汰所有设置了过期时间的键值中最久未使用的键值

    volatile-random

    随机淘汰设置了过期时间的任意键值

    volatile-ttl

    优先淘汰更早过期的键值

    而在 Redis 4.0 版本中又新增了 2 种淘汰策略:

    策略名称

    说明

    volatile-lfu

    淘汰所有设置了过期时间的键值中最少使用的键值

    allkeys-lfu

    淘汰整个键值中最少使用的键值

    内存淘汰策略我们可以通过配置文件来修改,redis.conf 对应的配置项是“maxmemory-policy noeviction”,只需要把它修改成我们需要设置的类型即可。

    需要注意的是,如果使用修改 redis.conf 的方式,当设置完成之后需要重启 Redis 服务器才能生效。还有另一种简单的修改内存淘汰策略的方式,我们可以使用命令行工具输入“config set maxmemory-policy noeviction”来修改内存淘汰的策略,这种修改方式的好处是执行成功之后就会生效,无需重启 Redis 服务器。但它的坏处是不能持久化内存淘汰策略,每次重启 Redis 服务器之后设置的内存淘汰策略就会丢失。

    LRU 算法和 LFU 算法

    1. LRU 算法

    LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。

    传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。

    Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:

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

    Redis 是如何实现 LRU 算法的?

    Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。

    Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

    Redis 实现的 LRU 算法的优点:

    • 不用为所有的数据维护一个大链表,节省了空间占用;
    • 不用在每次数据访问时都移动链表项,提升了缓存的性能;

    但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

    因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。

    1. LFU 算法

    LFU 全称是 Least Frequently Used 翻译为最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是如果数据过去被访问多次,那么将来被访问的频率也更高

    所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。

    • 总结      

    Redis使用惰性删除+定期删除来管理已过期的key。而内存淘汰策略是为了解决内存过大问题,当redis的运行内存超过最大运行内存时,就会触发内存淘汰策略。

  • 相关阅读:
    Vue2 Element Pagination组件 每页数据量不同的解决方案
    MySQL——事务(说明及其细节)
    01、用三种方法实现 DIV + CSS 进度条的展示(静态以及动态)
    Docker安装MySQL 8.0镜像,简易上手
    Sentinel黑白名单授权规则解读
    土地利用程度综合指数计算/argis教程
    C# 第五章『面向对象』◆第4节:析构函数destructor
    SpringBoot自动装配源码解析
    git提交设置忽略指定文件,文件夹
    C/C++ P2P自定义发现协议 Xndp。
  • 原文地址:https://blog.csdn.net/airingyuan/article/details/128116699