Redis中的LRU淘汰策略分析
本文内容纲要:
-LRU算法
-LRU配置参数
-淘汰策略
-近似LRU算法
-LRU源码分析
-参考链接
Redis
作为缓存使用时,一些场景下要考虑内存的空间消耗问题。Redis
会删除过期键以释放空间,过期键的删除策略有两种:
- 惰性删除:每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。
另外,Redis
也可以开启LRU
功能来自动淘汰一些键值对。
LRU算法
当需要从缓存中淘汰数据时,我们希望能淘汰那些将来不可能再被使用的数据,保留那些将来还会频繁访问的数据,但最大的问题是缓存并不能预言未来。一个解决方法就是通过LRU
进行预测:最近被频繁访问的数据将来被访问的可能性也越大。缓存中的数据一般会有这样的访问分布:一部分数据拥有绝大部分的访问量。当访问模式很少改变时,可以记录每个数据的最后一次访问时间,拥有最少空闲时间的数据可以被认为将来最有可能被访问到。
举例如下的访问模式,A每5s访问一次,B每2s访问一次,C与D每10s访问一次,|
代表计算空闲时间的截止点:
~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
可以看到,LRU
对于A、B、C工作的很好,完美预测了将来被访问到的概率B>A>C,但对于D却预测了最少的空闲时间。
但是,总体来说,LRU
算法已经是一个性能足够好的算法了
LRU配置参数
Redis
配置中和LRU
有关的有三个:
maxmemory
:配置Redis
存储数据时指定限制的内存大小,比如100m
。当缓存消耗的内存超过这个数值时,将触发数据淘汰。该数据配置为0时,表示缓存的数据量没有限制,即LRU功能不生效。64位的系统默认值为0,32位的系统默认内存限制为3GBmaxmemory_policy
:触发数据淘汰后的淘汰策略maxmemory_samples
:随机采样的精度,也就是随即取出key的数目。该数值配置越大,越接近于真实的LRU算法,但是数值越大,相应消耗也变高,对性能有一定影响,样本值默认为5。
淘汰策略
淘汰策略即maxmemory_policy
的赋值有以下几种:
noeviction
:如果缓存数据超过了maxmemory
限定值,并且客户端正在执行的命令(大部分的写入指令,但DEL和几个指令例外)会导致内存分配,则向客户端返回错误响应allkeys-lru
:对所有的键都采取LRU
淘汰volatile-lru
:仅对设置了过期时间的键采取LRU
淘汰allkeys-random
:随机回收所有的键volatile-random
:随机回收设置过期时间的键volatile-ttl
:仅淘汰设置了过期时间的键---淘汰生存时间TTL(TimeToLive)
更小的键
volatile-lru
,volatile-random
和volatile-ttl
这三个淘汰策略使用的不是全量数据,有可能无法淘汰出足够的内存空间。在没有过期键或者没有设置超时属性的键的情况下,这三种策略和noeviction
差不多。
一般的经验规则:
- 使用
allkeys-lru
策略:当预期请求符合一个幂次分布(二八法则等),比如一部分的子集元素比其它其它元素被访问的更多时,可以选择这个策略。 - 使用
allkeys-random
:循环连续的访问所有的键时,或者预期请求分布平均(所有元素被访问的概率都差不多) - 使用
volatile-ttl
:要采取这个策略,缓存对象的TTL
值最好有差异
volatile-lru
和volatile-random
策略,当你想要使用单一的Redis
实例来同时实现缓存淘汰和持久化一些经常使用的键集合时很有用。未设置过期时间的键进行持久化保存,设置了过期时间的键参与缓存淘汰。不过一般运行两个实例是解决这个问题的更好方法。
为键设置过期时间也是需要消耗内存的,所以使用allkeys-lru
这种策略更加节省空间,因为这种策略下可以不为键设置过期时间。
近似LRU算法
我们知道,LRU
算法需要一个双向链表来记录数据的最近被访问顺序,但是出于节省内存的考虑,Redis
的LRU
算法并非完整的实现。Redis
并不会选择最久未被访问的键进行回收,相反它会尝试运行一个近似LRU
的算法,通过对少量键进行取样,然后回收其中的最久未被访问的键。通过调整每次回收时的采样数量maxmemory-samples
,可以实现调整算法的精度。
根据Redis
作者的说法,每个RedisObject
可以挤出24bits的空间,但24bits是不够存储两个指针的,而存储一个低位时间戳是足够的,RedisObject
以秒为单位存储了对象新建或者更新时的unixtime
,也就是LRUclock
,24bits数据要溢出的话需要194天,而缓存的数据更新非常频繁,已经足够了。
Redis
的键空间是放在一个哈希表中的,要从所有的键中选出一个最久未被访问的键,需要另外一个数据结构存储这些源信息,这显然不划算。最初,Redis
只是随机的选3个key,然后从中淘汰,后来算法改进到了N个key
的策略,默认是5个。
Redis
3.0之后又改善了算法的性能,会提供一个待淘汰候选key的pool
,里面默认有16个key,按照空闲时间排好序。更新时从Redis
键空间随机选择N个key,分别计算它们的空闲时间idle
,key只会在pool
不满或者空闲时间大于pool
里最小的时,才会进入pool
,然后从pool
中选择空闲时间最大的key淘汰掉。
真实LRU
算法与近似LRU
的算法可以通过下面的图像对比:
浅灰色带是已经被淘汰的对象,灰色带是没有被淘汰的对象,绿色带是新添加的对象。可以看出,maxmemory-samples
值为5时Redis3.0
效果比Redis2.8
要好。使用10个采样大小的Redis3.0
的近似LRU
算法已经非常接近理论的性能了。
数据访问模式非常接近幂次分布时,也就是大部分的访问集中于部分键时,LRU
近似算法会处理得很好。
在模拟实验的过程中,我们发现如果使用幂次分布的访问模式,真实LRU
算法和近似LRU
算法几乎没有差别。
LRU源码分析
Redis
中的键与值都是redisObject
对象:
typedefstructredisObject{
unsignedtype:4;
unsignedencoding:4;
unsignedlru:LRU_BITS;/*LRUtime(relativetogloballru_clock)or
*LFUdata(leastsignificant8bitsfrequency
*andmostsignificant16bitsaccesstime).*/
intrefcount;
void*ptr;
}robj;
unsigned
的低24bits的lru
记录了redisObj
的LRUtime。
Redis命令访问缓存的数据时,均会调用函数lookupKey
:
robj*lookupKey(redisDb*db,robj*key,intflags){
dictEntry*de=dictFind(db->dict,key->ptr);
if(de){
robj*val=dictGetVal(de);
/*Updatetheaccesstimefortheageingalgorithm.
*Don'tdoitifwehaveasavingchild,asthiswilltrigger
*acopyonwritemadness.*/
if(server.rdb_child_pid==-1&&
server.aof_child_pid==-1&&
!(flags&LOOKUP_NOTOUCH))
{
if(server.maxmemory_policy&MAXMEMORY_FLAG_LFU){
updateLFU(val);
}else{
val->lru=LRU_CLOCK();
}
}
returnval;
}else{
returnNULL;
}
}
该函数在策略为LRU(非LFU)
时会更新对象的lru
值,设置为LRU_CLOCK()
值:
/*ReturntheLRUclock,basedontheclockresolution.Thisisatime
*inareduced-bitsformatthatcanbeusedtosetandcheckthe
*object->lrufieldofredisObjectstructures.*/
unsignedintgetLRUClock(void){
return(mstime()/LRU_CLOCK_RESOLUTION)&LRU_CLOCK_MAX;
}
/*ThisfunctionisusedtoobtainthecurrentLRUclock.
*Ifthecurrentresolutionislowerthanthefrequencywerefreshthe
*LRUclock(asitshouldbeinproductionservers)wereturnthe
*precomputedvalue,otherwiseweneedtoresorttoasystemcall.*/
unsignedintLRU_CLOCK(void){
unsignedintlruclock;
if(1000/server.hz<=LRU_CLOCK_RESOLUTION){
atomicGet(server.lruclock,lruclock);
}else{
lruclock=getLRUClock();
}
returnlruclock;
}
LRU_CLOCK()
取决于LRU_CLOCK_RESOLUTION(默认值1000)
,LRU_CLOCK_RESOLUTION
代表了LRU
算法的精度,即一个LRU
的单位是多长。server.hz
代表服务器刷新的频率,如果服务器的时间更新精度值比LRU
的精度值要小,LRU_CLOCK()
直接使用服务器的时间,减小开销。
Redis
处理命令的入口是processCommand
:
intprocessCommand(client*c){
/*Handlethemaxmemorydirective.
*
*Notethatwedonotwanttoreclaimmemoryifweareherere-entering
*theeventloopsincethereisabusyLuascriptrunningintimeout
*condition,toavoidmixingthepropagationofscriptswiththe
*propagationofDELsduetoeviction.*/
if(server.maxmemory&&!server.lua_timedout){
intout_of_memory=freeMemoryIfNeededAndSafe()==C_ERR;
/*freeMemoryIfNeededmayflushslaveoutputbuffers.Thismayresult
*intoaslave,thatmaybetheactiveclient,tobefreed.*/
if(server.current_client==NULL)returnC_ERR;
/*Itwasimpossibletofreeenoughmemory,andthecommandtheclient
*istryingtoexecuteisdeniedduringOOMconditionsortheclient
*isinMULTI/EXECcontext?Error.*/
if(out_of_memory&&
(c->cmd->flags&CMD_DENYOOM||
(c->flags&CLIENT_MULTI&&c->cmd->proc!=execCommand))){
flagTransaction(c);
addReply(c,shared.oomerr);
returnC_OK;
}
}
}
只列出了释放内存空间的部分,freeMemoryIfNeededAndSafe
为释放内存的函数:
intfreeMemoryIfNeeded(void){
/*Bydefaultreplicasshouldignoremaxmemory
*andjustbemastersexactcopies.*/
if(server.masterhost&&server.repl_slave_ignore_maxmemory)returnC_OK;
size_tmem_reported,mem_tofree,mem_freed;
mstime_tlatency,eviction_latency;
longlongdelta;
intslaves=listLength(server.slaves);
/*Whenclientsarepausedthedatasetshouldbestaticnotjustfromthe
*POVofclientsnotbeingabletowrite,butalsofromthePOVof
*expiresandevictionsofkeysnotbeingperformed.*/
if(clientsArePaused())returnC_OK;
if(getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL)==C_OK)
returnC_OK;
mem_freed=0;
if(server.maxmemory_policy==MAXMEMORY_NO_EVICTION)
gotocant_free;/*Weneedtofreememory,butpolicyforbids.*/
latencyStartMonitor(latency);
while(mem_freed<mem_tofree){
intj,k,i,keys_freed=0;
staticunsignedintnext_db=0;
sdsbestkey=NULL;
intbestdbid;
redisDb*db;
dict*dict;
dictEntry*de;
if(server.maxmemory_policy&(MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU)||
server.maxmemory_policy==MAXMEMORY_VOLATILE_TTL)
{
structevictionPoolEntry*pool=EvictionPoolLRU;
while(bestkey==NULL){
unsignedlongtotal_keys=0,keys;
/*Wedon'twanttomakelocal-dbchoiceswhenexpiringkeys,
*sotostartpopulatetheevictionpoolsamplingkeysfrom
*everyDB.*/
for(i=0;i<server.dbnum;i++){
db=server.db+i;
dict=(server.maxmemory_policy&MAXMEMORY_FLAG_ALLKEYS)?
db->dict:db->expires;
if((keys=dictSize(dict))!=0){
evictionPoolPopulate(i,dict,db->dict,pool);
total_keys+=keys;
}
}
if(!total_keys)break;/*Nokeystoevict.*/
/*Gobackwardfrombesttoworstelementtoevict.*/
for(k=EVPOOL_SIZE-1;k>=0;k--){
if(pool[k].key==NULL)continue;
bestdbid=pool[k].dbid;
if(server.maxmemory_policy&MAXMEMORY_FLAG_ALLKEYS){
de=dictFind(server.db[pool[k].dbid].dict,
pool[k].key);
}else{
de=dictFind(server.db[pool[k].dbid].expires,
pool[k].key);
}
/*Removetheentryfromthepool.*/
if(pool[k].key!=pool[k].cached)
sdsfree(pool[k].key);
pool[k].key=NULL;
pool[k].idle=0;
/*Ifthekeyexists,isourpick.Otherwiseitis
*aghostandweneedtotrythenextelement.*/
if(de){
bestkey=dictGetKey(de);
break;
}else{
/*Ghost...Iterateagain.*/
}
}
}
}
/*volatile-randomandallkeys-randompolicy*/
elseif(server.maxmemory_policy==MAXMEMORY_ALLKEYS_RANDOM||
server.maxmemory_policy==MAXMEMORY_VOLATILE_RANDOM)
{
/*Whenevictingarandomkey,wetrytoevictakeyfor
*eachDB,soweusethestatic'next_db'variableto
*incrementallyvisitallDBs.*/
for(i=0;i<server.dbnum;i++){
j=(++next_db)%server.dbnum;
db=server.db+j;
dict=(server.maxmemory_policy==MAXMEMORY_ALLKEYS_RANDOM)?
db->dict:db->expires;
if(dictSize(dict)!=0){
de=dictGetRandomKey(dict);
bestkey=dictGetKey(de);
bestdbid=j;
break;
}
}
}
/*Finallyremovetheselectedkey.*/
if(bestkey){
db=server.db+bestdbid;
robj*keyobj=createStringObject(bestkey,sdslen(bestkey));
propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
/*Wecomputetheamountofmemoryfreedbydb*Delete()alone.
*Itispossiblethatactuallythememoryneededtopropagate
*theDELinAOFandreplicationlinkisgreaterthantheone
*wearefreeingremovingthekey,butwecan'taccountfor
*thatotherwisewewouldneverexittheloop.
*
*AOFandOutputbuffermemorywillbefreedeventuallyso
*weonlycareaboutmemoryusedbythekeyspace.*/
delta=(longlong)zmalloc_used_memory();
latencyStartMonitor(eviction_latency);
if(server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
latencyEndMonitor(eviction_latency);
latencyAddSampleIfNeeded("eviction-del",eviction_latency);
latencyRemoveNestedEvent(latency,eviction_latency);
delta-=(longlong)zmalloc_used_memory();
mem_freed+=delta;
server.stat_evictedkeys++;
notifyKeyspaceEvent(NOTIFY_EVICTED,"evicted",
keyobj,db->id);
decrRefCount(keyobj);
keys_freed++;
/*Whenthememorytofreestartstobebigenough,wemay
*startspendingsomuchtimeherethatisimpossibleto
*deliverdatatotheslavesfastenough,soweforcethe
*transmissionhereinsidetheloop.*/
if(slaves)flushSlavesOutputBuffers();
/*Normallyourstopconditionistheabilitytorelease
*afixed,pre-computedamountofmemory.Howeverwhenwe
*aredeletingobjectsinanotherthread,it'sbetterto
*check,fromtimetotime,ifwealreadyreachedourtarget
*memory,sincethe"mem_freed"amountiscomputedonly
*acrossthedbAsyncDelete()call,whilethethreadcan
*releasethememoryallthetime.*/
if(server.lazyfree_lazy_eviction&&!(keys_freed%16)){
if(getMaxmemoryState(NULL,NULL,NULL,NULL)==C_OK){
/*Let'ssatisfyourstopcondition.*/
mem_freed=mem_tofree;
}
}
}
if(!keys_freed){
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
gotocant_free;/*nothingtofree...*/
}
}
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
returnC_OK;
cant_free:
/*Wearehereifwearenotabletoreclaimmemory.Thereisonlyone
*lastthingwecantry:checkifthelazyfreethreadhasjobsinqueue
*andwait...*/
while(bioPendingJobsOfType(BIO_LAZY_FREE)){
if(((mem_reported-zmalloc_used_memory())+mem_freed)>=mem_tofree)
break;
usleep(1000);
}
returnC_ERR;
}
/*ThisisawrapperforfreeMemoryIfNeeded()thatonlyreallycallsthe
*functionifrightnowtherearetheconditionstodososafely:
*
*-Theremustbenoscriptintimeoutcondition.
*-Norweareloadingdatarightnow.
*
*/
intfreeMemoryIfNeededAndSafe(void){
if(server.lua_timedout||server.loading)returnC_OK;
returnfreeMemoryIfNeeded();
}
几种淘汰策略maxmemory_policy
就是在这个函数里面实现的。
当采用LRU
时,可以看到,从0号数据库开始(默认16个),根据不同的策略,选择redisDb
的dict(全部键)
或者expires(有过期时间的键)
,用来更新候选键池子pool
,pool
更新策略是evictionPoolPopulate
:
voidevictionPoolPopulate(intdbid,dict*sampledict,dict*keydict,structevictionPoolEntry*pool){
intj,k,count;
dictEntry*samples[server.maxmemory_samples];
count=dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
for(j=0;j<count;j++){
unsignedlonglongidle;
sdskey;
robj*o;
dictEntry*de;
de=samples[j];
key=dictGetKey(de);
/*Ifthedictionarywearesamplingfromisnotthemain
*dictionary(buttheexpiresone)weneedtolookupthekey
*againinthekeydictionarytoobtainthevalueobject.*/
if(server.maxmemory_policy!=MAXMEMORY_VOLATILE_TTL){
if(sampledict!=keydict)de=dictFind(keydict,key);
o=dictGetVal(de);
}
/*Calculatetheidletimeaccordingtothepolicy.Thisiscalled
*idlejustbecausethecodeinitiallyhandledLRU,butisinfact
*justascorewhereanhigherscoremeansbettercandidate.*/
if(server.maxmemory_policy&MAXMEMORY_FLAG_LRU){
idle=estimateObjectIdleTime(o);
}elseif(server.maxmemory_policy&MAXMEMORY_FLAG_LFU){
/*WhenweuseanLRUpolicy,wesortthekeysbyidletime
*sothatweexpirekeysstartingfromgreateridletime.
*HoweverwhenthepolicyisanLFUone,wehaveafrequency
*estimation,andwewanttoevictkeyswithlowerfrequency
*first.Soinsidethepoolweputobjectsusingtheinverted
*frequencysubtractingtheactualfrequencytothemaximum
*frequencyof255.*/
idle=255-LFUDecrAndReturn(o);
}elseif(server.maxmemory_policy==MAXMEMORY_VOLATILE_TTL){
/*Inthiscasethesoonertheexpirethebetter.*/
idle=ULLONG_MAX-(long)dictGetVal(de);
}else{
serverPanic("UnknownevictionpolicyinevictionPoolPopulate()");
}
/*Inserttheelementinsidethepool.
*First,findthefirstemptybucketorthefirstpopulated
*bucketthathasanidletimesmallerthanouridletime.*/
k=0;
while(k<EVPOOL_SIZE&&
pool[k].key&&
pool[k].idle<idle)k++;
if(k==0&&pool[EVPOOL_SIZE-1].key!=NULL){
/*Can'tinsertiftheelementis<theworstelementwehave
*andtherearenoemptybuckets.*/
continue;
}elseif(k<EVPOOL_SIZE&&pool[k].key==NULL){
/*Insertingintoemptyposition.Nosetupneededbeforeinsert.*/
}else{
/*Insertinginthemiddle.Nowkpointstothefirstelement
*greaterthantheelementtoinsert.*/
if(pool[EVPOOL_SIZE-1].key==NULL){
/*Freespaceontheright?Insertatkshifting
*alltheelementsfromktoendtotheright.*/
/*SaveSDSbeforeoverwriting.*/
sdscached=pool[EVPOOL_SIZE-1].cached;
memmove(pool+k+1,pool+k,
sizeof(pool[0])*(EVPOOL_SIZE-k-1));
pool[k].cached=cached;
}else{
/*Nofreespaceonright?Insertatk-1*/
k--;
/*Shiftallelementsontheleftofk(included)tothe
*left,sowediscardtheelementwithsmalleridletime.*/
sdscached=pool[0].cached;/*SaveSDSbeforeoverwriting.*/
if(pool[0].key!=pool[0].cached)sdsfree(pool[0].key);
memmove(pool,pool+1,sizeof(pool[0])*k);
pool[k].cached=cached;
}
}
/*TrytoreusethecachedSDSstringallocatedinthepoolentry,
*becauseallocatinganddeallocatingthisobjectiscostly
*(accordingtotheprofiler,notmyfantasy.Remember:
*prematureoptimizblablablabla.*/
intklen=sdslen(key);
if(klen>EVPOOL_CACHED_SDS_SIZE){
pool[k].key=sdsdup(key);
}else{
memcpy(pool[k].cached,key,klen+1);
sdssetlen(pool[k].cached,klen);
pool[k].key=pool[k].cached;
}
pool[k].idle=idle;
pool[k].dbid=dbid;
}
}
Redis
随机选择maxmemory_samples
数量的key,然后计算这些key的空闲时间idletime
,当满足条件时(比pool中的某些键的空闲时间还大)就可以进pool。pool更新之后,就淘汰pool中空闲时间最大的键。
estimateObjectIdleTime
用来计算Redis
对象的空闲时间:
/*Givenanobjectreturnstheminnumberofmillisecondstheobjectwasnever
*requested,usinganapproximatedLRUalgorithm.*/
unsignedlonglongestimateObjectIdleTime(robj*o){
unsignedlonglonglruclock=LRU_CLOCK();
if(lruclock>=o->lru){
return(lruclock-o->lru)*LRU_CLOCK_RESOLUTION;
}else{
return(lruclock+(LRU_CLOCK_MAX-o->lru))*
LRU_CLOCK_RESOLUTION;
}
}
空闲时间基本就是就是对象的lru
和全局的LRU_CLOCK()
的差值乘以精度LRU_CLOCK_RESOLUTION
,将秒转化为了毫秒。
参考链接
- RandomnotesonimprovingtheRedisLRUalgorithm
- UsingRedisasanLRUcache
本文内容总结:LRU算法,LRU配置参数,淘汰策略,近似LRU算法,LRU源码分析,参考链接,
原文链接:https://www.cnblogs.com/linxiyue/p/10945216.html