首页 文章资讯内容详情

Redis中的LRU淘汰策略分析

2026-06-01 4 花语

本文内容纲要:

-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位的系统默认内存限制为3GB maxmemory_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个。

Redis3.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. *Dontdoitifwehaveasavingchild,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; /*Wedontwanttomakelocal-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,soweusethestaticnext_dbvariableto *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,butwecantaccountfor *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,itsbetterto *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){ /*Letssatisfyourstopcondition.*/ 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){ /*Cantinsertiftheelementis<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