作者:双子孤狼

www.cnblogs.com/lonely-wolf/p/14403264.html

前言

作为一台服务器来说,内存并不是无限的,所以总会存在内存耗尽的情况,那么当 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 资源。

  • 惰性删除:不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。

  • 定期扫描:系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。

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

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

8 种淘汰策略

假如 Redis 当中所有的键都没有过期,而且此时内存满了,那么客户端继续执行 set 等命令时 Redis 会怎么处理呢?Redis 当中提供了不同的淘汰策略来处理这种场景。

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

maxmemory <bytes>

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

如果没有设置该参数,那么在 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:淘汰策略也可以直接使用命令 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 值越大,删除的准确度越高。

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

  • 浅灰色带是被删除的对象。

  • 灰色带是未被删除的对象。

  • 绿色是添加的对象。

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

Redis 如何管理热度数据

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

typedef struct redisObject {unsigned type:4;//对象类型(4位=0.5字节)unsigned encoding:4;//编码(4位=0.5字节)unsigned lru: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 要这么做呢?直接取全局时间不是更准确吗?

这是因为这么做可以避免每次更新对象的 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

访问频次递增

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_factor 10

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

可以看到,当对数因子 lfu_log_factor100 时,大概是 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-time 1

具体算法如下:

  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

总结

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

Spring Cloud:统一异常处理
Spring 中的 Controller 和Service是线程安全的吗?JAVA实现PDF和EXCEL生成和数据动态插入以及导出SpringBoot+ Dubbo + Mybatis + Nacos +Seata整合来实现Dubbo分布式事务一个 SpringBoot 项目该包含哪些?
发现一款牛逼的IDEA插件:检测代码漏洞,一键修复!
springboot+redis+Interceptor+自定义annotation实现接口自动幂等100W个微信红包封面,速度领取!!!
API接口的安全设计验证:ticket,签名,时间戳点击阅读全文前往微服务电商教程

阿里面试官:内存耗尽后Redis会崩?相关推荐

  1. 天猫二面:内存耗尽后Redis会发生什么?

    原文地址:https://www.cnblogs.com/lonely-wolf/p/14403264.html 作为一台服务器来说,内存并不是无限的,所以总会存在内存耗尽的情况,那么当 Redis ...

  2. 内存耗尽后Redis会发生什么

    要想用活Redis,Lua脚本是绕不过去的坎 前言 内存回收 过期策略 8 种淘汰策略 LRU 算法 Redis 改进后的 LRU 算法 Redis 如何管理热度数据 LFU 算法 访问频次递增 访问 ...

  3. 京东二面:内存耗尽后Redis会发生什么?

    程序员的成长之路 互联网/程序员/技术/资料共享 关注 阅读本文大概需要 7 分钟. 来自:cnblogs.com/lonely-wolf/p/14403264.html # 前言 作为一台服务器来说 ...

  4. 面试官问我:Redis 内存满了怎么办

    转载自 想不到!面试官问我:Redis 内存满了怎么办 Redis占用内存大小 Redis的内存淘汰 LRU算法 LRU在Redis中的实现 LFU算法 问题 Redis占用内存大小 我们知道Redi ...

  5. 内存耗尽后,Redis会发生什么?

    点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/zwx900102/article/ details/113806440 前言 作为一台服务器来说,内存并不是无限的,所以总会存 ...

  6. 阿里面试官:Redis分片集群都不懂?还来面试?

    面试官:聊下Redis的分片集群,先聊 Redis Cluster好咯? 面试官:Redis Cluser是Redis 3.x才有的官方集群方案,这块你了解多少? 候选者:嗯,要不还是从基础讲起呗? ...

  7. mysql查询前段时间_没想到!我在简历上写了“精通MySQL”,阿里面试官跟我死磕后就给我发了高薪offer...

    事情是这样的 前段时间面试了阿里,大家也都清楚,如果你在简历上面写着你精通XX技术,那面试官就会跟你死磕到底. 我就是在自己的简历上写了精通MySQL,然后就开启了和阿里面试官的死磕之路,结果就是拿到 ...

  8. 一条mysql语句是事务吗_没想到!我在简历上写了“精通MySQL”,阿里面试官跟我死磕后就给我发了高薪offer...

    事情是这样的 前段时间面试了阿里,大家也都清楚,如果你在简历上面写着你精通XX技术,那面试官就会跟你死磕到底. 我就是在自己的简历上写了精通MySQL,然后就开启了和阿里面试官的死磕之路,结果就是拿到 ...

  9. 面试官:你对Redis缓存了解吗?面对这11道面试题是否有很多问号?

    关于Redis的知识总结了一个脑图分享给大家 1.在项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果? 面试官心理分析 这个问题,互联网公司必问,要是一个人连缓存都不太清楚,那确实比 ...

最新文章

  1. ubuntu 16.0.4 opencv 4.0.0 + opencv_contrib 4.0.0 cmake-gui 安装
  2. matlab求系统稳定时k的范围,Matlab大作业
  3. mysql 5.7.11 my.ini,mysql5.7以上版本配置my.ini的详细步骤
  4. 以 vim 的方式来使用 chrome 浏览器(利用 vimium 插件)
  5. 工作221:控制弹出框不全屏
  6. 升级python以及安装anaconda
  7. 请问一下:诸位遇到输入汉字时会重复输入
  8. Python 脚本相关知识
  9. UI设计开发工具介绍
  10. 如何由 XRD 图谱确定所做的样品是准晶结构
  11. 【Unity3D插件】FancyScrollView插件分享《通用UI滑动列表》
  12. c#mvc模式进行crud_实用的微服务开发模式:CRUD与。 CQRS
  13. 计算机光驱无法启用,光驱提示:无法访问G:\函数不正确解决方法
  14. android provided without an @Inject constructor or an @Provides-annotated method.报错原因
  15. 基于unity的直升机模拟设计
  16. 从 HTTP 瞎逼逼到 HTTP/2
  17. texstudio使用
  18. javaweb医院绩效考核管理信息系统
  19. Google奥运会logo
  20. python七巧板拼图_数学思维是什么?如何培养?

热门文章

  1. NumPy二维数组-行向量、列向量
  2. Python爬取淘宝商品数据,价值千元的爬虫外包项目
  3. php中socket的使用,php中socket的用法详解,phpsocket详解_PHP教程
  4. uni-app 小程序获取实时定位和车辆签到(wx.getLocation方法)
  5. 各种手机处理器排行榜_最新手机CPU排名(排行榜)
  6. java 修改word_Java 修改/替换Word批注
  7. 团建活动策划、年会组织、HR总结资料大全
  8. 2021.10.20股票小计
  9. 04 WebFlux和SpringMVC有什么区别
  10. Excel 下拉菜单