redis淘汰策略
redis当内存不够时,此时如果redis中没有数据过期,那么redis就会不得已去淘汰一些没有过期的数据。reids一共提供了
- 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 totrue
.
有过期时间范围内-最久不访问- volatile-lfu: Removes least frequently used keys with the
expire
field set totrue
.
有过期时间范围内-最不经常访问- allkeys-random: Randomly removes keys to make space for the new data added.
所有范围内-随机- volatile-random: Randomly removes keys with
expire
field set totrue
.
有过期时间范围内-随机- volatile-ttl: Removes keys with
expire
field set totrue
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开始,所以我们不能直接的用系统时间秒单位的
// 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;
}
}
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;
}
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;
}