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不至于过于冲突,避免耗时操作。这个线程中主要的逻辑过程如下:
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); //等待线程退出
}