Redis 过时淘汰策略

Redis 过时淘汰策略

redis的过时淘汰策略是很是值得去深刻了解以及考究的一个问题。不少使用者每每不能深得其意,每每停留在人云亦云的程度,若生产不出事故便划水就划过去了,可是当生产数据莫名其妙的消失,或者reids服务崩溃的时候,却又一筹莫展。本文尝试着从浅入深的将redis的过时策略剖析开来,指望帮助做者以及读者站在一个更加系统化的角度去看待过时策略。redis

redis做为缓存数据库,其底层数据结构主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过时时间。访问磁盘空间的成本是访问缓存的成本高出很是多,因此内存的成本比磁盘空间要大。在实际使用中,缓存的空间每每极为有限,因此为了在为数很少的容量中作到真正的物尽其用,必需要对缓存的容量进行管控。算法

内存策略

redis经过配置maxmemory来配置最大容量(阈值),当数据占有空间超过所设定值就会触发内部的内存淘汰策略(内存释放)。那么究竟要淘汰哪些数据,才是最符合业务需求?或者在业务容忍的范围内呢?为了解决这个问题,redis提供了可配置的淘汰策略,让使用者能够配置适合本身业务场景的淘汰策略,不配置的状况下默认是使用volatile-lru数据库

  • noeviction:当内存不足以容纳新写入数据时,新写入操做会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,有更早过时时间的key优先移除。

若是redis保存的key-value对数量很少(好比数十对),那么当内存超过阈值后,对整个内存空间的全部key进行检查,也无伤大雅。然而,在实际使用中redis保存的key-value数量远远不止于此,若是使用内存超过阈值就逐个去检查是否符合过时策略吗?随意遍历十万个key?显然不是,不然,redis又何以高性能著称?可是,检查多少key这个问题确实存在。为了解决该问题,redis的设计者们引入一个配置项maxmemory-samples,称之为过时检测样本,默认值是3,经过它来曲线救国。缓存

过时检测样本是如何配合redis来进行数据清理呢?服务器

mem_used内存已经超过maxmemory的设定,对于全部的读写请求,都会触发redis.c/freeMemoryIfNeeded函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出足够的内存空间。因此若是在达到maxmemory而且调用方还在不断写入的状况下,可能会反复触发主动清理策略,致使请求会有必定的延迟。数据结构

清理时会根据用户配置的maxmemory政策来作适当的清理(通常是LRU或TTL),这里的LRU或TTL策略并非针对redis的的全部键,而是以配置文件中的maxmemory样本个键做为样本池进行抽样清理。app

redis设计者将该值默认为3,若是增长该值,会提升LRU或TTL的精准度,redis的做者测试的结果是当这个配置为10时已经很是接近全量LRU的精准度了,并且增长maxmemory采样会致使在主动清理时消耗更多的CPU时间,因此在设置该值必须慎重把控,在业务的需求以及性能之间作权衡。建议以下:dom

  • 尽可能不要触发maxmemory,最好在mem_used内存占用达到maxmemory的必定比例后,须要考虑调大赫兹以加快淘汰,或者进行集群扩容。
  • 若是可以控制住内存,则能够不用修改maxmemory-samples配置。若是redis自己就做为LRU缓存服务(这种服务通常长时间处于maxmemory状态,由redis自动作LRU淘汰),能够适当调大maxmemory样本。

freeMemoryIfNeeded源码解读

int freeMemoryIfNeeded(void) {
    size_t mem_used, mem_tofree, mem_freed;
    int slaves = listLength(server.slaves);

    // 计算占用内存大小时,并不计算slave output buffer和aof buffer,
	// 所以maxmemory应该比实际内存小,为这两个buffer留足空间。
    mem_used = zmalloc_used_memory();
    if (slaves) {
        listIter li;
        listNode *ln;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = listNodeValue(ln);
            unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
            if (obuf_bytes > mem_used)
                mem_used = 0;
            else
                mem_used -= obuf_bytes;
        }
    }
    if (server.appendonly) {
        mem_used -= sdslen(server.aofbuf);
        mem_used -= sdslen(server.bgrewritebuf);
    }
	// 判断已经使用内存是否超过最大使用内存,若是没有超过就返回REDIS_OK,
    if (mem_used <= server.maxmemory) return REDIS_OK;
	// 当超过了最大使用内存时,就要判断此时redis到底采用何种内存释放策略,根据不一样的策略,采起不一样的清除算法。
	// 首先判断是不是为no-enviction策略,若是是,则返回REDIS_ERR,而后redis就再也不接受任何写命令了。
    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; 
    // 计算须要清理内存大小
    mem_tofree = mem_used - server.maxmemory;
    mem_freed = 0;
   
    while (mem_freed < mem_tofree) {
        int j, k, keys_freed = 0;

        for (j = 0; j < server.dbnum; j++) {
            long bestval = 0;
            sds bestkey = NULL;
            struct dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;
			
			// 一、从哪一个字典中剔除数据
            // 判断淘汰策略是基于全部的键仍是只是基于设置了过时时间的键,
			// 若是是针对全部的键,就从server.db[j].dict中取数据,
			// 若是是针对设置了过时时间的键,就从server.db[j].expires(记录过时时间)中取数据。
			if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
            {
                dict = server.db[j].dict;
            } else {
                dict = server.db[j].expires;
            }
            if (dictSize(dict) == 0) continue;
			
			// 二、从是否为随机策略
			// 是否是random策略,包括volatile-random 和allkeys-random,这两种策略是最简单的,就是在上面的数据集中随便去一个键,而后删掉。
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);// 从方法名猜出是随机获取一个dictEntry
                bestkey = dictGetEntryKey(de);// 获得删除的key
            }
			
			// 三、判断是否为lru算法
			// 是lru策略仍是ttl策略,若是是lru策略就采用lru近似算法
			// 为了减小运算量,redis的lru算法和expire淘汰算法同样,都是非最优解,
			// lru算法是在相应的dict中,选择maxmemory_samples(默认设置是3)份key,挑选其中lru的,进行淘汰
            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 = dictGetEntryKey(de);
                    /* When policy is volatile-lru we need an additonal lookup
                     * to locate the real key, as dict is set to db->expires. */
                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                        de = dictFind(db->dict, thiskey); //由于dict->expires维护的数据结构里并无记录该key的最后访问时间
                    o = dictGetEntryVal(de);
                    thisval = estimateObjectIdleTime(o);

                    /* Higher idle time is better candidate for deletion */
					// 找到那个最合适删除的key
					// 相似排序,循环后找到最近最少使用,将其删除
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }
			// 若是是ttl策略。
			// 取maxmemory_samples个键,比较过时时间,
			// 从这些键中找到最快过时的那个键,并将其删除
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetEntryKey(de);
                    thisval = (long) dictGetEntryVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }
			// 根据不一样策略挑选了即将删除的key以后,进行删除
            if (bestkey) {
                long long delta;

                robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                // 发布数据更新消息,主要是AOF 持久化和从机
                propagateExpire(db,keyobj); //将del命令扩散给slaves

				// 注意, propagateExpire() 可能会致使内存的分配,
				// propagateExpire() 提早执行就是由于redis 只计算
				// dbDelete() 释放的内存大小。假若同时计算dbDelete()
				// 释放的内存和propagateExpire() 分配空间的大小,与此
				// 同时假设分配空间大于释放空间,就有可能永远退不出这个循环。
				// 下面的代码会同时计算dbDelete() 释放的内存和propagateExpire() 分配空间的大小
                /* We compute the amount of memory freed by dbDelete() alone.
                 * It is possible that actually the memory needed to propagate
                 * the DEL in AOF and replication link is greater than the one
                 * we are freeing removing the key, but we can't account for
                 * that otherwise we would never exit the loop.
                 *
                 * AOF and Output buffer memory will be freed eventually so
                 * we only care about memory used by the key space. */
				// 只计算dbDelete() 释放内存的大小
                delta = (long long) zmalloc_used_memory();
                dbDelete(db,keyobj);
                delta -= (long long) zmalloc_used_memory();
                mem_freed += delta;
                server.stat_evictedkeys++;
                decrRefCount(keyobj);
                keys_freed++;

                /* When the memory to free starts to be big enough, we may
                 * start spending so much time here that is impossible to
                 * deliver data to the slaves fast enough, so we force the
                 * transmission here inside the loop. */
                 // 将从机回复空间中的数据及时发送给从机
                if (slaves) flushSlavesOutputBuffers();
            }
        }//在全部的db中遍历一遍,而后判断删除的key释放的空间是否足够,未能释放空间,且此时redis 使用的内存大小依旧超额,失败返回
        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }
    return REDIS_OK;
}

从源码分析中能够看到redis在使用内存中超过设定的阈值时是如何将清理key-value进行内管管理,其中涉及到redis的存储结构。开篇就说到redis底层数据结构是由dict以及expires两个字典构成,经过一张图能够很是清晰了解到redis中带过时时间的key-value的存储结构,能够更加深入认识到redis的内存管理机制。ide

输入图片说明

从redis的内存管理机制中咱们能够看到,当使用的内存超过设定的阈值,就会触发内存清理。那么必定要等到内存超过阈值才进行内存清理吗?非要亡羊补牢?redis的设计者显然是考虑到了这个问题,当redis在使用过程当中,自行去删除一些过时key,尽可能保证不要触发超过内存阈值而发生的清理事件。函数

有效时间

  • expire/pexpire key time(以秒/毫秒为单位)--这是最经常使用的方式(Time To Live 称为TTL)
  • setex(String key, int seconds, String value)--字符串独有的方式

在使用过时时间时,必要注意以下三点:

  • 除了字符串本身独有设置过时时间的方法外,其余方法都须要依靠expire方法来设置时间
  • 若是没有设置时间,那缓存就是永不过时
  • 若是设置了过时时间,以后又想让缓存永不过时,使用persist key

过时键自动删除策略

一、定时删除(主动删除策略):经过使用定时器(时间事件,采用无序链表实现,),定时删除数据。定时删除策略能够保证过时的键会尽量快的被删除了,并释放过时键锁占用的内存。

  • 好处:对内存是最友好的。
  • 坏处:它对CPU时间不友好,在过时键比较多的状况下,删除过时键这一行为会占用至关一部分CPU时间,在内存不紧张可是CPU很是紧张的状况下,将CPU应用于删除和当前任务无关的过时键上,无疑会对服务器的响应时间和吞吐量形成影响。

二、惰性删除(被动删除策略):程序在每次使用到键的时候去检查是否过时,若是过时则删除并返回空。

  • 好处:对CPU时间友好,永远只在操做与当前任务有关的键。
  • 坏处:可能会在内存中遗留大量的过时键而不删除,形成内存泄漏。

三、按期删除(主动删除):按期每隔一段时间执行一段删除过时键操做,经过限制删除操做的执行时长与频率来减小删除操做对CPU时间的影响。除此以外,按期执行也能够减小过时键长期驻留内存的影响,减小内存泄漏的可能。

  • 好处:能够控制过时删除的执行频率
  • 坏处:服务器必须合理设置过时键删除的操做时间以及执行的频率。

redis的过时键删除策略

redis服务器实际上使用的惰性删除和按期删除两种策略:经过配合使用两种删除策略,服务器能够很好地合理使用CPU时间和避免浪费内存空间之间取得平衡。

惰性删除策略的实现

redis提供一个expireIfNeeded函数,因此读写数据库的命令在执行以前都必须调用expireIfNeeded函数。(键是否存在)

  • 若是过时 --> 删除
  • 若是非过时 --> 执行命令(expireIfNeeded函数不作动做)

按期删除策略的实现

按期删除有函数activeExpireCycle函数实现,每当redis服务器调用serverCorn函数时执行按期删除函数。它会在规定时间内,分屡次遍历服务器中的各个数据库,并在数据库的expire字典中随机检查一部分键的过时时间,并删除过时键。

遍历数据库(就是redis.conf中配置的"database"数量,默认为16)

  • 检查当前库中的指定个数个key(默认是每一个库检查20个key,注意至关于该循环执行20次)
  • 若是当前库中没有一个key设置了过时时间,直接执行下一个库的遍历
  • 随机获取一个设置了过时时间的key,检查该key是否过时,若是过时,删除key
  • 判判定期删除操做是否已经达到指定时长,若已经达到,直接退出按期删除。

参考资料: