redis5.0内存淘汰机制

  • Post author:
  • Post category:其他


每台redis的服务器的内存都是有限的,而且也不是所有的内存都用来存储信息。而且redis的实现并没有在内存这块做太多的优化,所以实现者为了防止内存过于饱和,采取了一些措施来管控内存。

文章结构:(1)内存策略;

(2)内存释放机制原理;

(3)项目中如何合理应用淘汰策略;

(4)单机版Redis内存优化注意点。



一、内存策略:

最大内存的设置是通过设置maxmemory来完成的,格式为maxmemory bytes ,当目前使用的内存超过了设置的最大内存,就要进行内存释放了, 当需要进行内存释放的时候,需要用某种策略对保存的的对象进行删除。Redis有六种策略(默认的策略是volatile-lru。)

redis中当内存超过限制时,按照配置的策略,淘汰掉相应的key-value,使得内存可以继续留有足够的空间保存新的数据。redis 确定驱逐某个键值对后,会删除这个数据并,并将这个数据变更消息发布到本地(AOF 持久化)和从机(主从连接)。



(1)volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。



(2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰



(3)volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰



(4)allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰



(5)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰



(6)no-enviction:禁止淘汰数据

除此之外还有一个配置项,就是maxmemory-samples,默认值是3,因为上面的策略代码实现的都是近似算法,所以不管是lru算法,还是ttl,都并不是在数据库中所有的数据为基础的算法,因为当数据库的数据很多的时候,这样效率太低,所以代码中都是基于maxmemory-samples个数据的近似算法。详情请读下文。




置换策略是如何工作的:

1)客户端执行一条新命令,导致数据库需要增加数据(比如set key value)

2)Redis会检查内存使用,如果内存使用超过maxmemory,就会按照置换策略删除一些key

3)新的命令执行成功

注意:

如果我们持续的写数据会导致内存达到或超出上限maxmemory,但是置换策略会将内存使用降低到上限以下。

如果一次需要使用很多的内存(比如一次写入一个很大的set),那么,Redis的内存使用可能超出最大内存限制一段时间。



二、内存释放机制原理:

(1)概述:





mem_used


内存已经超过


maxmemory


的设定,对于所有的读写请求,都会触发


redis.c/freeMemoryIfNeeded(void)


函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出足够的内存空间。所以如果在达到


maxmemory


并且调用方还在不断写入的情况下,可能会反复触发主动清理策略,导致请求会有一定的延迟。


清理时会根据用户配置的


maxmemory-policy


来做适当的清理(一般是


LRU





TTL


),这里的


LRU





TTL


策略并不是针对


redis


的所有


key


,而是以配置文件中的


maxmemory-samples





key


作为样本池进行抽样清理。





redis-5.0.6


中的默认配置为


5


,如果增加,会提高


LRU





TTL


的精准度,


redis


作者测试的结果是当这个配置为


10


时已经非常接近全量


LRU


的精准度了,并且增加


maxmemory-samples


会导致在主动清理时消耗更多的


CPU


时间,有如下建议:


1


)尽量不要触发


maxmemory


,最好在


mem_used


内存占用达到


maxmemory


的一定比例后,需要考虑调大


hz


以加快淘汰,或者进行集群扩容。


2


)如果能够控制住内存,则可以不用修改


maxmemory-samples


配置;如果


Redis


本身就作为


LRU cache


服务(这种服务一般长时间处于


maxmemory


状态,由


Redis


自动做


LRU


淘汰),可以适当调大


maxmemory-samples



(2)内存管理源码解析:


Redis


释放内存是由函数


freeMemoryIfNeeded


完成的,


redis





processCommand


函数处理每条命令,函数中在真正处理命令之前都会调用


freeMemoryIfNeeded


函数,这个函数会判断当前使用的内存是否超过了最大使用内存,如果超过,就会根据内存释放策略释放内存。


freeMemoryIfNeeded


函数首先会计算出当前使用了多少内存,注意,这里并不会包括


slaves


输出缓存以及


AOF


缓存,源码如下:


int freeMemoryIfNeeded(void) {


size_t mem_used, mem_tofree, mem_freed;


int slaves = listLength(server.slaves);


/* Remove the size of slaves output buffers and AOF buffer from the


* count of used memory.


*/


//


计算占用内存大小时,并不计算


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




/* Check if we are over the memory limit. */


if (mem_used <= server.maxmemory) return REDIS_OK;


//


当超过了最大使用内存时,就要判断此时


redis


到底采用的是那种内存释放策略,根据不同的策略,采取不同的手段。


//





1


)首先判断是否是为


no-enviction


策略,如果是,则返回


REDIS_ERR,


然后


redis


就不再接受任何写命令了。


if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)


return REDIS_ERR; /* We need to free memory, but policy forbids. */


/* Compute how much memory we need to free. */


mem_tofree = mem_used – server.maxmemory;


mem_freed = 0;


//





2


)接下来就判断淘汰策略是基于所有的键还是只是基于设置了过期时间的键,如果是针对所有的键,就从


server.db[j].dict


中取数据,如果是针对设置了过期时间的键,就从


server.db[j].expires


中取数据。


while (mem_freed < mem_tofree) {


int j, k, keys_freed = 0;


for (j = 0; j < server.dbnum; j++) {


long bestval = 0; /* just to prevent warning */


sds bestkey = NULL;


struct dictEntry *de;


redisDb *db = server.db+j;


dict *dict;


//





3


)然后判断是不是


random


策略,包括


volatile-random





allkeys-random


,这两种策略是最简单的,就是在上面的数据集中随便去一个键,然后删掉。


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;


//


接着又判断


allkeys-random


还是


volatile-ttl


策略


/* volatile-random and allkeys-random policy */


if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||


server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)


{


de = dictGetRandomKey(dict);


bestkey = dictGetEntryKey(de);


}//


如果是


random delete,


则从


dict


中随机选一个


key


//


然后就是判断是


lru


策略还是


ttl


策略,如果是


lru


策略就采用


lru


近似算法


/* 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 = 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 */


if (bestkey == NULL || thisval > bestval) {


bestkey = thiskey;


bestval = thisval;


}


}//


为了减少运算量


,redis





lru


算法和


expire


淘汰算法一样,都是非最优解,


lru


算法是在相应的


dict


中,选择


maxmemory_samples(


默认设置是


3)





key


,挑选其中


lru


的,进行淘汰


}


//


如果是


ttl


策略。


ttl


策略很简单,就是取


maxmemory_samples


个键,然后比较他们的过期时间,然后从这些键中找到最快过期的那个键,就是我们将要删除的键。


/* volatile-ttl */


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;


}


}//


注意


ttl


实现和上边一样,都是挑选出


maxmemory_samples


份进行挑选


}


//


根据不同的策略,我们找到了将要删除的键,下面就是将他们删除的时候了,删除选定的键值对


/* Finally remove the selected 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 mu


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


}


此函数是在执行特定命令之前进行调用的,并且在当前占用内存低于限制后即返回


OK


。因此可能在后续执行命令后,


redis


占用的内存就超过了


maxmemory


的限制。因此


,maxmemory





redis


执行命令所需保证的最大内存占用,而非


redis


实际的最大内存占用。(在不考虑


slave buffer





aof buffer


的前提下)。




TTL


数据淘汰机制




redis


数据集数据结构中保存了键值对过期时间的表,即


redisDb.expires




定义:


从过期时间的表中随机挑选几个键值对,取出其中


ttl


最大的键值对淘汰。同样你会发现,



redis


并不是保证取得所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对中的。


freeMemoryIfNeeded


函数关于


TTL


的源码:


//


挑选将要过期的数据


else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {


// server.maxmemory_samples


为随机挑选键值对次数


//


随机挑选


server.maxmemory_samples


个键值对,驱逐最快要过期的数据


for (k = 0; k < server.maxmemory_samples; k++) {


sds thiskey;


long thisval;


de = dictGetRandomKey(dict);


thiskey = dictGetKey(de);


thisval = (long) dictGetVal(de);


/* Expire sooner (minor expire unix timestamp) is better


* candidate for deletion */


if (bestkey == NULL || thisval < bestval) {


bestkey = thiskey;


bestval = thisval;


}


}


}


LRU


数据淘汰机制



在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来的。另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。可以想象的是,每一次访问数据的时候,会更新 redisObject.lru。

LRU 数据淘汰机制定义:


在数据集中随机挑选几个键值对,取出其中 lru 最小的键值对淘汰。所以,你会发现,redis


并不是保证取得所有数据集中最近最少使用(LRU)的键值对,而只是随机挑选的几个键值对中的键值对。

freeMemoryIfNeeded函数关于LRU的源码:


//


不同的策略,操作的数据集不同


if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||


server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)


{


dict = server.db[j].dict;


} else {//


操作的是设置了过期时间的


key




dict = server.db[j].expires;


}


if (dictSize(dict) == 0) continue;


/* volatile-random and allkeys-random policy */


//


随机选择进行淘汰


if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||


server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)


{


de = dictGetRandomKey(dict);


bestkey = dictGetKey(de);


}


/* volatile-lru and allkeys-lru policy */


//


具体的


LRU


算法


else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||


server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)


{


struct evictionPoolEntry *pool = db->eviction_pool;


while(bestkey == NULL) {


//


选择随机样式,并从样本中作用


LRU


算法选择需要淘汰的数据


evictionPoolPopulate(dict, db->dict, db->eviction_pool);


/* Go backward from best to worst element to evict. */


for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k–) {


if (pool[k].key == NULL) continue;


de = dictFind(dict,pool[k].key);


sdsfree(pool[k].key);


//





pool+k+1


之后的元素向前平移一个单位


memmove(pool+k,pool+k+1,


sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));


/* Clear the element on the right which is empty


* since we shifted one position to the left.  */


pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;


pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;


//


选择了需要淘汰的数据


if (de) {


bestkey = dictGetKey(de);


break;


} else {


/* Ghost… */


continue;


}


}


}


}

LRU算法实现在evictionPoolPopulate方法内.(有兴趣的朋友再进行相关查阅了解吧)






三、项目中如何合理应用淘汰策略:



(1)合理设置maxmemory:


修改方式有两种:


1.


通过


CONFIG SET


设定:


127.0.0.1:6379> CONFIG GET maxmemory


1) “maxmemory”


2) “0”


127.0.0.1:6379> CONFIG SET maxmemory 80MB


OK


127.0.0.1:6379> CONFIG GET maxmemory


1) “maxmemory”


2) “83886080”


2.


修改配置文件


redis.conf




maxmemory 80mb


注意:在


64bit


系统下,


maxmemory


设置为


0


表示不限制


Redis


内存使用,在


32bit


系统下,


maxmemory


隐式不能超过


3GB







Redis


内存使用达到指定的限制时,就需要选择一个置换的策略。



(2)置换策略的选择:





Redis


内存使用达到


maxmemory


时,就会使用设置好的


maxmemory-policy


进行对老数据的置换。


设置


maxmemory-policy


的方法和设置


maxmemory


方法类似,通过


redis.conf


或是通过


CONFIG SET


动态修改。


如果没有匹配到可以删除的


key


,那么


volatile-lru





volatile-random





volatile-ttl


策略和


noeviction


替换策略一样


——


不对任何


key


进行置换。


选择合适的置换策略是很重要的,这主要取决于你的应用的访问模式,当然你也可以动态的修改置换策略,并通过用


Redis


命令


——INFO


去输出


cache


的命中率情况,进而可以对置换策略进行调优。



针对一些策略所使用的场景:

1)allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。

在所有的key都是最近最经常使用,那么就需要选择allkeys-lru进行置换最近最不经常使用的key,如果你不确定使用哪种策略。

设置是失效时间expire会占用一些内存,而采用allkeys-lru就没有必要设置失效时间,进而更有效的利用内存

2)allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。

如果所有的key的访问概率都是差不多的,那么可以选用allkeys-random策略去置换数据。

3)volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。

如果对数据有足够的了解,能够为key指定hint(通过expire/ttl指定),那么可以选择volatile-ttl进行置换

4)volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存。






四、




Redis




内存优化注意点:



(1)Redis的编码:

概述:

很多数据类型都可以通过特殊编码的方式来进行存储空间的优化。其中,Hash、List和由Integer组成的Sets都可以通过该方式来优化存储结构,以便占用更少的空间,在有些情况下,可以省去9/10的空间。

这些特殊编码对于Redis的使用而言是完全透明的,事实上,它只是CPU和内存之间的一个交易而言。如果内存使用率方面高一些,那么在操作数据时消耗的CPU自然要多一些,反之亦然。在Redis中提供了一组配置参数用于设置与特殊编码相关的各种阈值。

阈值详细设置:


#


如果


Hash


中字段的数量小于该参数值,


Redis


将对该


Key





Hash Value


采用特殊编码。


hash-max-zipmap-entries 64


#


如果


Hash


中各个字段的最大长度不超过


512


字节,


Redis


也将对该


Key





Hash Value


采用特殊编码方式。


hash-max-zipmap-value 512


#


下面两个参数的含义基本等同于上面两个和


Hash


相关的参数,只是作用的对象类型为


List




list-max-ziplist-entries 512


list-max-ziplist-value 64


#


如果


set


中整型元素的数量不超过


512


时,


Redis


将会采用该特殊编码。


set-max-intset-entries 512


#


如果


zset


中的元素数量小于


128


或者各字段长度不超过


64





redis


会对


zset


采用特殊编码


zset-max-ziplist-entries 128


zset-max-ziplist-value 64

倘若某个已经被编码的值再经过修改之后超过了配置信息中的最大限制,那么Redis会自动将其转换为正常编码格式,这一操作是非常快速的,但是如果反过来操作,将一个正常编码的较大值转换为特殊编码,Redis的建议是,在正式做之前最好先简单测试一下转换效率,因为这样的转换往往是非常低效的。



(2)使用bit位级别操作和byte字节级别操作来减少不必要的内存使用:

bit位级别操作:GETRANGE, SETRANGE, GETBIT and SETBIT

byte字节级别操作:GETRANGE and SETRANGE

Redis提供了GETRANGE/SETRANGE/GETBIT/SETBIT四个用于字符串类型Key/Value的命令。通过这些命令,我们便可以像操作数组那样来访问String类型的值数据了。比如唯一标识用户身份的ID,可能仅仅是String值的其中一段子字符串。这样就可以通过GETRANGE/SETRANGE命令来方便的提取。再有就是可以使用BITMAP来表示用户的性别信息,如1表示male,0表示female。用这种方式来表示100,000,000个用户的性别信息时,也仅仅占用12MB的存储空间,与此同时,在通过SETBIT/GETBIT命令进行数据遍历也是非常高效的。



(3)尽可能地使用hashes哈希,因为小Hashes会被编码成一个非常小的空间。

由于小的Hash类型数据占用的空间相对较少,因此我们在实际应用时应该尽可能的考虑使用Hash类型,比如用户的注册信息,这其中包括姓名、性别、email、年龄和口令等字段。我们当然可以将这些信息以Key的形式进行存储,而用户填写的信息则以String Value的形式存储。然而Redis则更为推荐以Hash的形式存储,以上信息则以Field/Value的形式表示。

现在我们就通过学习Redis的存储机制来进一步证明这一说法。在该篇博客的开始处已经提到了特殊编码机制,其中有两个和Hash类型相关的配置参数:hash-max-zipmap-entries和hash-max-zipmap-value。至于它们的作用范围前面已经给出,这里就不再过多的赘述了。现在我们先假设存储在Hash Value中的字段数量小于hash-max-zipmap-entries,而每个元素的长度又同时小于hash-max-zipmap-value。这样每当有新的Hash类型的Key/Value存储时,Redis都会为Hash Value创建定长的空间,最大可预分配的字节数为:

total_bytes = hash




max


-zipmap-entries * hash-


max


-zipmap-value

1

2

这样一来,Hash中所有字段的位置已经预留,并且可以像访问数组那样随机的访问Field/Value,他们之间的步长间隔为hash-max-zipmap-value。只有当Hash Value中的字段数量或某一新元素的长度分别超过以上两个参数值时,Redis才会考虑将他们以Hash Table的方式进行重新存储,否则将始终保持这种高效的存储和访问方式。不仅如此,由于每个Key都要存储一些关联的系统信息,如过期时间、LRU等,因此和String类型的Key/Value相比,Hash类型极大的减少了Key的数量(大部分的Key都以Hash字段的形式表示并存储了),从而进一步优化了存储空间的使用效率。



(4)过期策略的合理设计:





(5)合理的内存分配:

如果maxmemory没有设置的Redis会继续分配内存,因为它认为合适的,因此它可以(逐渐)吃了你的全部可用内存。因此,通常建议配置一些限制。您可能还需要设置maxmemory策略,默认的是:noeviction(这不是在一些旧版本的Redis的默认值)。

这使得Redis的返回内存不足的错误写命令,如果当它到达了极限 – 这反过来可能会导致应用程序错误,但不会导致因为内存饥饿而整机死亡。



(6)在存到Redis之前先把你的数据压缩下。



(7)一些真正的实际Redis内存设计方案会在本系列的后面写出。比如:共享对象,【string】–>【hash】–>【segment-hash】的优化



版权声明:本文为zhanglf88原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。