关于java:天猫二面内存耗尽后-Redis-会发生什么

4次阅读

共计 4813 个字符,预计需要花费 13 分钟才能阅读完成。

作为一台服务器来说,内存并不是有限的,所以总会存在内存耗尽的状况,那么当 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 的同时加上过期工夫,这样能够保障设值和设过期工夫的原子性。

设置了有效期后,能够通过 ttl 和 pttl 两个命令来查问残余过期工夫(如果未设置过期工夫则上面两个命令返回 -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 keys
        int 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,示意随机抽取 5 个 key 值,而后对这 5 个 key 值依照 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 天还不被应用的状况很少,再次只有 lruclock 第 2 轮持续超过 lru 属性时,计算才会出问题。

比方对象 A 记录的 lru 是 1 天,而 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 按以下形式递增:

  • 提取 0 和 1 之间的随机数 R。
  • counter – 初始值(默认为 5),失去一个根底差值,如果这个差值小于 0,则间接取 0,为了不便计算,把这个差值记为 baseval。
  • 概率 P 计算公式为:1/(baseval * lfu_log_factor + 1)。
  • 如果 R < P 时,频次进行递增(counter++)。
  • 公式中的 lfu_log_factor 称之为对数因子,默认是 10,能够通过参数来进行管制:

    lfu_log_factor 10

    下图就是对数因子 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-time 1

具体算法如下:

  • 获取以后工夫戳,转化为分钟 后取低 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。

起源:cnblogs.com/lonely-wolf/p/14403264.html

正文完
 0