redis的过时淘汰策略是很是值得去深刻了解以及考究的一个问题。不少使用者每每不能深得其意,每每停留在人云亦云的程度,若生产不出事故便划水就划过去了,可是当生产数据莫名其妙的消失,或者reids服务崩溃的时候,却又一筹莫展。本文尝试着从浅入深的将redis的过时策略剖析开来,指望帮助做者以及读者站在一个更加系统化的角度去看待过时策略。redis
redis做为缓存数据库,其底层数据结构主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过时时间。访问磁盘空间的成本是访问缓存的成本高出很是多,因此内存的成本比磁盘空间要大。在实际使用中,缓存的空间每每极为有限,因此为了在为数很少的容量中作到真正的物尽其用,必需要对缓存的容量进行管控。算法
redis经过配置
maxmemory
来配置最大容量(阈值),当数据占有空间超过所设定值就会触发内部的内存淘汰策略(内存释放)。那么究竟要淘汰哪些数据,才是最符合业务需求?或者在业务容忍的范围内呢?为了解决这个问题,redis提供了可配置的淘汰策略,让使用者能够配置适合本身业务场景的淘汰策略,不配置的状况下默认是使用volatile-lru
。数据库
若是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
样本。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,尽可能保证不要触发超过内存阈值而发生的清理事件。函数
在使用过时时间时,必要注意以下三点:
一、定时删除(主动删除策略):经过使用定时器(时间事件,采用无序链表实现,),定时删除数据。定时删除策略能够保证过时的键会尽量快的被删除了,并释放过时键锁占用的内存。
二、惰性删除(被动删除策略):程序在每次使用到键的时候去检查是否过时,若是过时则删除并返回空。
三、按期删除(主动删除):按期每隔一段时间执行一段删除过时键操做,经过限制删除操做的执行时长与频率来减小删除操做对CPU时间的影响。除此以外,按期执行也能够减小过时键长期驻留内存的影响,减小内存泄漏的可能。
redis服务器实际上使用的惰性删除和按期删除两种策略:经过配合使用两种删除策略,服务器能够很好地合理使用CPU时间和避免浪费内存空间之间取得平衡。
redis提供一个
expireIfNeeded
函数,因此读写数据库的命令在执行以前都必须调用expireIfNeeded函数。(键是否存在)
按期删除有函数
activeExpireCycle
函数实现,每当redis服务器调用serverCorn
函数时执行按期删除函数。它会在规定时间
内,分屡次遍历服务器中的各个数据库,并在数据库的expire字典中随机检查
一部分键的过时时间,并删除过时键。
遍历数据库(就是redis.conf中配置的"database"数量,默认为16)