assoc.c

/*
  
maintenance_cond:
    用于激活睡眠的hash表维护线程
hash表维护线程:
    主要用于实现对hash表的扩充(当item的数量超过桶数的3/2时),并控制数据的逐步迁移
数据的逐步迁移:
    为了避免在迁移的时候worker线程增删哈希表,所以要在数据迁移的时候加锁,worker线程抢到了锁才能增删查找哈希表。memcached为了实现快速响应(即worker线程能够快速完成增删查找操作),就不能让迁移线程占锁太久。但数据迁移本身就是 一个耗时的操作,这是一个矛盾。memcached为了解决这个矛盾,就采用了逐步迁移的方法。其做法是,在一个循环里面:加锁-》只进行小部分数据的迁移-》解锁。这样做的效果是:虽然迁移线程会多次抢占锁,但每次占有锁的 时间都是很短的,这就增加了worker线程抢到锁的概率,使得worker 线程能够快速完成它的操作。一小部分是多少个item呢?前面说到的全局变量hash_bulk_move就指明是多少个桶的item, 默认值是1个桶,后面为了方便叙述也就认为hash_bulk_move的值为1。 逐步迁移的具体做法是,调用assoc_expand函数申请一个新的更大的 哈希表,每次只迁移旧哈希表一个桶的item到新哈希表,迁移完一桶就释放锁。此时就要求有一个旧哈希表和新哈希表。在memcached实现里面,用primary_hashtable表示新表(也有一些博文称之为主表),old_hashtable表示旧表(副表)。
old_hashtable表示旧表(副表):
    存放还没有转移到primary_hashtable中的数据item,注意:数据转移过程中,都是链表的节点的转移,并不会释放任何空间,只有当数据全部转移完成后,需要free(old_hashtable)
*/
assoc.c

#include "memcached.h"
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/signal.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>

static pthread_cond_t maintenance_cond = PTHREAD_COND_INITIALIZER;

typedef  unsigned long  int  ub4;   /* unsigned 4-byte quantities */
typedef  unsigned       char ub1;   /* unsigned 1-byte quantities */

/* how many powers of 2's worth of buckets we use */
unsigned int hashpower = HASHPOWER_DEFAULT;//hash table 的桶大小永远是2的倍数,且按2的倍数扩增,可以通过设置参数来改变哈希表中桶的数量

#define hashsize(n) ((ub4)1<<(n))// 桶的大小
#define hashmask(n) (hashsize(n)-1)// 主要用于取模 a % (2^n) = a & (2^n - 1)

/* Main hash table. This is where we look except during expansion. */
static item** primary_hashtable = 0;//哈希表数组指针 

/*
 * Previous hash table. During expansion, we look here for keys that haven't
 * been moved over to the primary yet.
 */
static item** old_hashtable = 0;
/*桶扩增后,旧的哈希表的指针。因为当hash表扩展后,并没有一次性把原来hash表中
的数据全部都转移到新的hash表中,而是一次转移一个桶中的所有item数据到新hash表
中。(原因是对hash表的操作会加锁,这时其他线程就不能对hash表进行操作,包
括work线程向hash表中插入item,为了缩短)*/

/* Number of items in the hash table. */
static unsigned int hash_items = 0;//目前表内的节点个数

/* Flag: Are we in the middle of expanding now? */
static bool expanding = false;//旧桶数据是否清理
static bool started_expanding = false;//是否开始扩增桶

/*
 * During expansion we migrate values with bucket granularity; this is how
 * far we've gotten so far. Ranges from 0 .. hashsize(hashpower - 1) - 1.
 */
static unsigned int expand_bucket = 0;//旧桶清理到那个位置,每次清理 2^expand_bucket 个,expand_bucket 递增

//默认参数值为0。本函数由main函数调用,参数的默认值为0
//初始化主要做一件事: 申请桶的内存。
void assoc_init(const int hashtable_init) {
    if (hashtable_init) {
        hashpower = hashtable_init;
    }
	//因为哈希表会慢慢增大,所以要使用动态内存分配。哈希表存储的数据是一个  
    //指针,这样更省空间。  
    //hashsize(hashpower)就是哈希表的长度了
    primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));
    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();
}




/*由于 key-value 的储存需要保证 相同的key 对应唯一的 value.
这样也就代表 一个 key 是唯一的,就和我的 hash table 的 val 是一个性质的。
nkey 代表 这个 key 的字符串长度。
hv 代表 这个 字符串 key 的 hash 值。

expanding 代表是否有旧桶,有的话我们需要先判断当前 key 是在新桶还是旧桶里面。
怎么判断呢?
新桶范围是 0~ 2^hashpower, 插入到新桶的值的范围是 0 ~ expand_bucket
旧桶范围是 0 ~ 2^(hashpower - 1),旧桶的值范围是 expand_bucket ~ 2^(hashpower - 1)

这时可能就会有人说不对呀,那 对于 2^(hashpower - 1) ~ 2^hashpower 的数据在哪呢?
其实,那些数据超过了 2^(hashpower - 1), 所以会进行取模,这样就还在那个范围了。
什么意思呢?
对于新来的数据,只看范围,如果在 expand_bucket ~ 2^(hashpower - 1), 即使有新桶还会存在旧桶里。

it 指针指向当前 key 对应的桶的位置。
然后就可以循环判断了。
由于是内存比较,所以需要先比较长度,再比较内存,完全相同了就找到了。*/
	//由于哈希值只能确定是在哈希表中的哪个桶(bucket),但一个桶里面是有一条冲突链的	
	//此时需要用到具体的键值遍历并一一比较冲突链上的所有节点。因为key并不是以'\0'结尾  
	//的字符串,所以需要另外一个参数nkey指明这个key的长度 

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

    // 得到相应的桶, bucket
    if (expanding &&//正在扩展哈希表
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)//该item还在旧表里面 
    {
        it = old_hashtable[oldbucket];
    } else {
    //由哈希值判断这个key是属于那个桶(bucket)的
        it = primary_hashtable[hv & hashmask(hashpower)];
    }

    // 在桶里搜索目标
     //到这里,已经确定这个key是属于那个桶的。 遍历对应桶的冲突链即可
    item *ret = NULL;
    int depth = 0;
    while (it) {
		//长度相同的情况下才调用memcmp比较,更高效
        if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {
            ret = it;
            break;
        }
        it = it->h_next;
        ++depth;
    }
    MEMCACHED_ASSOC_FIND(key, nkey, depth);
    return ret;
}

/* returns the address of the item pointer before the key.  if *item == 0,
   the item wasn't found */
	//查找item。返回前驱节点的h_next成员地址,如果查找失败那么就返回冲突链中最后  
	//一个节点的h_next成员地址。因为最后一个节点的h_next的值为NULL。通过对返回值  
	//使用 * 运算符即可知道有没有查找成功。
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)];//找到哈希表中对应的桶位置
    }
	//到这里已经确定了要查找的item是属于哪个表的了,并且也确定了桶位置。遍历对应桶的冲突链即可 
	//遍历桶的冲突链查找item
    while (*pos && ((nkey != (*pos)->nkey) || memcmp(key, ITEM_key(*pos), nkey))) {
        pos = &(*pos)->h_next;
    }
	 //*pos就可以知道有没有查找成功。如果*pos等于NULL那么查找失败,否则查找成功。
    return pos;
}

/* grows the hashtable to the next power of 2. */
//申请更大的哈希表,并将expanding设置为true

static void assoc_expand(void) {
    old_hashtable = primary_hashtable;
	//申请一个新哈希表,并用old_hashtable指向旧哈希表
    primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
    if (primary_hashtable) {
        if (settings.verbose > 1)
            fprintf(stderr, "Hash table expansion starting\n");
        hashpower++;
        expanding = true;//标明已经进入扩展状态  
        expand_bucket = 0;//从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;
        /* Bad news, but we can keep running. */
    }
}

/*迁移线程被创建后会进入休眠状态(通过等待条件变量),当worker线程插入item后,发现需要扩展哈希表就会调用assoc_start_expand函数唤醒这个迁移线程。*/
//assoc_insert函数会调用本函数,当item数量到了哈希表表长的1.5倍才会调用的

static void assoc_start_expand(void) {
    if (started_expanding)
        return;
    started_expanding = true;
    pthread_cond_signal(&maintenance_cond);
}

/*添加比较简单,实现方式和我的差不多,插在链表头部。
这里多了一步桶大小的检测,节点个数超过当前桶大小的 1.5 倍时就增大桶(调用启动增大桶线程)。 
这里要注意的一点是对于插入的key,已经在其他地方检察过是否存在了。
意思就是这里保证一定不存在。*/
/* Note: this isn't an assoc_update.  The key must not already exist to call this */
int assoc_insert(item *it, const uint32_t hv) {//hv是这个item键值的哈希值
    unsigned int oldbucket;

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

    // 头插法
    if (expanding &&//目前处于扩展hash表状态
        (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;
    }

    hash_items++;//哈希表的item数量加一

    //当hash表的item数量到达了hash表容量的1.5倍时,就会进行扩展  
    //当然如果现在正处于扩展状态,是不会再扩展的  
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
        assoc_start_expand();
    }

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





/*删除也比较简单,先找到需要删除的那个节点的父节点,然后删除即可。
需要注意的是这里也是保证要删除的节点已经存在。
另外大家不能理解的是为什么要用指向指针的指针。
这个问题曾经在 segmentfault 上有人问过这个问题,不过他那个问题就没有办法使用指向指针的指针了。

问题就是,如果不使用指向指针的指针,查找的节点不在第一个位置的话,可以正常操作。
但是在第一个位置的话,我们的操作不会生效的。*/
void assoc_delete(const char *key, const size_t nkey, const uint32_t hv) {
    // 寻找到需要删除节点的前一个节点, 这是链表删除的经典操作
    item **before = _hashitem_before(key, nkey, hv);//得到前驱节点的h_next成员地址

    if (*before) {//查找成功 
        item *nxt;
        hash_items--;
        /* The DTrace probe cannot be triggered as the last instruction
         * due to possible tail-optimization by the compiler
         */

		//因为before是一个二级指针,其值为所查找item的前驱item的h_next成员地址.  
        //所以*before指向的是所查找的item.因为before是一个二级指针,所以  
        //*before作为左值时,可以给h_next成员变量赋值。所以下面三行代码是  
        //使得删除中间的item后,前后的item还能连得起来。
        MEMCACHED_ASSOC_DELETE(key, nkey, hash_items);
        nxt = (*before)->h_next;
        (*before)->h_next = 0;   /* probably pointless, but whatever. */
        *before = nxt;
        return;
    }
    /* Note:  we never actually get here.  the callers don't delete things
       they can't find. */
    assert(*before != 0);
}


static volatile int do_run_maintenance_thread = 1;

#define DEFAULT_HASH_BULK_MOVE 1
int hash_bulk_move = DEFAULT_HASH_BULK_MOVE;//指向待迁移的桶







/*迁移线程:
        扩展哈希表有一个很大的问题:扩展后哈希表的长度变了,
        item哈希后的位置也是会跟着变化的(回忆一下memcached是
        怎么根据键值的哈希值确定桶的位置的)。所以如果要扩展哈
        希表,那么就需要对哈希表中所有的item都要重新计算哈希
        值得到新的哈希位置(桶位置),然后把item迁移到新的桶上。
        对所有的item都要做这样的处理,所以这必然是一个耗时的
        操作。后文会把这个操作称为数据迁移。
        因为数据迁移是一个耗时的操作,所以这个工作由一个专门的
        线程(姑且把这个线程叫做迁移线程吧)负责完成。这个迁移线
        程是由main函数调用一个函数创建的。*/
  //do_run_maintenance_thread是全局变量,初始值为1,在stop_assoc_maintenance_thread  
    //函数中会被赋值0,终止迁移线程  



/*逐步迁移数据:
        为了避免在迁移的时候worker线程增删哈希表,所以要在数据
        迁移的时候加锁,worker线程抢到了锁才能增删查找哈希表。
        memcached为了实现快速响应(即worker线程能够快速完成增删
        查找操作),就不能让迁移线程占锁太久。但数据迁移本身就是
        一个耗时的操作,这是一个矛盾。
        memcached为了解决这个矛盾,就采用了逐步迁移的方法。其
        做法是,在一个循环里面:加锁-》只进行小部分数据的迁移-》解锁
        。这样做的效果是:虽然迁移线程会多次抢占锁,但每次占有锁的
        时间都是很短的,这就增加了worker线程抢到锁的概率,使得worker
        线程能够快速完成它的操作。一小部分是多少个item呢?前面说到的
        全局变量hash_bulk_move就指明是多少个桶的item,默认值是1个桶,
        后面为了方便叙述也就认为hash_bulk_move的值为1。
        逐步迁移的具体做法是,调用assoc_expand函数申请一个新的更大的
        哈希表,每次只迁移旧哈希表一个桶的item到新哈希表,迁移完一桶
        就释放锁。此时就要求有一个旧哈希表和新哈希表。在memcached实现
        里面,用primary_hashtable表示新表(也有一些博文称之为主表),
        old_hashtable表示旧表(副表)。
        前面说到,迁移线程被创建后就会休眠直到被worker线程唤醒。当迁移
        线程醒来后,就会调用assoc_expand函数扩大哈希表的表长。*/
static void *assoc_maintenance_thread(void *arg) {

	//do_run_maintenance_thread是全局变量,初始值为1,
	//在stop_assoc_maintenance_thread函数中会被赋值0,
	//终止迁移线程
    while (do_run_maintenance_thread) {
        int ii = 0;
		//上锁	
        /* Lock the cache, and bulk move multiple buckets to the new
         * hash table. */
        item_lock_global();
        mutex_lock(&cache_lock);


		//进行item迁移
		 //hash_bulk_move用来控制每次迁移,移动多少个桶的item。默认是一个.  
        //如果expanding为true才会进入循环体,所以迁移线程刚创建的时候,并不会进入循环体 
        for (ii = 0; ii < hash_bulk_move && expanding; ++ii) {
            item *it, *next;
            int bucket;

			 //在assoc_expand函数中expand_bucket会被赋值0  
            //遍历旧哈希表中由expand_bucket指明的桶,将该桶的所有item  
            //迁移到新哈希表中。
            for (it = old_hashtable[expand_bucket]; NULL != it; it = next) {
                next = it->h_next;

				//重新计算新的哈希值,得到其在新哈希表的位置 
                bucket = hash(ITEM_key(it), it->nkey, 0) & hashmask(hashpower);
				//将这个item插入到新哈希表中
				it->h_next = primary_hashtable[bucket];
                primary_hashtable[bucket] = it;
            }

			//不需要清空旧桶。直接将冲突链的链头赋值为NULL即可
            old_hashtable[expand_bucket] = NULL;

			//迁移完一个桶,接着把expand_bucket指向下一个待迁移的桶 
            expand_bucket++;
            if (expand_bucket == hashsize(hashpower - 1)) {//全部数据迁移完毕
                expanding = false;//将扩展标志设置为false
                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");
            }
        }

		//遍历完hash_bulk_move个桶的所有item后,就释放锁
        mutex_unlock(&cache_lock);
        item_unlock_global();

        if (!expanding) {//不需要迁移数据(了)。
            /* finished expanding. tell all threads to use fine-grained locks */
			//进入到这里,说明已经不需要迁移数据(停止扩展了)。
			switch_item_lock_type(ITEM_LOCK_GRANULAR);
            slabs_rebalancer_resume();
            /* We are done expanding.. just wait for next invocation */
            mutex_lock(&cache_lock);
            started_expanding = false;//重置
            //挂起迁移线程,直到worker线程插入数据后发现item数量已经到了1.5倍哈希表大小,  
            //此时调用worker线程调用assoc_start_expand函数,该函数会调用pthread_cond_signal  
            //唤醒迁移线程
            pthread_cond_wait(&maintenance_cond, &cache_lock);
            /* Before doing anything, tell threads to use a global lock */
            mutex_unlock(&cache_lock);
            slabs_rebalancer_pause();
            switch_item_lock_type(ITEM_LOCK_GLOBAL);
            mutex_lock(&cache_lock);
            assoc_expand();//申请更大的哈希表,并将expanding设置为true
            mutex_unlock(&cache_lock);
        }
    }
    return NULL;
}

static pthread_t maintenance_tid;

int start_assoc_maintenance_thread() {
    int ret;
    char *env = getenv("MEMCACHED_HASH_BULK_MOVE");
    if (env != NULL) {
		//hash_bulk_move的作用在后面会说到。这里是通过环境变量给hash_bulk_move赋值  
        hash_bulk_move = atoi(env);
        if (hash_bulk_move == 0) {
            hash_bulk_move = DEFAULT_HASH_BULK_MOVE;
        }
    }
    if ((ret = pthread_create(&maintenance_tid, NULL,
                              assoc_maintenance_thread, NULL)) != 0) {
        fprintf(stderr, "Can't create thread: %s\n", strerror(ret));
        return -1;
    }
    return 0;
}

void stop_assoc_maintenance_thread() {
    mutex_lock(&cache_lock);
    do_run_maintenance_thread = 0;
    pthread_cond_signal(&maintenance_cond);
    mutex_unlock(&cache_lock);

    /* Wait for the maintenance thread to stop */
    pthread_join(maintenance_tid, NULL);
}







/*回马枪:
        现在再回过头来再看一下哈希表的插入、删除和查找操作,因为这
        些操作可能发生在哈希表迁移阶段。有一点要注意,在assoc.c文件
        里面的插入、删除和查找操作,是看不到加锁操作的。但前面已经
        说了,需要和迁移线程抢占锁,抢到了锁才能进行对应的操作。其
        实,这锁是由插入、删除和查找的调用者(主调函数)负责加的,所
        以在代码里面看不到。
        因为插入的时候可能哈希表正在扩展,所以插入的时候要面临一个选
        择:插入到新表还是旧表?memcached的做法是:当item对应在旧表
        中的桶还没被迁移到新表的话,就插入到旧表,否则插入到新表。下
        面是插入部分的代码。*/



/*这里有一个疑问,为什么不直接插入到新表呢?直接插入到新表对于数据一
致性来说完全是没有问题的啊。网上有人说是为了保证同一个桶item的顺序,
但由于迁移线程和插入线程对于锁抢占的不确定性,任何顺序都不能通过
assoc_insert函数来保证。本文认为是为了快速查找。如果是直接插入到新表,
那么在查找的时候就可能要同时查找新旧两个表才能找到item。查找完一个表,
发现没有,然后再去查找另外一个表,这样的查找被认为是不够快速的。
        如果按照assoc_insert函数那样的实现,不用查找两个表就能找到
item。*/</span>


你可能感兴趣的:(assoc.c)