Redis内部有一个异步线程叫做BIO(background io),懒惰删除的内存回收就是使用这个线程异步去做的。

非异步回收

redis最开始实现懒惰删除时,并不是在异步线程里做,而是在异步线程里使用字典渐进式搬迁的方法来实现渐进式回收,

例如对一个很大的hash字典,使用scan的方式,遍历第一维数组逐步删除二维链表内容,等所有的链表回收完了,再一次性回收一维数组,

这种方式,需要控制好回收频率,不能太慢,否则内存会增长的较快,也不能太快,会消耗CPU资源,

Redis在主线程中实现时,先判断内存增长趋势是+1还是-1,来渐进式调整回收频率系数,但是经过测试,这种方式在服务繁忙时,QPS会下降到正常情况的65%水平,

因为使用主线程渐进式回收效率不是很理想,所以redis实现了异步回收,只需将对象从全局字典中摘除,放入一个队列中,主线程继续响应业务请求,删除回收的操作由异步线程来做,

由于主线程和异步线程之间在内存回收器(jemalloc)的使用上存在竞争,也会造成一些资源消耗,不过与直接用主线程来做回收来比,这点消耗可以忽略不计。

异步回收的问题

redis之前的设计中,存在对象共享,这个设计导致了异步回收删除会存在一些问题,对象共享的概念如下:

使用 sunionstore 将两个对象合并成一个对象:

127.0.0.1:6379> sadd src1 v1 v2 v3(integer) 3127.0.0.1:6379> sadd src2 v3 v4 v5(integer) 3127.0.0.1:6379> sunionstore dest src1 src2(integer) 5127.0.0.1:6379> smembers dest1) "v2"2) "v3"3) "v4"4) "v1"5) "v5"复制代码

在对象共享设计中,dest 会与 src1、src2 共享底层字符串对象。

此时如果我们异步删除dest,但是dest中的元素还与src1,src2有关联,那么我们就无法做到直接删除整个dest的空间,还需要判断里面的元素有没有被其他对象引用,没有才可以删除,有则将引用-1,

这是一个比较耗费资源的事情,同时如果对ref-1的时候,主线程也在操作这个对象,那么就会产生竞争,导致主线程的卡顿,因此对象共享设计严重影响到了异步删除为了提高效率的初衷,

为此,redis直接抛弃了对象共享设计,采用 share-nothing,无共享设计的办法,解决了这个问题,此后异步删除就不会导致主线程的卡顿了。

异步删除实现

在使用异步删除时,主线程通过包装一个bio_job结构的元素,放到一个双向链表中,异步线程通过遍历链表得到job元素来挨个执行异步任务,因为链表是多线程被操作的,因此有锁对链表进行保护。

bio_job结构:

struct bio_job {time_t time; // 时间,预留字段void *arg1, *arg2, *arg3;
}复制代码

三个arg的作用:

/* What we free changes depending on what arguments are set:* arg1 -> free the object at pointer.* arg2 & arg3 -> free two dictionaries (a Redis DB).* only arg3 -> free the skiplist. */
if (job->arg1)// 释放一个普通的对象,string/set/zset/hash等,用于普通对象的异步删除lazyfreeFreeObjectFromBioThread(job->arg1);
else if (job->arg2 && job->arg3)// 释放全局redisDb对象的dict字典和expires字典,用户flushdblazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
else if (job->arg3)// 释放cluster的slots_to_keys对象lazyfreeFreeSlotsMapFromBioThread(job->arg3);
复制代码

普通对象的异步删除方法实现 lazyfreeFreeObjectFromBioThread :

void lazyfreeFreeObjectFromBioThread(robj *o) {decrRefCount(o); // 降低对象的引用计数,如果为0,直接释放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); break;case OBJ_HASH: freeHashObject(o); break; // 释放hash对象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--;// 引用计数-1}
}//释放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;/* Free all the elements */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);//释放keydictFreeVal(d, he);//释放valuezfree(he);//最后释放entryht->used--;he = nextHe;}}/* Free the table and the allocated cache structure */zfree(ht->table);//回收一维数组/* Re-initialize the table */_dictReset(ht);return DICT_OK; /* never fails */
}复制代码

队列安全

异步删除任务队列是一个不安全的双向链表,需要锁进行保护,当主线程追加元素时,需要对其加锁,完毕后,释放锁,如果异步线程正在休眠,并对其唤醒。

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) {struct bio_job *job;unsigned long type = (unsigned long) arg;sigset_t sigset;/* Check that the type is within the right interval. */if (type >= BIO_NUM_OPS) {serverLog(LL_WARNING,"Warning: bio thread started with wrong type %lu",type);return NULL;}/* Make the thread killable at any time, so that bioKillThreads()* can work reliably. */pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);pthread_mutex_lock(&bio_mutex[type]); // 加锁/* Block SIGALRM so we are sure that only the main thread will* receive the watchdog signal. */sigemptyset(&sigset);sigaddset(&sigset, SIGALRM);if (pthread_sigmask(SIG_BLOCK, &sigset, NULL))serverLog(LL_WARNING,"Warning: can't mask SIGALRM in bio.c thread: %s", strerror(errno));// 循环处理while(1) {listNode *ln;/* The loop always starts with the lock hold. */if (listLength(bio_jobs[type]) == 0) { // 队列为空,休眠pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);continue;}/* Pop the job from the queue. */ln = listFirst(bio_jobs[type]); // 获取队列头元素job = ln->value;/* It is now possible to unlock the background system as we know have* a stand alone job structure to process.*/pthread_mutex_unlock(&bio_mutex[type]); // 释放锁/* Process the job accordingly to its type. */if (type == BIO_CLOSE_FILE) {close((long)job->arg1);} else if (type == BIO_AOF_FSYNC) {aof_fsync((long)job->arg1);} else if (type == BIO_LAZY_FREE) {/* What we free changes depending on what arguments are set:* arg1 -> free the object at pointer.* arg2 & arg3 -> free two dictionaries (a Redis DB).* only arg3 -> free the skiplist. */if (job->arg1)lazyfreeFreeObjectFromBioThread(job->arg1);else if (job->arg2 && job->arg3)lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);else if (job->arg3)lazyfreeFreeSlotsMapFromBioThread(job->arg3);} else {serverPanic("Wrong job type in bioProcessBackgroundJobs().");}zfree(job); // 释放任务对象/* Unblock threads blocked on bioWaitStepOfType() if any. */pthread_cond_broadcast(&bio_step_cond[type]);/* Lock again before reiterating the loop, if there are no longer* jobs to process we'll block again in pthread_cond_wait(). */pthread_mutex_lock(&bio_mutex[type]); // 再次加锁继续处理下一个元素listDelNode(bio_jobs[type],ln); // 任务处理完毕,从链表中删除节点bio_pending[type]--;// 计数-1}
}

Redis之懒惰删除相关推荐

  1. 【Redis】懒惰删除

    一直以来我们认为 Redis 是单线程的,单线程为 Redis 带来了代码的简洁性和丰富多样的数据结构.不过Redis内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作. R ...

  2. 关于 Redis 的 懒惰删除 (十三)

    平波缓进 -- 懒惰删除 一直以来我们认为 Redis 是单线程的,单线程为 Redis 带来了代码的简洁性和丰富多样的数据结构.不过Redis内部实际上并不是只有一个主线程,它还有几个异步线程专门用 ...

  3. 如履薄冰 —— Redis懒惰删除的巨大牺牲

    前言 之前我们介绍了Redis懒惰删除的特性,它是使用异步线程对已经删除的节点进行延后内存回收.但是还不够深入,所以本节我们要对异步线程逻辑处理的细节进行分析,看看Antirez是如何实现异步线程处理 ...

  4. 如履薄冰:Redis 懒惰删除的巨大牺牲

    作者 | 老钱 责编 | 胡巍巍 大家都知道 Redis 是单线程的,但是 Redis 4.0 增加了懒惰删除功能,懒惰删除需要使用异步线程对已删除的节点进行内存回收,这意味着 Redis 底层其实并 ...

  5. Redis 大键值对 Big Key 懒惰删除机制

    一.懒惰删除介绍 在删除元素数量很多的集合(set/hash/list/sortedSet)时,无论是使用DEL命令删除还是redis为了释放内存空间而进行的删除,在删除这些big key的时候,会导 ...

  6. Redis进阶-Redis的惰性删除

    文章目录 Pre del ------> unlink FLUSHDB/FLUSHALL --> FLUSHDB ASYNC/FLUSHALL ASYNC 异步队列 AOF Sync 扩展 ...

  7. Redis之惰性删除

    场景 使用del指令删除key时,redis会直接释放对象的内存,当对象很小时这个指令速度是很快的,但是如果是一个很大的对象被del时,那么删除操作就会比较耗时,造成其他指令阻塞,对客户端造成卡顿的现 ...

  8. php redis删除所有key,redis中批量删除key的方法

    Redis是一个高性能的key-value数据库.redis中可以借助Linux的xargs指令来批量删除key,也可以使用flushdb和flushall命令删除所有key. 批量删除Key Red ...

  9. 数据结构和算法分析学习笔记(三)--二叉查找树的懒惰删除(lazy deletion)

    这次的问题来自<数据结构与算法分析(C++描述)>的习题4.16,如下: -------------------------- 4.16  重做二叉查找树类以实现懒惰删除.注意,这将影响所 ...

最新文章

  1. 坚定不移地加速,并且不断解决新问题
  2. java调用qq接口_用java代码怎么去请求腾讯接口并返回值
  3. UE4使用自定义字体
  4. 指向老域名的反链丢失问题
  5. html网络通信协议设计,智能家居无线网络通讯协议设计方案
  6. win10恢复出厂设置_手机如何恢复出厂设置
  7. Teigha 4.0 Net 开发记录
  8. NorthWind基本数据库添加问题
  9. Matlab 导入数据操作
  10. 苹果macOS 13 Ventura beta版如何转成正式版?如何将 MacOS Beta 版更新为正式版?
  11. CorelDRAW2022增强版CDR2022新版功能
  12. 我的申请已经通过审核,你想成为阿里云 MVP 吗?
  13. 计算机音乐数字乐谱梦中的婚礼,乐谱梦中的婚礼代码是什么-天谕手游梦中的婚礼歌曲最新乐谱代码分享 - QT软件园...
  14. js笔试面试题(随意)
  15. currency.js -用于处理货币值的小型轻量级 JavaScript 库(也可以处理数值计算精度问题等)
  16. turtle画樱花树林
  17. 13. 模板匹配-cv2.matchTemplate()、cv2.minMaxLoc()
  18. mysql中计算两个日期的时间差函数TIMESTAMPDIFF用法
  19. 动手吧,vue移动端悬浮球组件
  20. 2013新东方考研英语视频课程(33G)【全程无加密】

热门文章

  1. IDEA快捷键高清壁纸
  2. uniapp 二维码展示和扫码
  3. 什么事css+hack,css hack的理解
  4. Benchmark初印象
  5. 我的职业思路:做建模,为什么,不学C4D
  6. Filter过滤器导致CSS样式失效
  7. 学习QT之图形视图实例#-飞舞的蝴蝶
  8. android上调试H5小工具
  9. 巴比馒头:年销售20亿背后的数字化秘密
  10. 团队作业(2)项目选题