1 缓存过期策略
果我们设置了Redis的key-value的过期时间,当缓存中的数据过期之后,Redis就需要将这些数据进行清除,释放占用的内存空间。Redis中主要使用 定期删除 + 惰性删除 两种数据过期清除策略。
1.1 定期删除
redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。注意这里是随机抽取的。
-
为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。
1.2 惰性删除
定期删除可能导致很多过期的key 到了时间并没有被删除掉。这时就要使用到惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且过期了,是的话就删除。
1.3 定时删除
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略。
2 内存淘汰机制
如果大量过期的key堆积在内存中,redis的内存会越来越高,导致redis的内存块耗尽。那么就应该采用内存淘汰机制。
2.1 八种机制
通常情况下推荐优先使用 allkeys-lru 策略。这样可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。
-
noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。
-
volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。
-
volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。
-
volatile-lru:在设置了过期时间的键值对中,移除最近最少使用的键值对。
-
volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对
-
allkeys-random:在所有键值对中,随机移除某个key。
-
allkeys-lru:在所有的键值对中,移除最近最少使用的键值对。
-
allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对
2.2 LRU算法
LRU 算法的全称是 Least Recently Uses,按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来。LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。
如果按照HashMap和双向链表实现,需要额外的存储存放 next 和 prev 指针,牺牲比较大的存储空间,显然是不划算的。所以Redis采用了一个近似的做法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的,具体分析如下:
......
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
for (k = 0; k < server.maxmemory_samples; k++) {
sds thiskey;
long thisval;
robj *o;
de = dictGetRandomKey(dict);
thiskey = dictGetKey(de);
/* volatile-lru机制时执行,从过期字典中获取 */
if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
de = dictFind(db->dict, thiskey);
o = dictGetVal(de);
thisval = estimateObjectIdleTime(o);
/* 空闲时间更大的作为淘汰键 */
if (bestkey == NULL || thisval > bestval) {
bestkey = thiskey;
bestval = thisval;
}
}
}
......
2.3 LFU算法
LFU(Least Frequently Used) 是在Redis4.0后出现的,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。LFU算法能更好的表示一个key被访问的热度。最近最不频繁使用,跟使用的次数有关,淘汰使用次数最少的。
之前的24位的lru字段,在lfu算法下,前16位将会存放最后一次访问的时间,精确到分钟,而后八位将会记录一个couter频率值,作为判定的依据。
// 以下是在访问key是更新频率的函数LFULogIncr()。
uint8_t LFULogIncr(uint8_t counter) {
// 频率不超过255
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*server.lfu_log_factor+1);
// 比例大于随机数则新增count,避免新增过于频繁
if (r < p) counter++;
return counter;
}
// 长时间没访问需要降低访问次数
unsigned long LFUDecrAndReturn(robj *o) {
// 上次访问时间,单位min
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;
// 返回当前计算后的访问频率
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
unsigned long LFUTimeElapsed(unsigned long ldt) {
unsigned long now = LFUGetTimeInMinutes();
if (now >= ldt) return now-ldt;
return 65535-ldt+now;
}