面试官:内存耗尽后Redis会发生什么

发布于 2021-11-24 13:18 ,所属分类:2021面试经验技巧分享

点击上方蓝色“后端面试那些事儿”,选择“设为星标”

学最好的别人,做最好的自己

来源:http://ii081.cn/6FJSr

前言

作为一台服务器来说,内存并不是无限的,所以总会存在内存耗尽的情况,那么当Redis服务器的内存耗尽后,如果继续执行请求命令,Redis会如何处理呢?

内存回收

使用Redis服务时,很多情况下某些键值对只会在特定的时间内有效,为了防止这种类型的数据一直占有内存,我们可以给键值对设置有效期。Redis中可以通过4个独立的命令来给一个键设置过期时间:

  • expire key ttl:将key值的过期时间设置为ttl
  • pexpire key ttl:将key值的过期时间设置为ttl毫秒
  • expireat key timestamp:将key值的过期时间设置为指定的timestamp秒数
  • pexpireat key timestamp:将key值的过期时间设置为指定的timestamp毫秒数

PS:不管使用哪一个命令,最终Redis底层都是使用pexpireat命令来实现的。另外,set等命令也可以设置key的同时加上过期时间,这样可以保证设值和设过期时间的原子性。

设置了有效期后,可以通过ttlpttl两个命令来查询剩余过期时间(如果未设置过期时间则下面两个命令返回-1,如果设置了一个非法的过期时间,则都返回-2):

  • ttl key返回key剩余过期秒数。
  • pttl key返回key剩余过期的毫秒数。

过期策略

如果将一个过期的键删除,我们一般都会有三种策略:

  • 定时删除:为每个键设置一个定时器,一旦过期时间到了,则将键删除。这种策略对内存很友好,但是对CPU不友好,因为每个定时器都会占用一定的CPU资源。如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/
  • 惰性删除:不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。
  • 定期扫描:系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。

Redis当中,其选择的是策略2和策略3的综合使用。不过Redis的定期扫描只会扫描设置了过期时间的键,因为设置了过期时间的键Redis会单独存储,所以不会出现扫描所有键的情况:

typedefstructredisDb{
dict*dict;//所有的键值对
dict*expires;//设置了过期时间的键值对
dict*blocking_keys;//被阻塞的key,如客户端执行BLPOP等阻塞指令时
dict*watched_keys;//WATCHEDkeys
intid;//DatabaseID
//...省略了其他属性
}redisDb;

8 种淘汰策略

假如Redis当中所有的键都没有过期,而且此时内存满了,那么客户端继续执行set等命令时Redis会怎么处理呢?Redis当中提供了不同的淘汰策略来处理这种场景。如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/

首先Redis提供了一个参数maxmemory来配置Redis最大使用内存:

maxmemory<bytes>

或者也可以通过命令config set maxmemory 1GB来动态修改。

如果没有设置该参数,那么在32位的操作系统中Redis最多使用3GB内存,而在64位的操作系统中则不作限制。

Redis中提供了8种淘汰策略,可以通过参数maxmemory-policy进行配置:

PS:淘汰策略也可以直接使用命令config set maxmemory-policy <策略>来进行动态配置。

LRU 算法

LRU全称为:Least Recently Used。即:最近最长时间未被使用。这个主要针对的是使用时间。

Redis 改进后的 LRU 算法

Redis当中,并没有采用传统的LRU算法,因为传统的LRU算法存在2个问题:

  • 需要额外的空间进行存储。
  • 可能存在某些key值使用很频繁,但是最近没被使用,从而被LRU算法删除。

为了避免以上2个问题,Redis当中对传统的LRU算法进行了改造,通过抽样的方式进行删除

配置文件中提供了一个属性maxmemory_samples 5,默认值就是5,表示随机抽取5key值,然后对这5key值按照LRU算法进行删除,所以很明显,key值越大,删除的准确度越高。如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/

对抽样LRU算法和传统的LRU算法,Redis官网当中有一个对比图:

  • 浅灰色带是被删除的对象。
  • 灰色带是未被删除的对象。
  • 绿色是添加的对象。

左上角第一幅图代表的是传统LRU算法,可以看到,当抽样数达到10个(右上角),已经和传统的LRU算法非常接近了。

Redis 如何管理热度数据

前面我们讲述字符串对象时,提到了redisObject对象中存在一个lru属性:

typedefstructredisObject{
unsignedtype:4;//对象类型(4位=0.5字节)
unsignedencoding:4;//编码(4位=0.5字节)
unsignedlru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节)
void *ptr;//指向底层实际的数据存储结构,如:SDS等(8字节)
}robj;

lru属性是创建对象的时候写入,对象被访问到时也会进行更新。正常人的思路就是最后决定要不要删除某一个键肯定是用当前时间戳减去lru,差值最大的就优先被删除。但是Redis里面并不是这么做的,Redis中维护了一个全局属性lru_clock,这个属性是通过一个全局函数serverCron每隔100毫秒执行一次来更新的,记录的是当前unix时间戳。

最后决定删除的数据是通过lru_clock减去对象的lru属性而得出的。那么为什么Redis要这么做呢?直接取全局时间不是更准确吗?如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/

这是因为这么做可以避免每次更新对象的lru属性的时候可以直接取全局属性,而不需要去调用系统函数来获取系统时间,从而提升效率(Redis当中有很多这种细节考虑来提升性能,可以说是对性能尽可能的优化到极致)。

不过这里还有一个问题,我们看到,redisObject对象中的lru属性只有24位,24位只能存储194天的时间戳大小,一旦超过194天之后就会重新从0开始计算,所以这时候就可能会出现redisObject对象中的lru属性大于全局的lru_clock属性的情况。

正因为如此,所以计算的时候也需要分为2种情况:

  • 当全局lruclock>lru,则使用lruclock-lru得到空闲时间。
  • 当全局lruclock<lru,则使用lruclock_max(即194天) -lru+lruclock得到空闲时间。

需要注意的是,这种计算方式并不能保证抽样的数据中一定能删除空闲时间最长的。这是因为首先超过194天还不被使用的情况很少,再次只有lruclock2轮继续超过lru属性时,计算才会出问题。

比如对象A记录的lru1天,而lruclock第二轮都到10天了,这时候就会导致计算结果只有10-1=9天,实际上应该是194+10-1=203天。但是这种情况可以说又是更少发生,所以说这种处理方式是可能存在删除不准确的情况,但是本身这种算法就是一种近似的算法,所以并不会有太大影响。

LFU 算法

LFU全称为:Least Frequently Used。即:最近最少频率使用,这个主要针对的是使用频率。这个属性也是记录在redisObject中的lru属性内。

当我们采用LFU回收策略时,lru属性的高16位用来记录访问时间(last decrement time:ldt,单位为分钟),低8位用来记录访问频率(logistic counter:logc),简称counter另外,如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/

访问频次递增

LFU计数器每个键只有8位,它能表示的最大值是255,所以Redis使用的是一种基于概率的对数器来实现counter的递增。r

给定一个旧的访问频次,当一个键被访问时,counter按以下方式递增:

  1. 提取01之间的随机数R
  2. counter- 初始值(默认为5),得到一个基础差值,如果这个差值小于0,则直接取0,为了方便计算,把这个差值记为baseval
  3. 概率P计算公式为:1/(baseval * lfu_log_factor + 1)
  4. 如果R < P时,频次进行递增(counter++)。

公式中的lfu_log_factor称之为对数因子,默认是10,可以通过参数来进行控制:

lfu_log_factor10

下图就是对数因子lfu_log_factor和频次counter增长的关系图:

可以看到,当对数因子lfu_log_factor100时,大概是10M(1000万)次访问才会将访问counter增长到255,而默认的10也能支持到1M(100万)次访问counter才能达到255上限,这在大部分场景都是足够满足需求的。另外,如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/

访问频次递减

如果访问频次counter只是一直在递增,那么迟早会全部都到255,也就是说counter一直递增不能完全反应一个key的热度的,所以当某一个key一段时间不被访问之后,counter也需要对应减少。

counter的减少速度由参数lfu-decay-time进行控制,默认是1,单位是分钟。默认值1表示:N分钟内没有访问,counter就要减N

lfu-decay-time1

具体算法如下:

  1. 获取当前时间戳,转化为分钟后取低16位(为了方便后续计算,这个值记为now)。
  2. 取出对象内的lru属性中的高16位(为了方便后续计算,这个值记为ldt)。
  3. lru>now时,默认为过了一个周期(16位,最大65535),则取差值65535-ldt+now:当lru<=now时,取差值now-ldt(为了方便后续计算,这个差值记为idle_time)。
  4. 取出配置文件中的lfu_decay_time值,然后计算:idle_time / lfu_decay_time(为了方便后续计算,这个值记为num_periods)。
  5. 最后将counter减少:counter - num_periods

看起来这么复杂,其实计算公式就是一句话:取出当前的时间戳和对象中的lru属性进行对比,计算出当前多久没有被访问到,比如计算得到的结果是100分钟没有被访问,然后再去除配置参数lfu_decay_time,如果这个配置默认为1也即是100/1=100,代表100分钟没访问,所以counter就减少100另外,如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:https://blog.didispace.com/spring-cloud-learning/

总结

本文主要介绍了Redis过期键的处理策略,以及当服务器内存不够时Redis8种淘汰策略,最后介绍了Redis中的两种主要的淘汰算法LRULFU



往期推荐

面试官:SpringMVC的控制器是单例的吗?

Spring Boot 2.x基础教程:使用@Scheduled实现定时任务

分享一款CHROME极速下载管理器插件

面试官:如何决定使用 HashMap 还是 TreeMap?

Spring Boot 中使用@Async实现异步调用,加速任务执行!

这款IDEA插件,可以让你用中文编码哟



一起进大厂,每日学干货

我回复【加群】,加入Java技术交流群




点击“”,领取 2021年最新免费技术资料大全

↓↓↓

相关资源