为什么要处理数据过期
1.过期设置为程序逻辑的一部分,所以为了保证逻辑正确(不读取到过期数据),不得不对缓存做数据过期处理
2.过期数据,对业务来说已是无用数据,但是却仍然占有服务资源(主要是内存和磁盘),故处理过期数据,将其删除可以使服务资源得到释放
处理过期数据的常用策略
策略 | 说明 | 优点 | 缺点 |
---|---|---|---|
定时删除 | 根据键的过期时间设置定时器,触发超时及删除对应键 | 删除及时,内存友好 | 在内存不紧张,cpu紧张的情况下,cpu不友好 |
惰性删除 | 程序在取键时才对键进行过期检查 | cpu友好,只有在取键时才可能会触发删除当前键 | 内存不友好 |
定期删除 | 每隔一段时间执行一次限时限量的批量删除 | 相对中性,不像定时删除那样可能短时间占用大量cpu,也没有惰性删除浪费内存空间多 | - |
redis同时采用定期删除和惰性删除策略: 使用定期策略可以更平滑的利用cpu和内存资源,但是会存在过期数据失效不及时的问题。用惰性删除加以辅助便可达到定期删除下访问实时失效的效果
redis惰性删除
redis在客户端获取数据时,首先判断数据是否设置过期时间,如果设置了过期时间,且当前时间大于过期时间则删除对应的key
核心代码: db.c:expireIfNeeded函数中
//返回0未过期、大于0过期
int expireIfNeeded(redisDb *db, robj *key) {
//key没设置超时时间返回-1,设置超时时间则返回超时时间的ms值
mstime_t when = getExpire(db,key);
mstime_t now;
//未设置过期时间
if (when < 0) return 0;
if (server.loading) return 0;
//如果该缓存是slave,则只返回结果,不执行后续删除键操作
if (server.masterhost != NULL) return now > when;
//master过期则直接返回0
if (now <= when) return 0;
server.stat_expiredkeys++;
//往aof文件里添加删除命令,同时也给slave发送删除key的命令
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
//删除过期键
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
redis定期删除
除了惰性删除外,redis还会定期批量删除过期键
核心代码: expire:activeExpireCycle函数
void activeExpireCycle(int type) {
...
int j, iteration = 0;
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
int expired;
//递增选取一个数据库进行接下来的删除工作(redis支持多数据库)
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
iteration++;
//超时字典里没有任何数据,则直接退出
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
//获取当前时间
now = mstime();
//过期字典中元素数少于其slot位数量的1%则不处理过期(说明过期的健很少,对系统内存影响不大,先不处理)
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
//控制循环次数为<=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP(20次以内)
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
while (num--) {
dictEntry *de;
long long ttl;
//随机获取过期字典中的某个键
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
//获取该键的超时时间
ttl = dictGetSignedIntegerVal(de)-now;
//如果该键超时则删除之
if (activeExpireCycleTryExpire(db,de,now)) expired++;
//每循环16次则进行如下检查
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
elapsed = ustime()-start;
//删除过期键执行时间超过最大执行时间(1s),则退出删除操作
if (elapsed > timelimit) {
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break;
}
}
//当随机抽取的键超过1/4过期(说明过期键的比例相对比较高),则继续循环删除
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
}
...
定期删除由redis中databasesCron接口调用activeExpireCycle触发(每秒执行一次)
定期删除流程如下:
关键点分析:
1.为什么存储设置了过期键的槽位占过期字典总槽位1%以下就不处理过期?
答: 因为此种情况下过期键的数量很少,对内存利用率的影响并不显著,暂时无需花费大量cpu处理过期删除任务
2.为什么随机查找某个库过期字典中20个元素,过期键超过1/4就继续执行此操作?
答: 随机挑选20个元素,过期比例超过1/4(这里属于抽样调查),则过期键相对挺多,可能对内存的使用率造成影响。故可循环执行此删除操作
3.为什么删除操作执行超过1s就会停止删除
答: redis执行删除操作会在主线程执行, 如果过期键太多,执行时间太长会使当前redis处于阻塞状态无法执行其他命令。所以加此最大时间限制可尽可能的减少此种情况下对服务的影响
redis持久化如何应对过期数据
rdb持久化模式下:
生成rdb文件时: redis会检查保存的数据是否过期,如果过期则不会写入rdb文件
载入rdb文件时: redis会对rdb文件中的数据进行过期检查,已过期的数据会被忽略,不写入内存
aof持久化模式下:
追加aof文件时: 当有数据被删除,redis会追加该删除命令倒aof文件中去
重写aof文件时: 已过期的数据不会写入新aof文件(重写文件体积压缩的原因之一)
###redis复制如何应对过期数据
这里的复制指的是redis主备下的复制,前文介绍的其实都是redis主机处理过期键的方式,那么redis备机处理过期方式有何不同呢?
redis备机处理过期:
redis备机不主动对过期键进行删除,主机在处理过期键时会发送del命令给备机,此时备机才会删除对应的过期键
可以看出redis备机处理过期键相当简单,只需等待主机发来del命令执行即可,但是关于备机过期键这里还是需要弄清楚如下几个问题:
1.reids备机为啥不自己去执行类似主机的删除策略去处理过期键?
答: 因为redis需要保证主备机数据的一致性,如果各自处理过期数据,会造成数据不一致。可能有些人会疑惑,既然数据都过期了,不管删不删用户都不会访问到此数据了,所以站到用户的角度其实数据还是一致的呀?这里其实作者起初也认为如此,但是不知道您知不知道redis提供了一个移除键过期时间的命令:persist,在极端情况下,例如主备发生分区,然后主机对某个设置了过期时间的key执行了persist将其变为永久key,此时备机收不到主机的persist命令,当key过期时,备机将其移除。这种情况下就明显发生了主备不一致的现象
2.既然备机不主动执行删除操作,那么用户访问备机上的某个备机键,能获得结果么?
答: 当有客户端访问备机过期键时,备机不执行删除操作,但是会给客户端返回空
总结
redis源码相对比较简洁,代码注释详细,且对应的分析书籍较多。多学习学习总有收获