Redis的懒惰删除

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]--;
	}
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

loser与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值