Redis-懒惰删除内部实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zanpengfei/article/details/86519006

        异步线程在Redis内部有个特别的名称,叫作BIO(background IO),意思是后台运行的io线程,不过内存回收本身不是io操作,只是cpu计算消耗比较大。

1、Redis的内部对象共享机制会阻碍异步线程的改造,原因是懒惰删除是要测底删除某个对象,不能藕断丝连,如果底层数据的共享的,就不能做到彻底删除了效果。所以为了支持懒惰删除,只能将对象共享机制彻底抛弃。

2、异步删除的实现

1)主线程将删除任务传递给异步线程,通过一个双向列表的结构,双向列表要支持多线程并发,需要一把锁来保护

2)执行懒惰删除,是将其参数构造成一个bio_job结构,然后追加到列表的末尾,异步线程通过遍历列表来执行删除操作

struct bio_job {

    time_t time;  // 时间字段暂时没有使用,应该是预留的

    void *arg1, *arg2, *arg3;

};

3)bio_job结构中有三个参数,通过下面的源码可以看出,arg1是释放普通对象,arg2释放全局redisDb对象的dict字段及expire字段,arg3释放Cluster的slots_to_keys对象

/* 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);

4)删除普通对象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); 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 */

}

3、队列安全

1)上诉我们提到主线程将待删除对象放到双向列表中,放之前要对该列表加锁,添加完成后释放锁,如果异步线程处于休眠的状态下,还要唤醒该线程

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]); // 释放锁

}

2)异步线程对列表进行轮训处理,依次从列表表头摘去元素逐个处理,摘取元素时,也要对该列表上锁,摘取完以后,释放锁,如果一个元素都没有,该线程将休眠,需要主线程唤醒

// 异步线程执行逻辑

void *bioProcessBackgroundJobs(void *arg) {

...

    pthread_mutex_lock(&bio_mutex[type]); // 先加锁

    ...

    // 循环处理

    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]); // 释放锁

        // 这里是处理过程,为了省纸,就略去了

        ...

        // 释放任务对象

        zfree(job)

        ...      

        // 再次加锁继续处理下一个元素

        pthread_mutex_lock(&bio_mutex[type]);

        // 因为任务已经处理完了,可以放心从链表中删除节点了

        listDelNode(bio_jobs[type],ln);

        bio_pending[type]--; // 计数减 1

    }

 

猜你喜欢

转载自blog.csdn.net/zanpengfei/article/details/86519006