redis 订阅key过期 redis key过期机制_redis 订阅key过期

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的内存块耗尽。那么就应该采用内存淘汰机制。

redis 订阅key过期 redis key过期机制_数据库_02

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;
}