关于Redis数据过期策略
1、Redis中key的的过期时间
通过EXPIREkeyconds命令来设置数据的过期时间。返回1表明设置成功,返回0表明key不存在或者不能成功设置过期时间。在key上设
置了过期时间后key将在指定的秒数后被⾃动删除。被指定了过期时间的key在Redis中被称为是不稳定的。
当key被DEL命令删除或者被SET、GETSET命令重置后与之关联的过期时间会被清除
127.0.0.1:6379>texs201
OK
127.0.0.1:6379>ttls
(integer)17
127.0.0.1:6379>texs2001
OK
127.0.0.1:6379>ttls
(integer)195
127.0.0.1:6379>tranges3100
(integer)6
127.0.0.1:6379>ttls
(integer)152
127.0.0.1:6379>gets
"1x00x00100"
127.0.0.1:6379>ttls
(integer)108
127.0.0.1:6379>getts200
"1x00x00100"
127.0.0.1:6379>gets
"200"
127.0.0.1:6379>ttls
(integer)-1
使⽤PERSIST可以清除过期时间
127.0.0.1:6379>texs100test
OK
127.0.0.1:6379>gets
"test"
127.0.0.1:6379>ttls
(integer)94
127.0.0.1:6379>types
string
127.0.0.1:6379>strlens
(integer)4
127.0.0.1:6379>persists
(integer)1
127.0.0.1:6379>ttls
(integer)-1
127.0.0.1:6379>gets
"test"
使⽤rename只是改了key值
127.0.0.1:6379>expires200
(integer)1
127.0.0.1:6379>ttls
(integer)198
127.0.0.1:6379>renamesss
OK
127.0.0.1:6379>ttlss
(integer)187
127.0.0.1:6379>typess
string
127.0.0.1:6379>getss
"test"
说明:Redis2.6以后expire精度可以控制在0到1毫秒内,key的过期信息以绝对Unix时间戳的形式存储(Redis2.6之后以毫秒级别的精度存
储),所以在多服务器同步的时候,⼀定要同步各个服务器的时间
2、Redis过期键删除策略
Rediskey过期的⽅式有三种:
被动删除:当读/写⼀个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
主动删除:由于惰性删除策略⽆法保证冷数据被及时删掉,所以Redis会定期主动淘汰⼀批已过期的key
当前已⽤内存超过maxmemory限定时,触发主动清理策略
被动删除
只有key被操作时(如GET),REDIS才会被动检查该key是否过期,如果过期则删除之并且返回NIL。
1、这种删除策略对CPU是友好的,删除操作只有在不得不的情况下才会进⾏,不会其他的expirekey上浪费⽆谓的CPU时间。
2、但是这种策略对内存不友好,⼀个key已经过期,但是在它被操作之前不会被删除,仍然占据内存空间。如果有⼤量的过期键存在但是⼜
很少被访问到,那会造成⼤量的内存空间浪费。expireIfNeeded(redisDb*db,robj*key)函数位于src/db.c。
/*-----------------------------------------------------------------------------
*ExpiresAPI
*----------------------------------------------------------------------------*/
intremoveExpire(redisDb*db,robj*key){
/*Anexpiremayonlyberemovedifthereisacorrespondingentryinthe
*i,thekeywillneverbefreed.*/
redisAsrtWithInfo(NULL,key,dictFind(db->dict,key->ptr)!=NULL);
returndictDelete(db->expires,key->ptr)==DICT_OK;
}
voidtExpire(redisDb*db,robj*key,longlongwhen){
dictEntry*kde,*de;
/*Reuthesdsfromthemaindictintheexpiredict*/
kde=dictFind(db->dict,key->ptr);
redisAsrtWithInfo(NULL,key,kde!=NULL);
de=dictReplaceRaw(db->expires,dictGetKey(kde));
dictSetSignedIntegerVal(de,when);
}
/*Returntheexpiretimeofthespecifiedkey,or-1ifnoexpire
*isassociatedwiththiskey(isnonvolatile)*/
longlonggetExpire(redisDb*db,robj*key){
dictEntry*de;
/*Noexpire?returnASAP*/
if(dictSize(db->expires)==0||
(de=dictFind(db->expires,key->ptr))==NULL)return-1;
/*Theentrywasfoundintheexpiredict,thismeansitshouldalso
*beprentinthemaindict(safetycheck).*/
redisAsrtWithInfo(NULL,key,dictFind(db->dict,key->ptr)!=NULL);
returndictGetSignedIntegerVal(de);
}
/*PropagateexpiresintoslavesandtheAOFfile.
*Whenakeyexpiresinthemaster,aDELoperationforthiskeyisnt
*toalltheslavesandtheAOFfileifenabled.
*
*Thiswaythekeyexpiryiscentralizedinoneplace,andsinceboth
*AOFandthemaster->slavelinkguaranteeoperationordering,everything
*willbeconsistentevenifweallowwriteoperationsagainstexpiring
*keys.*/
voidpropagateExpire(redisDb*db,robj*key){
robj*argv[2];
argv[0]=;
argv[1]=key;
incrRefCount(argv[0]);
incrRefCount(argv[1]);
if(_state!=REDIS_AOF_OFF)
feedAppendOnlyFile(mand,db->id,argv,2);
replicationFeedSlaves(,db->id,argv,2);
decrRefCount(argv[0]);
decrRefCount(argv[1]);
}
intexpireIfNeeded(redisDb*db,robj*key){
mstime_twhen=getExpire(db,key);
mstime_tnow;
if(when<0)return0;/*Noexpireforthiskey*/
/*Don'bedonelater.*/
if(g)return0;
/*IfweareinthecontextofaLuascript,weclaimthattimeis
*yakeycanexpire
*onlythefirsttimeitisaccesdandnotinthemiddleofthe
*scriptexecution,makingpropagationtoslaves/AOFconsistent.
*Seeissue#1525onGithubformoreinformation.*/
now=_caller?_time_start:mstime();
/*Ifwearerunninginthecontextofaslave,returnASAP:
*theslavekeyexpirationiscontrolledbythemasterthatwill
*ndussynthesizedDELoperationsforexpiredkeys.
*
*Stillwetrytoreturntherightinformationtothecaller,
*thatis,0ifwethinkthekeyshouldbestillvalid,1if
*wethinkthekeyixpiredatthistime.*/
if(host!=NULL)returnnow>when;
/*Returnwhenthiskeyhasnotexpired*/
if(now<=when)return0;
/*Deletethekey*/
_expiredkeys++;
propagateExpire(db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
returndbDelete(db,key);
}
/*-----------------------------------------------------------------------------
*ExpiresCommands
*----------------------------------------------------------------------------*/
/*ThisisthegenericcommandimplementationforEXPIRE,PEXPIRE,EXPIREAT
*ethecommadcondargumentmayberelativeorabsolute
*the"batime"argumentisudtosignalwhatthebatimeis(either0
*for*ATvariantsofthecommand,orthecurrenttimeforrelativeexpires).
*
*unitiitherUNIT_SECONDSorUNIT_MILLISECONDS,andisonlyudfor
*theargv[2]etimeisalwaysspecifiedinmilliconds.*/
voidexpireGenericCommand(redisClient*c,longlongbatime,intunit){
robj*key=c->argv[1],*param=c->argv[2];
longlongwhen;/*unixtimeinmillicondswhenthekeywillexpire.*/
if(getLongLongFromObjectOrReply(c,param,&when,NULL)!=REDIS_OK)
return;
if(unit==UNIT_SECONDS)when*=1000;
when+=batime;
/*Nokey,returnzero.*/
if(lookupKeyRead(c->db,key)==NULL){
addReply(c,);
return;
}
/*EXPIREwithnegativeTTL,orEXPIREATwithatimestampintothepast
*shouldneverbeexecutedasaDELwhenloadtheAOForinthecontext
*ofaslaveinstance.
*
*InsteadwetaketheotherbranchoftheIFstatementttinganexpire
*(possiblyinthepast)andwaitforanexplicitDELfromthemaster.*/
if(when<=mstime()&&!g&&!host){
robj*aux;
redisAsrtWithInfo(c,key,dbDelete(c->db,key));
++;
/*Replicate/AOFthisasanexplicitDEL.*/
aux=createStringObject("DEL",3);
rewriteClientCommandVector(c,2,aux,key);
decrRefCount(aux);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c,);
return;
}el{
tExpire(c->db,key,when);
addReply(c,);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
++;
return;
}
}
voidexpireCommand(redisClient*c){
expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
voidexpireatCommand(redisClient*c){
expireGenericCommand(c,0,UNIT_SECONDS);
}
voidpexpireCommand(redisClient*c){
expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
voidpexpireatCommand(redisClient*c){
expireGenericCommand(c,0,UNIT_MILLISECONDS);
}
voidttlGenericCommand(redisClient*c,intoutput_ms){
longlongexpire,ttl=-1;
/*Ifthekeydoesnotexistatall,return-2*/
if(lookupKeyRead(c->db,c->argv[1])==NULL){
addReplyLongLong(c,-2);
return;
}
/*-1ifithasnoexpire,ortheactual
*TTLvalueotherwi.*/
expire=getExpire(c->db,c->argv[1]);
if(expire!=-1){
ttl=expire-mstime();
if(ttl<0)ttl=0;
}
if(ttl==-1){
addReplyLongLong(c,-1);
}el{
addReplyLongLong(c,output_ms?ttl:((ttl+500)/1000));
}
}
voidttlCommand(redisClient*c){
ttlGenericCommand(c,0);
}
voidpttlCommand(redisClient*c){
ttlGenericCommand(c,1);
}
voidpersistCommand(redisClient*c){
dictEntry*de;
de=dictFind(c->db->dict,c->argv[1]->ptr);
if(de==NULL){
addReply(c,);
}el{
if(removeExpire(c->db,c->argv[1])){
addReply(c,);
++;
}el{
addReply(c,);
}
}
}
但仅是这样是不够的,因为可能存在⼀些key永远不会被再次访问到,这些设置了过期时间的key也是需要在过期后被删除的,我们甚⾄可以
将这种情况看作是⼀种内存泄露----⽆⽤的垃圾数据占⽤了⼤量的内存,⽽服务器却不会⾃⼰去释放它们,这对于运⾏状态⾮常依赖于内存的
Redis服务器来说,肯定不是⼀个好消息
主动删除
先说⼀下时间事件,对于持续运⾏的服务器来说,服务器需要定期对⾃⾝的资源和状态进⾏必要的检查和整理,从⽽让服务器维持在⼀个
健康稳定的状态,这类操作被统称为常规操作(cronjob)
在Redis中,常规操作由redis.c/rverCron实现,它主要执⾏以下操作
更新服务器的各类统计信息,⽐如时间、内存占⽤、数据库占⽤情况等。
清理数据库中的过期键值对。
对不合理的数据库进⾏⼤⼩调整。
关闭和清理连接失效的客户端。
尝试进⾏AOF或RDB持久化操作。
如果服务器是主节点的话,对附属节点进⾏定期同步。
如果处于集群模式的话,对集群进⾏定期同步和连接测试。
Redis将rverCron作为时间事件来运⾏,从⽽确保它每隔⼀段时间就会⾃动运⾏⼀次,⼜因为rverCron需要在Redis服务器运⾏期间⼀
直定期运⾏,所以它是⼀个循环时间事件:rverCron会⼀直定期执⾏,直到服务器关闭为⽌。
在Redis2.6版本中,程序规定rverCron每秒运⾏10次,平均每100毫秒运⾏⼀次。从Redis2.8开始,⽤户可以通过修改hz选项来调
整rverCron的每秒执⾏次数,具体信息请参考⽂件中关于hz选项的说明
也叫定时删除,这⾥的“定期”指的是Redis定期触发的清理策略,由位于src/redis.c的activeExpireCycle(void)函数来完成。
rverCron是由redis的事件框架驱动的定位任务,这个定时任务中会调⽤activeExpireCycle函数,针对每个db在限制的时间
REDIS_EXPIRELOOKUPS_TIME_LIMIT内迟可能多的删除过期key,之所以要限制时间是为了防⽌过长时间的阻塞影响redis的正常运
⾏。这种主动删除策略弥补了被动删除策略在内存上的不友好。
因此,Redis会周期性的随机测试⼀批设置了过期时间的key并进⾏处理。测试到的已过期的key将被删除。典型的⽅式为,Redis每秒做10次
如下的步骤:
随机测试100个设置了过期时间的key
删除所有发现的已过期的key
若删除的key超过25个则重复步骤1
这是⼀个基于概率的简单算法,基本的假设是抽出的样本能够代表整个key空间,redis持续清理过期的数据直⾄将要过期的key的百分⽐降
到了25%以下。这也意味着在任何给定的时刻已经过期但仍占据着内存空间的key的量最多为每秒的写操作量除以4.
Redis-3.0.0中的默认值是10,代表每秒钟调⽤10次后台任务。
除了主动淘汰的频率外,Redis对每次淘汰任务执⾏的最⼤时长也有⼀个限定,这样保证了每次主动淘汰不会过多阻塞应⽤请求,以下是这
个限定计算公式:
#defineACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC25/*CPUmax%forkeyscollection*/
...
timelimit=1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC//100;
hz调⼤将会提⾼Redis主动淘汰的频率,如果你的Redis存储中包含很多冷数据占⽤内存过⼤的话,可以考虑将这个值调⼤,但Redis作者建
议这个值不要超过100。我们实际线上将这个值调⼤到100,观察到CPU会增加2%左右,但对冷数据的内存释放速度确实有明显的提⾼(通
过观察keyspace个数和ud_memory⼤⼩)。
可以看出timelimit和是⼀个倒数的关系,也就是说hz配置越⼤,timelimit就越⼩。换句话说是每秒钟期望的主动淘汰频率越⾼,则
每次淘汰最长占⽤时间就越短。这⾥每秒钟的最长淘汰占⽤时间是固定的
250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),⽽淘汰频率和每次淘汰的最长时间是通过hz参数控制的。
从以上的分析看,当redis中的过期key⽐率没有超过25%之前,提⾼hz可以明显提⾼扫描key的最⼩个数。假设hz为10,则⼀秒内最少扫描
200个key(⼀秒调⽤10次*每次最少随机取出20个key),如果hz改为100,则⼀秒内最少扫描2000个key;另⼀⽅⾯,如果过期key⽐率超
过25%,则扫描key的个数⽆上限,但是cpu时间每秒钟最多占⽤250ms。
当REDIS运⾏在主从模式时,只有主结点才会执⾏上述这两种过期删除策略,然后把删除操作”delkey”同步到从结点。
maxmemory
当前已⽤内存超过maxmemory限定时,触发主动清理策略
volatile-lru:只对设置了过期时间的key进⾏LRU(默认值)
allkeys-lru:删除lru算法的key
volatile-random:随机删除即将过期key
allkeys-random:随机删除
volatile-ttl:删除即将过期的
noeviction:永不过期,返回错误当mem_ud内存已经超过maxmemory的设定,对于所有的读写请求,都会触发
redis.c/freeMemoryIfNeeded(void)函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出⾜够的内存空间。所以如果在达
到maxmemory并且调⽤⽅还在不断写⼊的情况下,可能会反复触发主动清理策略,导致请求会有⼀定的延迟。
当mem_ud内存已经超过maxmemory的设定,对于所有的读写请求,都会触发redis.c/freeMemoryIfNeeded(void)函数以清理超出的内
存。注意这个清理过程是阻塞的,直到清理出⾜够的内存空间。所以如果在达到maxmemory并且调⽤⽅还在不断写⼊的情况下,可能会反
复触发主动清理策略,导致请求会有⼀定的延迟。
清理时会根据⽤户配置的maxmemory-policy来做适当的清理(⼀般是LRU或TTL),这⾥的LRU或TTL策略并不是针对redis的所有key,⽽
是以配置⽂件中的maxmemory-samples个key作为样本池进⾏抽样清理。
maxmemory-samples在redis-3.0.0中的默认配置为5,如果增加,会提⾼LRU或TTL的精准度,redis作者测试的结果是当这个配置为10时已
经⾮常接近全量LRU的精准度了,并且增加maxmemory-samples会导致在主动清理时消耗更多的CPU时间,建议:
尽量不要触发maxmemory,最好在mem_ud内存占⽤达到maxmemory的⼀定⽐例后,需要考虑调⼤hz以加快淘汰,或者进⾏集群
扩容。
如果能够控制住内存,则可以不⽤修改maxmemory-samples配置;如果Redis本⾝就作为LRUcache服务(这种服务⼀般长时间处于
maxmemory状态,由Redis⾃动做LRU淘汰),可以适当调⼤maxmemory-samples。
以下是上⽂中提到的配置参数的说明
#Rediscallsaninternalfunctiontoperformmanybackgroundtasks,like
#closingconnectionsofclientsintimeout,purgingexpiredkeysthatare
#neverrequested,andsoforth.
##Notalltasksareperformedwiththesamefrequency,butRedischecksfor
#taskstoperformaccordingtothespecified"hz"value.
##Bydefault"hz"gthevaluewillumoreCPUwhen
#Redisisidle,butatthesametimewillmakeRedismoreresponsivewhen
#therearemanykeyxpiringatthesametime,andtimeoutsmaybe
#handledwithmoreprecision.
##Therangeisbetween1and500,howeveravalueover100isusuallynot
#ersshoulduthedefaultof10andraithisupto
#100onlyinenvironmentswhereverylowlatencyisrequired.
hz10
#MAXMEMORYPOLICY:howRediswilllectwhattoremovewhenmaxmemory
#lectamongfivebehaviors:
##volatile-lru->removethekeywithanexpiretusinganLRUalgorithm
#allkeys-lru->removeanykeyaccordingtotheLRUalgorithm
#volatile-random->removearandomkeywithanexpiret
#allkeys-random->removearandomkey,anykey
#volatile-ttl->removethekeywiththenearestexpiretime(minorTTL)
#noeviction->don'texpireatall,justreturnanerroronwriteoperations
##Note:withanyoftheabovepolicies,Rediswillreturnanerroronwrite
#operations,whentherearenosuitablekeysforeviction.
##Atthedateofwritingthecommandsare:ttnxtexappend
#incrdecrrpushlpushrpushxlpushxlinrtltrpoplpushsadd
#sintersinterstoresunionsunionstoresdiffsdiffstorezaddzincrby
#zunionstorezinterstorehthtnxhmthincrbyincrbydecrby
#gettmtmtnxexecsort
##Thedefaultis:
#maxmemory-policynoeviction
#LRUandminimalTTLalgorithmsarenotprecialgorithmsbutapproximated
#algorithms(inordertosavememory),soyoucantuneitforspeedor
#aultRediswillcheckfivekeysandpicktheonethatwas
#udlessrecently,youcanchangethesamplesizeusingthefollowing
#configurationdirective.
##Thedefaultof5producesgoodenoughresults.10Approximatesverycloly
#trueLRUbutcostsabitmoreCPU.3isveryfastbutnotveryaccurate.
#maxmemory-samples5
Replicationlink和AOF⽂件中的过期处理
为了获得正确的⾏为⽽不⾄于导致⼀致性问题,当⼀个key过期时DEL操作将被记录在AOF⽂件并传递到所有相关的slave。也即过期删除操
作统⼀在master实例中进⾏并向下传递,⽽不是各salve各⾃掌控。这样⼀来便不会出现数据不⼀致的情形。当slave连接到master后并不能
⽴即清理已过期的key(需要等待由master传递过来的DEL操作),slave仍需对数据集中的过期状态进⾏管理维护以便于在slave被提升为
master会能像master⼀样独⽴的进⾏过期处理。
本文发布于:2022-12-28 05:40:38,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/44974.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |