Redis之懒惰删除

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) 3

127.0.0.1:6379> sadd src2 v3 v4 v5

(integer) 3

127.0.0.1:6379> sunionstore dest src1 src2

(integer) 5

127.0.0.1:6379> smembers dest

1) "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字典,用户flushdb
    lazyfreeFreeDatabaseFromBioThread(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);//释放key
            dictFreeVal(d, he);//释放value
            zfree(he);//最后释放entry
            ht->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
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值