Redis淘汰策略

243 阅读5分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

image.png Redis通过设置maxmemory来设置redis可以占用的最大内存大小,或者也可以通过命令 config set maxmemory 1GB 来动态修改。

maxmemory <bytes>

如果没有设置该参数,那么在 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默认策略,不作任何处理,直接报错

Redis改进后的LRU

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

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

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

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

配置文件中提供了一个属性 maxmemory_samples 5,默认值就是 5,表示随机抽取 5 个 key 值,然后对这 5 个 key 值按照 LRU 算法进行删除,所以很明显,key 值越大,删除的准确度越高。

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_RESOLUTION)。相当一个时间区间。

最后决定删除的数据是通过 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 天)+ lruclock -lru 得到空闲时间。

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

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


新的LFU模式

LFU 全称为:Least Frequently Used。即:最近最少频率使用,这个主要针对的是使用频率。

访问频次递增

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

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

  1. 提取 0 和 1 之间的随机数 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

访问频次递减

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

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

lfu-decay-time 1

参考资料