5分钟
Redis存储原理剖析<二>:key的惰性删除–同步删除策略
6. key删除策略
redis中,对于过期key的淘汰机制,可以分为以下两种:
- 定期删除:一种主动删除策略,定期去轮询存储了key的过期时间的字典。
- 惰性删除:在寻找/写入dictEntry的时候判断是否过期,属于被动删除策略。
6.1 惰性删除
惰性删除的代码,入口在db.c中的expireIfNeeded,该方法由执行查找命令或写入命令的方法调用,在执行真正的命令之前,做key的删除判断以及删除操作:
int expireIfNeeded(redisDb *db, robj *key) {
//设定了过期时间且未过期的key会在这一步直接返回
if (!keyIsExpired(db,key)) return 0;
//当前redis节点过期了,但是存在master节点。
//意味着当前属于slave节点,返回1,由master执行删除后同步至slave
if (server.masterhost != NULL) return 1;
//过期key的数量+1
server.stat_expiredkeys++;
//把过期的key的消息传播到slave,同时将该key的删除命令写入到aof文件
propagateExpire(db,key,server.lazyfree_lazy_expire);
//通知监听了该key的客户端,通知key的过期事件
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
//根据设定的删除方式,决定采用同步删除还是异步删除
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
int keyIsExpired(redisDb *db, robj *key) {
//获得设定的过期时间戳
mstime_t when = getExpire(db,key);
mstime_t now;
//如果时间戳小于0,说明没有设置过期时间
if (when < 0) return 0;
//loading状态下不允许进行过期判断
if (server.loading) return 0;
//获得系统当前时间
if (server.lua_caller) {
now = server.lua_time_start;
}
else if (server.fixed_time_expire > 0) {
now = server.mstime;
}
else {
now = mstime();
}
//执行判断
return now > when;
}
long long getExpire(redisDb *db, robj *key) {
dictEntry *de;
//如果存储expireTime的字典为空 或者 当前key没有设置过期时间,返回-1
if (dictSize(db->expires) == 0 ||
(de = dictFind(db->expires,key->ptr)) == NULL) return -1;
//安全保护机制,确保存储在expireDict中的key也存在于dataDict中
serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
//取得过期时间戳并返回
return dictGetSignedIntegerVal(de);
}
简略画出这段代码调用的时序图:
sequenceDiagram
participant expireIfNeeded
participant keyIsExpired
participant getExpire
participant dictFind
expireIfNeeded->>keyIsExpired:invoke
keyIsExpired->>getExpire:invoke
getExpire->>dictFind:invoke
Note OVER dictFind: 从expireDict中获取value
dictFind-->>getExpire:return value
Note OVER getExpire: 解析value获取过期时间戳
getExpire-->>keyIsExpired:return
Note OVER keyIsExpired: 将时间戳和系统当前时间比较
keyIsExpired-->>expireIfNeeded:return
Note OVER expireIfNeeded: 判断是否过期、是否为master节点
Note OVER expireIfNeeded: 通知slave节点进行key删除,并将删除命令写入AOF文件
Note OVER expireIfNeeded: 向监听了key的客户端发布key过期事件
Note OVER expireIfNeeded: 根据预设的删除方式,选择同步或者是异步删除key对应的dictEntry
其中,在使用具体的删除方式时,涉及到一个值lazyfree_lazy_expire,它需要我们在redis.conf文件中进行配置:
lazyfree-lazy-expire no
可以看到默认值为no,即代表在删除是会采用同步删除,如果将该项置为yes,则采用异步删除的模式。
接下来,分别来看一下同步删除和异步删除的代码实现。
6.1.1 同步删除
同步删除的代码比较简单:
int dbSyncDelete(redisDb *db, robj *key) {
//如果expireDict大于0,先进行expireDict的删除
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
//再进行dataDict的删除
if (dictDelete(db->dict,key->ptr) == DICT_OK) {
//如果当前是集群模式,则定位到具体存储dictEntry的slot,进行remove操作
if (server.cluster_enabled) slotToKeyDel(key);
return 1;
} else {
return 0;
}
}
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t h, idx;
dictEntry *he, *prevHe;
int table;
//dataDict和expireDict同时为空的情况下直接返回NULL
if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
//判断是否正在扩容,如果正在扩容则帮助推进hashTable的expand过程
if (dictIsRehashing(d)) _dictRehashStep(d);
//得到hash值
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
//定位到存储在hash桶中的链表头节点
he = d->ht[table].table[idx];
prevHe = NULL;
//开始从链表的头部开始,依次向后查找
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
//链表解链操作
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
//nofree传入的值为0,表示立马释放dictEntry以及其中key和value占用的内存
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
d->ht[table].used--;
return he;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return NULL;
}
其中释放内存主要是三个方法dictFreeKey,dictFreeVal,zfree,前两个方法直接调用key和value的destructor,依次看看key和value的释放过程:
define dictFreeKey(d, entry)
if ((d)->type->keyDestructor)
(d)->type->keyDestructor((d)->privdata, (entry)->key)
define dictFreeVal(d, entry)
if ((d)->type->valDestructor)
(d)->type->valDestructor((d)->privdata, (entry)->v.val)
在server.c中,可以找到对应的keyDestructor和valDestructor的原型:
dictType dbDictType = {
dictSdsHash,
NULL,
NULL,
dictSdsKeyCompare,
//负责key的销毁
dictSdsDestructor,
//负责value的销毁
dictObjectDestructor
};
可以看到,key的销毁实际上就是释放存储具体key值的sds:
void dictSdsDestructor(void *privdata, void *val)
{
DICT_NOTUSED(privdata);
sdsfree(val);
}
但是对于value,就会存在比较多的类型:string、list、set、zset、hash…,根据redisobj中的type调用不同的free方法,同时value的删除依据就是引用计数法:
void dictObjectDestructor(void *privdata, void *val)
{
DICT_NOTUSED(privdata);
if (val == NULL) return;
decrRefCount(val);
}
void decrRefCount(robj *o) {
if (o->refcount == 1) {
//使用引用计数法作为依据进行删除
switch(o->type) {
case OBJ_STRING: freeStringObject(o); break;
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(o); break;
default: serverPanic("Unknown object type"); break;
}
zfree(o);
} else {
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
}
}
不同的value类型有着不同的释放内存方法,但是它们都调用了zfree,接下来看看zfree的方法内部实现:
void zfree(void *ptr) {
//ptr指针为void类型,可以指向任意数据类型的内存
#ifndef HAVE_MALLOC_SIZE
void *realptr;
size_t oldsize;
#endif
//根据不同的宏定义,释放不同大小的内存,但是都调用了free方法
if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_free(zmalloc_size(ptr));
free(ptr);
#else
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
#endif
}
底层调用了free接口,这属于操作系统提供的标准库函数,以我的macos为例,free函数存在于_malloc.h头文件定义中。一些linux系统可能会定义在stdlib标准库头文件中。