redis淘汰策略

redis淘汰策略       

        redis当内存不够时,此时如果redis中没有数据过期,那么redis就会不得已去淘汰一些没有过期的数据。reids一共提供了

Key eviction | Docs 【redis官网】

  • noeviction: New values aren’t saved when memory limit is reached. When a database uses replication, this applies to the primary database   
    默认的不删除,一个思想就是:不背锅
  • allkeys-lru: Keeps most recently used keys; removes least recently used (LRU) keys 
    所有范围内-最久不访问
  • allkeys-lfu: Keeps frequently used keys; removes least frequently used (LFU) keys
    所有范围内-最不经常访问
  • volatile-lru: Removes least recently used keys with the expire field set to true.
    有过期时间范围内-最久不访问
  • volatile-lfu: Removes least frequently used keys with the expire field set to true.
    有过期时间范围内-最不经常访问
  • allkeys-random: Randomly removes keys to make space for the new data added.
    所有范围内-随机
  • volatile-random: Randomly removes keys with expire field set to true.
    有过期时间范围内-随机
  • volatile-ttl: Removes keys with expire field set to true and the shortest remaining time-to-live (TTL) value.
    根据过期时间来淘汰即将过期的

既然提供了8种策略,肯定就有地方供我们配置

 maxmemory-policy noeviction

LRU

        Least Recently Used 最久未使用,就算之前很久没有使用过,只要近期使用过一次,我就认为是有效的。 

 从上面这个图可以看到,它大体可以分为2个动作:第一个是移动顺序,把最新的数据放在前面;

 第二个是查处最后一个数据淘汰。为了保证时间复杂度为哦o(1),可以用hash+双向链表来实现。

但是,redis的LRU是这么实现的吗?肯定不是的,因为这样要用额外的数据结构存储,

redis的LRU

redisObject:redis所有数据结构的对象

/* server.h 620 行*/
typedef struct redisObject { 
    unsigned type:4; /* 类型  list set zset */ 
    unsigned encoding:4; /* sds */ 
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock)*/ 
    int refcount; 
    void *ptr; 
} robj;

这个结构中有个属性:lru:LRU_BITS 就是来跟lru算法有关的,并且存储的是时间相关信息,最后一次访问时间的秒单位,但是只有24bit,无符号。 


long timeMillis=System.currentTimeMillis(); 
System.out.println(timeMillis/1000); //获取当前秒 
System.out.println(timeMillis/1000 & 16777215); //获取秒的


// 多久时间没有访问了
(timeMillis/1000 & 16777215)-(redisObject.lru)

redisObject记录了对象最后一次访问时间,那么计算该对象多久没访问就很简单了:

当前时间减去该对象最后一次访问时间就可以了。 

但是这个时候就有个隐藏的问题:

11111111111111111000000000011111110 
//假如这个是我当前秒单位的时间,获取后8位 是 11111110 
11111111111111111000000000011111111 
//获取后8位 是 11111111 
11111111111111111000000000100000000 
//获取后8位 是 00000000

有个轮询的问题,它如果超过24位,又会从0开始,所以我们不能直接的用系统时间秒单位的

24bit 位去减对象的 lru, 而是要判断一下。
// evict.c
unsigned long long estimateObjectIdleTime(robj *o) { 
    //获取秒单位时间的最后24位 
    unsigned long long lruclock = LRU_CLOCK(); 
    //因为只有24位,所有最大的值为2的24次方-1 //超过最大值从0开始,所以需要判断lruclock(当前系统时间)跟缓存对象的lru字段的大小 
    if (lruclock >= o->lru) { 
        //如果lruclock>=robj.lru,返回lruclock-o->lru,再转换单位 
        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
     } else { 
        //否则采用lruclock + (LRU_CLOCK_MAX - o->lru),得到对象的值越小,返回的值越大, 越大越容易被淘汰 
        return (lruclock + (LRU_CLOCK_MAX - o->lru)) * LRU_CLOCK_RESOLUTION; 
    }
 }
用server.lruclock 与 对象的 lru字段进行比较,因为server.lruclock只获取了当前秒单位时间的后 24位,所以肯定有个轮询。所以,我们会判断server.lruclock跟对象的lru字段进行比较,如果server.lruclock>obj.lru,那么我们用server.lruclock-obj.lru,否则server.lruclock+ (LRU_CLOCK_MAX-obj.lru),得到lru越小,那么返回的数据越大,相差越大的就会被淘汰
那么有个问题来了,如果轮询了2、3、4....n轮呢?
16777215*2/60/60/24=388 天 一年多.....  不管了,管不了那么多了,统一按一轮处理

redis的LFU

        Least Frequently Used 最不常用,根据访问次数和访问时间来决定淘汰。

/* server.h 620 行*/
typedef struct redisObject { 
    unsigned type:4; /* 类型  list set zset */ 
    unsigned encoding:4; /* sds */ 
    unsigned lru:LRU_BITS; /* LFU data (least significant 8 bits frequency 
     and most significant 16 bits access time). */ 
  
    int refcount; 
    void *ptr; 
} robj;

还是看redisObject,如果是LFU的时候,前面16位代表时间,后面8位代表的是一个数值,frequency代表的是频率,代表着访问的次数(counter)。那么8位 ,2的8次方 256次,够用么?讲道理肯定不够,那看redis的解决思路:既然不够,那么让它不用每次都加就可以了,能不能让它值越大,我们加的越慢就能解决这个问题,redis还加了个东西,让你们自己能控制它加的速率!!这个东西就是 lfu-log-factor!它配置的越大,那么对象的访问次数就会加的越慢。

/*evict.c 328行*/
uint8_t LFULogIncr(uint8_t counter) { 
    //如果已经到最大值255,返回255 ,8位的最大值 
    if (counter == 255) return 255; 
    //得到随机数(0-1)
    double r = (double)rand()/RAND_MAX; 
    //LFU_INIT_VAL表示基数值(在server.h配置) 
    double baseval = counter - LFU_INIT_VAL; 
    //如果达不到基数值,表示快不行了,baseval =0 
    if (baseval < 0) baseval = 0; 
    //如果快不行了,肯定给他加counter 
    //不然,按照几率是否加counter,同时跟baseval与lfu_log_factor相关 
    //都是在分子,所以2个值越大,加counter几率越小 
    double p = 1.0/(baseval*server.lfu_log_factor+1); 
    if (r < p) counter++; return counter;
 }
1. 如果已经是最大值,我们不再加!因为到达 255 的几率不是很高!可以支撑很大很大的数据量!
2. counter 属于随机添加,添加的几率根据已有的 counter 值和配置 lfu-log-factor 相关, counter 值越
大,添加的几率越小, lfu-log-factor 配置的值越大,添加的几率越小!
这个次数只加不减?
上面说的16bit的时间是用来减次数的, 根据这个时间判断 这个对象多久没访问了就去减少次数。
这个次数每分钟没访问减多少呢?
根据配置lfu-decay-time
unsigned long LFUDecrAndReturn(robj *o) { 
    //lru字段右移8位,得到前面16位的时间 
    unsigned long ldt = o->lru >> 8; 
    //lru字段与255进行&运算(255代表8位的最大值),
    //得到8位counter值 
    unsigned long counter = o->lru & 255; 
    //如果配置了lfu_decay_time,用LFUTimeElapsed(ldt) 除以配置的值 
    //LFUTimeElapsed(ldt)源码见下 
    //总的没访问的分钟时间/配置值,得到每分钟没访问衰减多少 
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) /              
               server.lfu_decay_time : 0; 
    if (num_periods) 
        //不能减少为负数,非负数用couter值减去衰减值 
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter; 
}

//对象ldt时间的值越小,则说明时间过得越久
unsigned long LFUTimeElapsed(unsigned long ldt) { 
    //得到当前时间分钟数的后面16位
    unsigned long now = LFUGetTimeInMinutes(); 
    //如果当前时间分钟数的后面16位大于缓存对象的16位   
    //得到2个的差值 
    if (now >= ldt) 
        return now-ldt; 
    //如果缓存对象的时间值大于当前时间后16位值,则用65535-ldt+now得到差值 
    return 65535-ldt+now;
}
1. 我们可以根据对象的 LRU 字段的前 16 位得到对象的访问时间(分钟),根据跟系统时间比较获取到 多久没有访问过!
2. 根据 lfu-decay-time (配置),代表每分钟没访问减少多少 counter, 不能减成负数
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值