memcached学习之assoc部分

本文详细介绍了memcached的assoc部分,包括其作为哈希表的功能,整体逻辑,如双表结构、数据操作和数据均衡线程。assoc_insert、assoc_find和assoc_delete是主要的数据操作,而assoc_maintenance_thread则负责维护哈希表的性能,避免冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

assoc部分的功能介绍

这部分主要是一个hash表(hash函数采用Jenkins_hash),用于保存item数据的地址,主要是用来快速查找item信息,这是因为一般情况下缓存或存储数据库的读写频率是不一样的,读频率会很大,这样如果每次都采用遍历操作,会严重影响性能。其实这种做法和DB是很相似的地方,只不过真正的DB采用的树结构,这里由于在内存中存储,不涉及磁盘等细节,采用hash能够更加简单方便。

assoc部分的整体逻辑

1、assoc中的存储组织结构

内部哈希表组织结构图

assoc中主要包含了两张hash表,一张主表primary_hashtable,一张副表old_hashtable,old_hashtable是在当primary_hashtable需要扩展时充当备份表来用的;primary_hashtable是一个包含hashsize(hashpower)个item的指针的数组,其中每一个item的指针实际上一个对应哈希值的数据链表的表头。

2、assoc中的数据操作

对数据的操作,无非是增删改查,这是针对item的,而在改的过程中,实际上对hash表是没有影响的,因为key没有变化,另外许多改的操作也可以转化为先删再增的分解过程,因此可以看到assoc中的数据操作只有assoc_insert, assoc_find, assoc_delete三个。

3、assoc中的数据均衡线程

在memcached中,会启动一个assoc的maintenance线程,这个线程的主要目的是保持hashtable不至于过于冲突,避免耗时操作。这个线程中主要的逻辑过程如下:

Created with Raphaël 2.1.0开始判断hash_items是否超过hashsizeYes or No启动扩张执行每次移动hash_bulk_move个bucket扩张完毕判断是否停止线程Yes or No结束yesnoyesno

assoc部分的代码分解

1、变量分解
1)条件锁maintenance_cond,用于触发扩张hash表线程启动;
2)互斥锁maintenance_lock,目前看主要用于锁定do_run_assoc_thread;
3)互斥锁hash_items_counter_lock,用于hash_items的计数锁;
4)整型hashpower,用来指定hash表的bucket的数量,是该值的2次幂,默认是16;
5)primary_hashtable,主hash表,当没有在扩张的时候,查询都是查询这个表;
6)old_hashtable,旧hash表,在扩张时,需要在这里查询尚未转移到新表中的item;
7)hash_items, hash表中item的个数;
8)布尔类型expanding,是否正在扩张中;
9)布尔类型start_expanding,是否启动扩张;
10)expand_bucket,当前已扩张到的bucket;
11)do_run_maintenance_thread,是否执行maintenance线程,默认为1;
12)hash_bulk_move,扩张时单次移动的hash表中的bucket数量。

2、函数分解
1)assoc_init函数,根据设定的hash表大小初始化primary_table,该函数在main函数中调用,只被调用一次;

void assoc_init(const int hashtable_init) {
    if (hashtable_init) {    //判断hashtable_init是否包含值,没有则hashpower为默认值16
        hashpower = hashtable_init;
    }
    primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));      //为primary_hashtable分配内存,hash表中只用来存储item的指针
    if (! primary_hashtable) {   //若分配不成功退出
        fprintf(stderr, "Failed to init hashtable.\n");
        exit(EXIT_FAILURE);
    }
    STATS_LOCK();   //以下是状态信息统计,不分析
    stats.hash_power_level = hashpower;
    stats.hash_bytes = hashsize(hashpower) * sizeof(void *);
    STATS_UNLOCK();
}

2)assoc_find,通过key和key的长度以及哈希值hv,返回找到的内容,要么是对应的item,要么是NULL;

item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
    item *it;
    unsigned int oldbucket;

    //查看是否正在扩张,是的话需要判断去primary_hashtable中查找还是old_hashtable中查找
    if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {   //如果正在扩张,且待搜索的bucket还未被转移,在old_hashtable中搜索
        it = old_hashtable[oldbucket];
    } else {
        //否则,在primary_hashtable中搜索
        it = primary_hashtable[hv & hashmask(hashpower)];
    }

    //需要注意的是,在hash表中找到的只是一个链表的头,这个链表对应的哈希值都是hv,所以需要遍历链表去寻找真正的item
    item *ret = NULL;
    int depth = 0;
    while (it) {
        if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {    //先比较长度,避免字符串比较,节省时间,之所以这样,是因为同样长度且不同key内容在同一个bucket中的概率要小得多
            ret = it;
            break;
        }
        it = it->h_next;
        ++depth;
    }
    MEMCACHED_ASSOC_FIND(key, nkey, depth);
    return ret;
}

3)_hashitem_before函数,获取当前key对应的item的前驱item的h_next的地址,若*item=0,则item不存在;

static item** _hashitem_before (const char *key, const size_t nkey, const uint32_t hv) {
    item **pos;
    unsigned int oldbucket;

    if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        pos = &old_hashtable[oldbucket];
    } else {
        pos = &primary_hashtable[hv & hashmask(hashpower)];
    }

    while (*pos && ((nkey != (*pos)->nkey) || memcmp(key, ITEM_key(*pos), nkey))) {
        pos = &(*pos)->h_next;  //注意,这里面很巧妙,这里面pos指的是待寻找item的前面的item的h_next指针的地址,后面用pos的时候实际上用的是(*post)->h_next
    }
    return pos;  //若*pos=NULL则表示没有找到
}

4)assoc_expand函数,用于扩张hash表,新hash表大小是原来的两倍;

static void assoc_expand(void) {
    old_hashtable = primary_hashtable;  //将当前primary_hashtable赋给old_hashtable, primary_hashtable用于保存新的hash表

    primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));  //分配新的内存给primary_hashtable, 大小为之前2倍
    if (primary_hashtable) {
        if (settings.verbose > 1)
            fprintf(stderr, "Hash table expansion starting\n");
        hashpower++;   //hashpower加1,通过hashsize宏定义后为2倍
        expanding = true;  //开始扩展
        expand_bucket = 0;   //扩展进度bucket,设置为0
        STATS_LOCK();   //状态信息,不分析
        stats.hash_power_level = hashpower;
        stats.hash_bytes += hashsize(hashpower) * sizeof(void *);
        stats.hash_is_expanding = 1;
        STATS_UNLOCK();
    } else {
        primary_hashtable = old_hashtable;   //分配不成功,继续使用原来的hash表,不退出继续运行
        /* Bad news, but we can keep running. */
    }
}

5)assoc_start_expand函数,启动hash表扩张。

static void assoc_start_expand(void) {
    if (started_expanding)
        return;

    started_expanding = true; //设置started_expanding
    pthread_cond_signal(&maintenance_cond);  //发送信号给maintence线程开始扩张
}

5)assoc_insert函数,插入一个item进入到hash表中去,这里不是更新,因此肯定没有已存在的数据;

int assoc_insert(item *it, const uint32_t hv) {
    unsigned int oldbucket;

//    assert(assoc_find(ITEM_key(it), it->nkey) == 0);  /* shouldn't have duplicately named things defined */

    //下面跟前面函数内部一样,用于判断是添加到primary_hashtable中还是old_hashtable中,无论是什么情况,都将该item加到该hash值对应的item链表的开头
    if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        it->h_next = old_hashtable[oldbucket];
        old_hashtable[oldbucket] = it;  
    } else {
        it->h_next = primary_hashtable[hv & hashmask(hashpower)];
        primary_hashtable[hv & hashmask(hashpower)] = it;
    }

    pthread_mutex_lock(&hash_items_counter_lock);
    hash_items++;
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {   //如果不在扩张,且当前item的数量已经超过了当前hash值的1.5倍,则调用assoc_start_expand函数启动扩张程序
        assoc_start_expand();
    }
    pthread_mutex_unlock(&hash_items_counter_lock);

    MEMCACHED_ASSOC_INSERT(ITEM_key(it), it->nkey, hash_items);
    return 1;
}

6)assoc_delete函数,用于删除hash表中某个item指针;

void assoc_delete(const char *key, const size_t nkey, const uint32_t hv) {
    item **before = _hashitem_before(key, nkey, hv);
    //这里找到的是key对应的item的前驱p_item->h_next的地址
    //也即是(*before) = p_item->h_next = c_item

    if (*before) {
        item *nxt;
        pthread_mutex_lock(&hash_items_counter_lock);
        hash_items--;
        pthread_mutex_unlock(&hash_items_counter_lock);
        /* The DTrace probe cannot be triggered as the last instruction
         * due to possible tail-optimization by the compiler
         */
        MEMCACHED_ASSOC_DELETE(key, nkey, hash_items);
        nxt = (*before)->h_next;
        (*before)->h_next = 0;   /* probably pointless, but whatever. */
        *before = nxt;
        //这几句可以这么翻译
        //nxt = (*before)->h_next = p_item->h_next->h_next = c_item->h_next;
        //*before = p_item->h_next = c_item = nxt;
        //这样就从链表中去掉了c_item
        return;
    }
    /* Note:  we never actually get here.  the callers don't delete things
       they can't find. */
    assert(*before != 0);
}

7)start_assoc_maintenance_thread函数,用于开启一个maintenance线程;

int start_assoc_maintenance_thread() {
    int ret;
    char *env = getenv("MEMCACHED_HASH_BULK_MOVE");//获取环境变量
    if (env != NULL) {
        hash_bulk_move = atoi(env);//hash单次移动的块数,默认为1
        if (hash_bulk_move == 0) {
            hash_bulk_move = DEFAULT_HASH_BULK_MOVE;
        }
    }
    pthread_mutex_init(&maintenance_lock, NULL);//初始化互斥锁maintenance_lock,全局锁
    if ((ret = pthread_create(&maintenance_tid, NULL,
                              assoc_maintenance_thread, NULL)) != 0) { //创建线程,线程id为maintenance_tid, 回调函数为assoc_maintenance_thread
        fprintf(stderr, "Can't create thread: %s\n", strerror(ret));
        return -1;
    }
    return 0;
}

8)assoc_maintenance_thread函数,assoc的maintenance线程的回调函数,主要用来进行处理扩张;

static void *assoc_maintenance_thread(void *arg) {

    mutex_lock(&maintenance_lock);   //加锁
    while (do_run_maintenance_thread) { //确定是否执行maintenance过程,do_run_maintenance_thread默认是1
        int ii = 0;

        /* There is only one expansion thread, so no need to global lock. */
        for (ii = 0; ii < hash_bulk_move && expanding; ++ii) {
            //前提是expanding置位,一次移动hash_bulk_move块
            item *it, *next;
            int bucket;
            void *item_lock = NULL;

            /* bucket = hv & hashmask(hashpower) =>the bucket of hash table
             * is the lowest N bits of the hv, and the bucket of item_locks is
             *  also the lowest M bits of hv, and N is greater than M.
             *  So we can process expanding with only one item_lock. cool! */
            if ((item_lock = item_trylock(expand_bucket))) {
                    //下面是移动某个bucket的全部item,这里面看顺序会调整,但这个不妨碍查询
                    for (it = old_hashtable[expand_bucket]; NULL != it; it = next) {
                        next = it->h_next;
                        bucket = hash(ITEM_key(it), it->nkey) & hashmask(hashpower);
                        it->h_next = primary_hashtable[bucket];
                        primary_hashtable[bucket] = it;
                    }

                    old_hashtable[expand_bucket] = NULL; //原来hash表的bucket置空

                    expand_bucket++; //扩展的bucket标记
                    if (expand_bucket == hashsize(hashpower - 1)) { //如果当前已经扩展完毕
                        expanding = false;  //将expanding归位
                        free(old_hashtable);  //释放原来的内存空间
                        STATS_LOCK();   //状态信息不分析
                        stats.hash_bytes -= hashsize(hashpower - 1) * sizeof(void *);
                        stats.hash_is_expanding = 0;
                        STATS_UNLOCK();
                        if (settings.verbose > 1)
                            fprintf(stderr, "Hash table expansion done\n");
                    }

            } else {
                usleep(10*1000);   //如果暂停10秒
            }

            if (item_lock) {
                item_trylock_unlock(item_lock); //bucket扩展完毕释放锁
                item_lock = NULL;
            }
        }

        if (!expanding) {    //如果已经扩展完毕或者尚未有扩展,会阻塞在这里等待条件锁触发,这里是通过assoc_start_expand中实现触发,而assoc_start_expand是由当item的数目超过哈希表的1.5倍时调用,这里面只负责修改started_expanding标志,真正扩张的时候是expanding标记,有这两种情况的区分是因为下面的原因:
            /* We are done expanding.. just wait for next invocation */
            started_expanding = false;
            pthread_cond_wait(&maintenance_cond, &maintenance_lock);
            /* assoc_expand() swaps out the hash table entirely, so we need
             * all threads to not hold any references related to the hash
             * table while this happens.
             * This is instead of a more complex, possibly slower algorithm to
             * allow dynamic hash table expansion without causing significant
             * wait times.
             */
            //上面解释很清楚,assoc_expand中是要讲hash表整体交换,因此需要所有线程都停止操作哈希表,为了简单采用的这种方法,可能需要消耗时间,因此需要两个标记
            pause_threads(PAUSE_ALL_THREADS);
            assoc_expand();
            pause_threads(RESUME_ALL_THREADS);
        }
    }
    return NULL;
}

9)stop_assoc_maintenance_thread函数,停止assoc的maintenance线程,程序里面是在主程序退出的时候调用。

void stop_assoc_maintenance_thread() {
    mutex_lock(&maintenance_lock);
    do_run_maintenance_thread = 0;    //停止执行maintenance线程
    pthread_cond_signal(&maintenance_cond);  //发送信号,避免线程阻塞
    mutex_unlock(&maintenance_lock);

    /* Wait for the maintenance thread to stop */
    pthread_join(maintenance_tid, NULL);   //等待线程退出
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值