大家都知道Redis是单线程的,但是Redis4.0增加了懒删除功能。惰性删除需要使用异步线程来回收被删除节点的内存,也就是说Redis底层其实并不是单线程的,它内部多了几个鲜为人知的辅助线程。这些辅助线程在Redis内部有一个特殊的名字,就是“BIO”。全称是BackgroundIO,意思是在幕后默默工作的IO线程。但是内存回收本身并不是IO操作,但是对CPU的计算消耗可能比较大。01.懒删除最初的实现并不是异步线程。Redis老大Antirez在实现懒删除的时候,从一开始就没有想到异步线程。它最初的尝试是在主线程中实现渐进式删除和恢复,采用了类似于字典渐进式搬迁的方法。例如,对于一个非常大的字典,延迟删除使用类似于扫描操作的方法。通过遍历一维数组逐步删除回收二维链表的内容,等到所有链表都被回收了,再一次回收第一个。一维数组。这样也可以达到删除大对象时不阻塞主线程的效果。但这说起来容易做起来难。渐进式回收需要小心控制回收频率,不能回收太快,会造成过多的CPU资源占用,也不能像蜗牛一样缓慢回收,因为内存消耗可能会持续增长没有及时回收。Antirez需要使用合适的自适应算法来控制回收频率。他首先想到的是通过检测内存增长趋势是增加“+1”还是减少“-1”来逐步调整恢复频率系数。这种自适应算法实现起来也非常简单。但是经过测试发现,当服务繁忙时,QPS会下降到正常水平的65%,这是非常致命的。这就是为什么Antirez使用今天的解决方案——异步线程。异步线程解决方案要简单得多。释放内存,不需要为每个数据结构适配渐进式释放策略,也不需要使用自适应算法来小心控制回收频率。您只需从全局字典中删除该对象。然后丢进队列,主线程再做其他事情。异步线程从队列中取出对象,按照正常的同步释放逻辑即可。但是,使用异步线程是有代价的。内存收集器(jemalloc)的使用在主线程和异步线程之间存在竞争。这种竞争消耗可以忽略不计,因为Redis的主线程花费在内存分配和回收上的时间相对于整体的计算时间来说是非常少的。02.异步线程方案其实挺复杂的。刚才作者说异步线程方案很简单。这里为什么说复杂呢?因为有一点作者之前没有提到,这一点非常可怕,严重阻碍了异步线程方案的改造,那就是Redis的内部对象是有共享机制的。例如,集合联合操作sunionstore用于将多个集合合并为一个新的集合。>saddsrc1value1value2value3(integer)3>saddsrc2value3value4value5(integer)3>sunionstoredestsrc1src2(integer)5>smembersdest1)"value2"2)"value3"3)"value1"4)"value4"5)"value5"我们看到新的集合包含旧系列的所有元素。但是这里有一个技巧我们没有看到,就是底层的字符串对象是共享的,如下图所示。为什么对象共享对惰性删除来说是一个巨大的障碍?因为懒删除相当于完全切断一个分支,丢进异步删除队列。注意这里一定要彻底删除,不能破解。如果底层对象是共享的,则无法完全删除。图2中所示的删除不是完全删除。因此,为了支持惰性删除,Antirez完全舍弃了对象共享机制。它将这个对象结构称为“share-nothing”,这是一种不共享的设计。但是摆脱对象共享说起来容易做起来难!这种对象共享机制散布在源代码的各个角落,牵一发而动全身。这就像小心翼翼地走在布满地雷的路上。然而,Antirez决心改变它。他将这次变革描述为“绝望和疯狂”,可见这次变革有多么大、多么深、多么危险,而且花了数周时间才完成变革。不过这个修改的效果也很明显,对象的删除不会再导致主线程卡顿。03.异步删除的实现主线程需要把删除任务交给异步线程,通过一个普通的双向链表来传递。因为链表需要支持多线程并发操作,所以需要用锁来保护。在进行惰性删除时,Redis会将删除操作的相关参数封装成一个bio_job结构体,然后追加到链表的尾部。异步线程通过遍历链表提取作业元素,一个一个执行异步任务。structbio_job{time_ttime;//时间字段暂时不用,应该保留void*arg1,*arg2,*arg3;};我们注意到这个作业结构有三个参数。为什么删除对象需要三个参数?让我们看看下面的代码。/*Whatwefreechangesdependingonwhatargumentsareset:*arg1->freetheobjectatpointer.*arg2&arg3->freetwodictionaries(aRedisDB).*onlyarg3->freetheskiplist.*/if(job->arg1)//释放一个普通对象,string/set/zset/hash等.,普通对象的异步删除lazyfreeFreeObjectFromBioThread(job->arg1);elseif(job->arg2&&job->arg3)//释放全局redisDb对象的dict字典和expires字典forflushdblazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);elseif(job->arg3)//释放Cluster的slots_to_keys对象,参考5.7节lazyfreeFreeSlotsMapFromBioThread(job->arg3);可以看到通过这三个参数的组合可以实现不同结构体的释放逻辑。接下来我们继续跟踪lazyfreeFreeObjectFromBioThread是如何进行普通对象异步删除的,请仔细阅读代码注释。voidlazyfreeFreeObjectFromBioThread(robj*o){decrRefCount(o);//减少对象的引用计数,如果为零则释放atomicDecr(lazyfree_objects,1);//lazyfree_objects为要释放的对象个数,用于统计}//减少引用计数voiddecrRefCount(robj*o){if(o->refcount==1){//是时候释放对象了switch(o->type){caseOBJ_STRING:freeStringObject(o);break;caseOBJ_LIST:freeListObject(o);break;caseOBJ_SET:freeSetObject(o);break;caseOBJ_ZSET:freeZsetObject(o);break;caseOBJ_HASH:freeHashObject(o);break;//释放hash对象,继续追踪caseOBJ_MODULE:freeModuleObject(o);break;caseOBJ_STREAM:freeStreamObject(o);break;default:serverPanic("Unknownobjecttype");break;}zfree(o);}else{if(o->refcount<=0)serverPanic("decrRefCountagainstrefcount<=0");if(o->refcount!=OBJ_SHARED_REFCOUNT)o->refcount--;//引用计数减1}}//释放哈希对象voidfreeHashObject(robj*o){switch(o->encoding){caseOBJ_ENCODING_HT://释放字典,我们继续跟踪dictRelease((dict*)o->ptr);break;caseOBJ_ENCODING_ZIPLIST://如果是压缩列表,可以直接释放//因为压缩列表是一整字节数组zfree(o->ptr);break;default:serverPanic("Unknownhashencodingtype");break;}}//释放字典,如果字典正在迁移,ht[0]和ht[1]分别存储旧字典和新字典voiddictRelease(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*)){unsignedlongi;/*Freealltheelements*/for(i=0;i
