Redis的懒惰删除
1.Redis中的懒惰删除
Redis内部实际上并不只有一个主线程,还有几个异步线程专门用来处理一些耗时的操作。异步线程在Redis内部被称为“BIO”,全称是Background IO,意思是在背后默默干活的IO线程。
(1)del指令
Redis的删除指令del会直接释放对象的内存,多数情况下,这个指令非常快。但是如果被删除的key是一个非常大的对象,那么删除操作就会导致单线程卡顿。
为了解决这个问题,Redis4.0版本引入了unlink指令,它能对删除指令进行懒处理,将其丢给后台线程来异步回收内存。
并不是所有的unlink操作都会延后处理,若是对应key所占用的内存很小,延后处理就没有必要了。这时候redis会将对应key的内存立即回收,跟del指令一样。
(2)flush操作
Redis提供了flushdb和flushall指令,用来清空数据库,这也是极其缓慢的操作。Redis4.0同样给这两个指令带来了异步化,在指令后面增加async参数就可以将其丢给后台线程来异步处理。
主线程将对象的引用摘除后,会将这个key的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。
(3)AOF Sync操作
Redis需要每秒1次同步AOF日志到磁盘,确保消息尽量不丢失,需要调用sync函数,这个操作比较耗时,会导致主线程的效率下降,因而Redis也将这个操作移到异步线程来完成。
执行AOF Sync操作的线程是一个独立的异步线程,和前面的懒惰删除线程不是一个线程。
除了上述操作之外,redis在key的过期、LRU淘汰、rename指令过程中,也会实施回收内存。
2.懒惰删除的牺牲
用异步线程的方法来进行释放内存确实非常方便,要做的只是将对象从全局字典中摘掉,然后往队列里一扔,主线程就可以去干别的事情了。异步线程从队列里取出对象,直接进行释放内存操作就可以了。
但是,使用异步线程是有代价的。主线程和异步线程之间在内存回收器的使用上存在竞争。并且,更为重要的是用异步队列来回收内存的话,就不得不将对象共享机制摈弃了。
Redis内部的对象有共享机制,比如集合的并集操作
这里可以看到新的集合包含了旧集合的所有元素,此时底层的字符串对象被共享了,如图所示
懒惰删除是把某个对象丢掉,扔到异步删除队列中去,这里必须是彻底删除,不可以藕断丝连。如果底层对象是共享的话,就做不到彻底删除。
3.异步删除的实现
主线程需要将删除任务传递给异步线程,它是通过一个普通的双向链表来传递的,由于需要涉及多线程并发操作,需要有锁来保护。
执行懒惰删除时,Redis将删除操作的相关参数封装成一个bio_job结构,然后追加到链表尾部,异步线程通过遍历链表摘取job元素来挨个执行异步任务。
struct bio_job {
time_t time;
void *arg1, *arg2, *arg3;
}
上面这三个参数arg1,arg2,arg3就是用来确定具体删除哪种类型对象的。
/* 释放的内容取决于设置的参数
* arg1:释放普通对象
* arg2 & arg3:释放dict字典和expiers字典
* 只有arg3:释放跳表
*/
if(job->arg1) {
//释放一个普通对象,用于普通对象的异步删除
lazyfreeFreeObjectFromBioThread(job->arg1);
}else if(job->arg2 & job->arg3) {
//释放全局redisDb对象的dict字典和expires字典,用于flushdb
lazyfreeFreeDatabaseFromBioThread(job->arg2, job->arg3);
}else if(job->arg3) {
//释放Cluster的slots_to_keys对象
lazyfreeFreeSlotMapFromBioThread(job->arg3);
}
接下来看看普通对象的异步删除lazyfreeFreeObjectFromBioThread是如何进行的。
void lazyfreeFreeObjectFromBioThread(robj *o) {
decrRefCount(o); //降低对象的引用计数,如果为零,就释放
atomicDecr(lazyfree_objects, 1); //lazyfree_objects为待释放对象的数量,用于统计
}
//减少引用计数
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);
beak;
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--;
}
}
//释放hash对象
void freeHashObject(robj *o){
switch(o->encoding){
case OBJ_ENCODING_HT:
dictRelease((*dict) o->ptr);
break;
case OBJ_ENCODING_ZIPLIST:
//压缩列表可以直接释放,因为是一整块字节数组
zfree(o->ptr);
break;
default:
serverPanic("Unknown hash encoding type");
break;
}
}
//释放字典,如果字典正在迁移中,ht[0]和ht[1]分布存储旧字典和新字典
void dictRelease(dict *d){
_dictClear(d, &d->ht[0], NULL);
_dictClear(d, &d->ht[1], NULL);
zfree(d);
}
//释放hashtable
//需要遍历第一维数组,然后继续遍历第二维数组
int _dictClear(dict *d, dictht *ht, void(callback)(void *)){
unsigned long i;
for(i = 0; i < ht->size && ht->used > 0; i++){
dictEntry *he, *nextHe;
if(callback && (i & 65535) == 0)
callback(d->privdata);
if((he = ht->table[i]) == NULL)
continue;
while(he) {
nextHe = he->next;
dictFreeKey(d, he); //先释放key
dictFreeVal(d, he); //再释放value
zfree(he); //最后释放entry
ht->used--;
he = nextHe;
}
}
zfree(ht->table); //回收第一维数组
_dictReset(ht);
return DICT_OK;
}
4.队列安全
当主线程将任务追加到队列之前需要给它加锁,追加完毕后,再释放锁,若是异步线程在休眠中的话还需要唤醒异步线程。
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
struct bio_job *job = zmalloc(sizeof(*job));
job->time = time(NULL);
job->arg1 = arg1;
job->arg2 = arg2;
job->arg3 = arg3;
pthread_mutex_lock(&bio_mutex[type]); //加锁
listAddNodeTail(bio_jobs[type], job); //追加任务
bio_pending[type]++; //计数
pthread_cond_signal(&bio_newjob_cond[type]); //唤醒异步线程
pthread_mutex_unlock(&bio_mutex[type]); //释放锁
}
异步线程需要对任务队列进行轮询处理,依次从链表表头摘取元素逐个处理。摘取元素的时候也需要加锁,摘下后再解锁。如果一个元素都没有的话,它需要等待,直到主线程来唤醒它继续工作。
//异步线程执行逻辑
void *bioProcessBackgroundJobs(void *arg) {
...
pthread_mutex_lock(&bio_mutex[type]);
...
while(1){
listNode *ln;
if(listLength(bio_jobs[type]) == 0) {
//队列空,线程休眠
pthread_cond_wait(&bio_newjob_cond[type], &bio_mutex[type]);
continue;
}
ln = listFirst(bio_jobs[type]); //获取队列头元素
job = ln->value;
pthread_mutex_unlick(&bio_mutex[type]); //释放锁
...
//释放任务对象
zfree(job);
...
//再次加锁处理下一个元素
pthread_mutex_lock(&bio_mutex[type]);
//因为任务已经处理完了,可以放心从链表中删除节点了
listDelNode(bio_jobs[type], ln);
bio_pending[type]--;
}
}