浅谈内存耗尽后Redis会发生什么
前言
作为一台服务器来说,内存并不是无限的,所以总会存在内存耗尽的情况,那么当Redis服务器的内存耗尽后,如果继续执行请求命令,Redis会如何处理呢?
内存回收
使用Redis服务时,很多情况下某些键值对只会在特定的时间内有效,为了防止这种类型的数据一直占有内存,我们可以给键值对设置有效期。Redis中可以通过4个独立的命令来给一个键设置过期时间:
- expirekeyttl:将key值的过期时间设置为ttl秒。
- pexpirekeyttl:将key值的过期时间设置为ttl毫秒。
- expireatkeytimestamp:将key值的过期时间设置为指定的timestamp秒数。
- pexpireatkeytimestamp:将key值的过期时间设置为指定的timestamp毫秒数。
PS:不管使用哪一个命令,最终Redis底层都是使用pexpireat命令来实现的。另外,set等命令也可以设置key的同时加上过期时间,这样可以保证设值和设过期时间的原子性。
设置了有效期后,可以通过ttl和pttl两个命令来查询剩余过期时间(如果未设置过期时间则下面两个命令返回-1,如果设置了一个非法的过期时间,则都返回-2):
- ttlkey返回key剩余过期秒数。
- pttlkey返回key剩余过期的毫秒数。
过期策略
如果将一个过期的键删除,我们一般都会有三种策略:
- 定时删除:为每个键设置一个定时器,一旦过期时间到了,则将键删除。这种策略对内存很友好,但是对CPU不友好,因为每个定时器都会占用一定的CPU资源。
- 惰性删除:不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。
- 定期扫描:系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。
在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当中提供了不同的淘汰策略来处理这种场景。
首先Redis提供了一个参数maxmemory来配置Redis最大使用内存:
maxmemory
或者也可以通过命令configsetmaxmemory1GB来动态修改。
如果没有设置该参数,那么在32位的操作系统中Redis最多使用3GB内存,而在64位的操作系统中则不作限制。
Redis中提供了8种淘汰策略,可以通过参数maxmemory-policy进行配置:
淘汰策略 | 说明 |
---|---|
volatile-lru | 根据LRU算法删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
allkeys-lru | 根据LRU算法删除所有的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
volatile-lfu | 根据LFU算法删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
allkeys-lfu | 根据LFU算法删除所有的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
volatile-random | 随机删除设置了过期时间的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
allkeys-random | 随机删除所有键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错 |
volatile-ttl | 根据键值对象的ttl属性,删除最近将要过期数据。如果没有,则直接报错 |
noeviction | 默认策略,不作任何处理,直接报错 |
PS:淘汰策略也可以直接使用命令configsetmaxmemory-policy<策略>来进行动态配置。
LRU算法
LRU全称为:LeastRecentlyUsed。即:最近最长时间未被使用。这个主要针对的是使用时间。
Redis改进后的LRU算法
在Redis当中,并没有采用传统的LRU算法,因为传统的LRU算法存在2个问题:
- 需要额外的空间进行存储。
- 可能存在某些key值使用很频繁,但是最近没被使用,从而被LRU算法删除。
为了避免以上2个问题,Redis当中对传统的LRU算法进行了改造,通过抽样的方式进行删除。
配置文件中提供了一个属性maxmemory_samples5,默认值就是5,表示随机抽取5个key值,然后对这5个key值按照LRU算法进行删除,所以很明显,key值越大,删除的准确度越高。
对抽样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字节) intrefcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节) void*ptr;//指向底层实际的数据存储结构,如:SDS等(8字节) }robj;
lru属性是创建对象的时候写入,对象被访问到时也会进行更新。正常人的思路就是最后决定要不要删除某一个键肯定是用当前时间戳减去lru,差值最大的就优先被删除。但是Redis里面并不是这么做的,Redis中维护了一个全局属性lru_clock,这个属性是通过一个全局函数serverCron每隔100毫秒执行一次来更新的,记录的是当前unix时间戳。
最后决定删除的数据是通过lru_clock减去对象的lru属性而得出的。那么为什么Redis要这么做呢?直接取全局时间不是更准确吗?
这是因为这么做可以避免每次更新对象的lru属性的时候可以直接取全局属性,而不需要去调用系统函数来获取系统时间,从而提升效率(Redis当中有很多这种细节考虑来提升性能,可以说是对性能尽可能的优化到极致)。
不过这里还有一个问题,我们看到,redisObject对象中的lru属性只有24位,24位只能存储194天的时间戳大小,一旦超过194天之后就会重新从0开始计算,所以这时候就可能会出现redisObject对象中的lru属性大于全局的lru_clock属性的情况。
正因为如此,所以计算的时候也需要分为2种情况:
- 当全局lruclock>lru,则使用lruclock-lru得到空闲时间。
- 当全局lruclock
需要注意的是,这种计算方式并不能保证抽样的数据中一定能删除空闲时间最长的。这是因为首先超过194天还不被使用的情况很少,再次只有lruclock第2轮继续超过lru属性时,计算才会出问题。
比如对象A记录的lru是1天,而lruclock第二轮都到10天了,这时候就会导致计算结果只有10-1=9天,实际上应该是194+10-1=203天。但是这种情况可以说又是更少发生,所以说这种处理方式是可能存在删除不准确的情况,但是本身这种算法就是一种近似的算法,所以并不会有太大影响。
LFU算法
LFU全称为:LeastFrequentlyUsed。即:最近最少频率使用,这个主要针对的是使用频率。这个属性也是记录在redisObject中的lru属性内。
当我们采用LFU回收策略时,lru属性的高16位用来记录访问时间(lastdecrementtime:ldt,单位为分钟),低8位用来记录访问频率(logisticcounter:logc),简称counter。
访问频次递增
LFU计数器每个键只有8位,它能表示的最大值是255,所以Redis使用的是一种基于概率的对数器来实现counter的递增。r
给定一个旧的访问频次,当一个键被访问时,counter按以下方式递增:
- 提取0和1之间的随机数R。
- counter-初始值(默认为5),得到一个基础差值,如果这个差值小于0,则直接取0,为了方便计算,把这个差值记为baseval。
- 概率P计算公式为:1/(baseval*lfu_log_factor+1)。
- 如果R
公式中的lfu_log_factor称之为对数因子,默认是10,可以通过参数来进行控制:
lfu_log_factor10
下图就是对数因子lfu_log_factor和频次counter增长的关系图:
可以看到,当对数因子lfu_log_factor为100时,大概是10M(1000万)次访问才会将访问counter增长到255,而默认的10也能支持到1M(100万)次访问counter才能达到255上限,这在大部分场景都是足够满足需求的。
访问频次递减
如果访问频次counter只是一直在递增,那么迟早会全部都到255,也就是说counter一直递增不能完全反应一个key的热度的,所以当某一个key一段时间不被访问之后,counter也需要对应减少。
counter的减少速度由参数lfu-decay-time进行控制,默认是1,单位是分钟。默认值1表示:N分钟内没有访问,counter就要减N。
lfu-decay-time1
具体算法如下:
- 获取当前时间戳,转化为分钟后取低16位(为了方便后续计算,这个值记为now)。
- 取出对象内的lru属性中的高16位(为了方便后续计算,这个值记为ldt)。
- 当lru>now时,默认为过了一个周期(16位,最大65535),则取差值65535-ldt+now:当lru<=now时,取差值now-ldt(为了方便后续计算,这个差值记为idle_time)。
- 取出配置文件中的lfu_decay_time值,然后计算:idle_time/lfu_decay_time(为了方便后续计算,这个值记为num_periods)。
- 最后将counter减少:counter-num_periods。
看起来这么复杂,其实计算公式就是一句话:取出当前的时间戳和对象中的lru属性进行对比,计算出当前多久没有被访问到,比如计算得到的结果是100分钟没有被访问,然后再去除配置参数lfu_decay_time,如果这个配置默认为1也即是100/1=100,代表100分钟没访问,所以counter就减少100。
总结
本文主要介绍了Redis过期键的处理策略,以及当服务器内存不够时Redis的8种淘汰策略,最后介绍了Redis中的两种主要的淘汰算法LRU和LFU。
到此这篇关于浅谈内存耗尽后Redis会发生什么的文章就介绍到这了,更多相关Redis内存耗尽内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。