Redis的LRU和LFU浅谈

1.概论      

        Redis中的缓存淘汰算法大体分为两种,volatile-xxx和allkeys-xxx。volatile-xxx 是对带过期时间的 key 进行淘汰,即使用了expire的key;allkeys-xxx 策略会对所有的key 进行淘汰。如果只是用redis做缓存,几乎不使用持久化功能,则使用allkeys-xxx,每个key不带过期时间。而如果要使用持久化,或key的量级特别大的时候使用 volatile-xxx策略,这样可以保留没有设置过期时间的key,它们是永久的 key 不会被淘汰算法淘汰。

        但即使设置了expire,也不是在他过期后直接淘汰,这样需要维护一个轮询的线程,还需要考虑读取个删除线程的冲突,会增加额外复杂度,故redis是在每次获取key时,先去检查是否过期,过期返回(nil)。

      具体的淘汰策略中,比较常用的有LRU和LFU两种:

        LRU(The Least Recently Used,最近最久未使用算法):如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。

        LFU(Least Frequently Used ,最近最少使用算法):如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。

        这是操作系统的课程中内存做换页时的两种经典算法,但实际实现起来非常麻烦,如果使用某些数据结构,但就LRU而言,就将近100行代码。(字节跳动就爱让你手撕这个,我好多同学面试的时候就把他背下来,直接默写)。

        而Redis中的实现比较不同,个人认为最大的区别在于随机数的使用。详情如下

2.LRU 算法

        从LRU的实现上来讲,要么额外存储一个访问时间字段,要么将所有key做成链表,每次读取key,对链表做重新排序。原生的LRU用的就是第二种消耗大量的额外的内存,且需要大量链表操作和排序。而Redis中的实习则很简单,类似于第一种,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和 LRU算法非常近似的效果(详细效果下面有图可以看到)。 Redis 为实现近似LRU 算法,它给每个 key 增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。

        而LRU 淘汰不一样,它的处理方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory(想到要阈值,可以通过参数配置),就会执行一次LRU 淘汰算法。这个淘汰就是我上面说的随机的,随机采样出 5(默认5,可以通过参数配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于maxmemory 为止。

        而采样范围就是看maxmemory-policy选择的是allkeys还是volatile。

        但这种随机性肯定会带来一个局部性问题,举例来说就是现在有10个key,由新到旧分别是1,2,3,4,5,6,7,8,9,10,如果现在要淘汰三个,严格的LRU应该淘汰8,9,10,但redis可能存在取样取出来1,2,3,4,5,此时只能淘汰5。这样的随机性可能带来淘汰的不严格,但因为实现简单,不会待读写key造成太大硬性,还是可以接收。

        而且在Redis 3.0中对近似LRU还做了一点小优化,也就是增加了一个数组,叫淘汰池(eviction poo,大小是 maxmemory_samples,可设置),在每一次淘汰循环中,新随机出来的 key 列表会和淘汰池中的 key 列表进行比较和排序,淘汰掉最旧的一个 key 之后,保留剩余较旧的 key 列表放入淘汰池中留待下一个循环。因为随机性可能会使得淘汰的数据过新,上一次淘汰的数远比这次淘汰的数新,使用淘汰池将这个问题解决。注:淘汰池中的key并未清除,仍可读取。

        这个是很小的优化,且内存和时间上的开销并不大,但对算法的提升还是很大的,如图

         图中绿色部分是新加入的 key,深灰色部分是老旧的 key,浅灰色部分是通过 LRU 算法淘汰掉的 key。从图中可以看出采样数量越大,近似 LRU 算法的效果越接近严格 LRU算法。同时 Redis3.0 在算法中增加了淘汰池,进一步提升了近似 LRU 算法的效果。

3.LFU

        LRU即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。所以要存储两个额外字段,一个是时间区间,一个是访问次数。Redis在4.0以后支持,Redis使用使用24bit存储:其中16bit用于表示上一次递减时间 (时间戳的后16位),8bit表示访问次数。注:8位只能代表255,但是redis并没有采用线性上升的方式,而是通过一个复杂的公式,通过配置两个参数来调整数据的递增速度。如在影响因子 lfu-log-factor 为10的情况下,经过1百万次命中才能达到 255。

        每次读取key会检查这24bit,根据当前时间戳key中那24bit存的时间的差值,如果距离现在超过 N 分钟(可配置lfu-decay-time),则递减或者减半,然后通过衰减后的24bit通过LFULogIncr()重新计算后更新这24bit。具体如何减少见代码

void updateLFU(robj *val) {
    //衰减
    unsigned long counter = LFUDecrAndReturn(val);
    //根据衰减后的counter重新计算新的counter
    counter = LFULogIncr(counter);
    val->lru = (LFUGetTimeInMinutes()<<8) | counter; 
}
 
unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;
    unsigned long counter = o->lru & 255; 
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    //server.lfu_decay_time默认为1,每经过一分钟counter衰减1
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;//如果需要衰减,则计算衰减后的值
    return counter;
}
 
uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;//counter最大只能存储到255,到达后不再增加
    double r = (double)rand()/RAND_MAX;//算一个随机的小数值
//新加入的key初始counter设置为LFU_INIT_VAL,默认5
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0;
    double p = 1.0/(baseval*server.lfu_log_factor+1);//server.lfu_log_facotr默认为10
//counter越大,则p越小,随机值r小于p的情况就越少,counter增加得越来越慢
    if (r < p) counter++;
    return counter;
}
 
unsigned long LFUGetTimeInMinutes(void) {
    return (server.unixtime/60) & 65535;//获取分钟级别的时间戳
}

        lfu随着分钟数对counter做衰减是基于一个原理:过去被大量访问的key不一定现在仍然会被访问。相当于除了计数,给时间也增加了一定的权重进行递减。

        而默认情况下新写的key的后8位计数器的值为5(可配置),防止因为访问频率过低而直接被删除。

        删除时,还是随机的,随机采样N个key(与LRU一样),淘汰25bit最小的。

参考文献:

[1] 官方文档,Key eviction

[2] 钱文品,Redis深度历险:核心原理和应用实践,书籍。

[3]知乎:张老师,Redis中的LRU算法,一篇文章彻底搞懂

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值