hash表的两个有意思的处理过程为自动调整hash表的大小,在需要时实现增量rehash,其他如查找,删除等不解释。
我们先看看resize的处理逻辑。
dict.c中有个变量dict_can_resize控制着是否允许自动调整hash表的大小,该变量的改变主要靠如下两个函数:
void dictEnableResize(void) { dict_can_resize = 1; } void dictDisableResize(void) { dict_can_resize = 0; }
总的说来,在系统运行有后台进程时,不允许自动自动调整大小,这是为了为了使得类linux系统的copy-on-write有更好的性能(没有调整大小, 就没有rehash,这样父进程的db没有改变,子进程就不需要真的copy)。在后台子进程退出后,又会允许resize。这里说到的后台子进程主要跟redis的持久化机制有关,在后面的持久化之快照和持久化之aof中会详细介绍其过程。
接下来我们看看自动调整大小的算法。
主要是由dictExpand完成。该函数在将size转成最接近2的幂的数后(比如size=60,realsize会等于64,_dictNextPower复杂度为O(log(Size)) ),将相关参数保存在dict中的ht[1]中,并将其rehashidx置为0(置为-1,表示没有rehash进行),这样就可以开始rehash了。
int dictExpand(dict *d, unsigned long size) { dictht n; /* the new hashtable */ unsigned long realsize = _dictNextPower(size); -- n.size = realsize; n.sizemask = realsize-1; n.table = _dictAlloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Initialize all the pointers to NULL */ memset(n.table, 0, realsize*sizeof(dictEntry*)); -- /* Prepare a second hash table for incremental rehashing */ d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
而dictExpand主要被rdbLoadObject、_dictExpandIfNeeded、dictResize调用。
rdbLoadObject表示文件中加载一个object,当加载的是一个set对象时,一开始就将其扩展为set中元素的个数,可以大大减少随后rehash的次数(set用ht实现,而ht的初始值DICT_HT_INITIAL_SIZE = 4)
if (type == REDIS_SET && listlen > DICT_HT_INITIAL_SIZE) dictExpand(o->ptr,listlen);
_dictExpandIfNeeded最终会被dictAdd调用,也就是说在每次向ht中增加一个元素时,都会判断。
_dictExpandIfNeeded的判断策略如下:
/* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling * the number of buckets. */ if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ? d->ht[0].size : d->ht[0].used)*2); }
dict_can_resize就是一开始提到的用于避免后台正在持久化时rehash的全局变量。除了这个变量,当ht桶的元素的平均个数多于 dict_force_resize_ratio=5时,也会强制进行rehash。扩展策略是现有值的2倍。
如果说_dictExpandIfNeeded会将ht增大的话,那么可以认为dictResize会将ht减小。其目标是将桶的个数减少到接近ht中所含元素的个数。
int dictResize(dict *d) { int minimal; if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; minimal = d->ht[0].used; if (minimal < DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE; return dictExpand(d, minimal); }
跟ht的增大过程是在向dict中增加元素时对应,ht的减小过程主要是在删除元素时进行。而redis中的set和zset都是用ht实现的,因此dictResize主要被sremCommand和spopCommand(当dict用于set时)、zremCommand和 zremrangebyscoreCommand和zremrangebyrankCommand(当dict是zset时)、hashDelete调用。除此之外,serverCron(每隔100ms执行一次)会调用tryResizeHashTables,后者会进一步调用dictResize来判断整个redis哪些db需要调整(每个db又是一个大的ht)。
static int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { --- if (server.bgsavechildpid == -1 && server.bgrewritechildpid == -1) { if (!(loops % 10)) tryResizeHashTables(); if (server.activerehashing) incrementallyRehash(); } --- } static void tryResizeHashTables(void) { int j; for (j = 0; j < server.dbnum; j++) { if (htNeedsResize(server.db[j].dict)) dictResize(server.db[j].dict); if (htNeedsResize(server.db[j].expires)) dictResize(server.db[j].expires); } }
而所有dictResize调用是否进行,要看htNeedsResize的返回值。htNeedsResize函数最终反映了redis中减小ht的策略。从其代码可以看出,当填充率不到10%时( REDIS_HT_MINFILL定义为10),会将ht的桶的个数调整到接近ht中元素的个数(参看前面dictResize)。
static int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); return (size && used && size > DICT_HT_INITIAL_SIZE && (used*100/size < REDIS_HT_MINFILL)); }
接下来看下rehash,主要在dictRehash中完成。先看下什么时候进行rehash。
在如上的serverCron中(每100ms执行一次),当没有后台子进程时,会调用incrementallyRehash,最终调用dictRehash。incrementallyRehash会对每个db,rehash大概1ms的时间,这1ms又进一步划分成多步,每步rehash 100个桶。
static void incrementallyRehash(void) { int j; for (j = 0; j < server.dbnum; j++) { if (dictIsRehashing(server.db[j].dict)) { dictRehashMilliseconds(server.db[j].dict,1); break; /* already used our millisecond for this loop... */ } } } int dictRehashMilliseconds(dict *d, int ms) { long long start = timeInMilliseconds(); int rehashes = 0; while(dictRehash(d,100)) { rehashes += 100; if (timeInMilliseconds()-start > ms) break; } return rehashes; }
另外在_dictRehashStep,也会调用dictRehash,而_dictRehashStep每次会rehash一个桶从ht[0]到 ht[1](注:以前写的rehash一个值是错误的),但由于_dictRehashStep是被dictGetRandomKey、dictFind、 dictGenericDelete、dictAdd调用的,因此在每次dict增删查改时都会被调用,这无疑就加快了rehash过程。
我们再来看看rehash过程。dictRehash每次增量rehash n个桶(dictRehash的参数n表示桶的个数),由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0]的各个桶,取得key,然后将该key按ht[1]的桶的大小重新rehash,并将相应的dictEntry从ht[0]移动到ht[1]。在rehash完后,会将ht[0]指向ht[1],然后将ht[1]清空。
int dictRehash(dict *d, int n) { if (!dictIsRehashing(d)) return 0; while(n--) { dictEntry *de, *nextde; /* Check if we already rehashed the whole table... */ if (d->ht[0].used == 0) { _dictFree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++; de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ while(de) { unsigned int h; nextde = de->next; /* Get the index in the new hash table */ h = dictHashKey(d, de->key) & d->ht[1].sizemask; de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used--; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } return 1; }
前面详细了解了rehash的过程,现在可以看看redis为什么使用两个hash表了(ht[0]与ht[1])。
通常,设计hash表时,只有一个ht。在进行rehash时,需要先申请更大内存(比如tmp指向该内存)后,然后一次性把ht中所有的数据rehash到tmp中,然后再让ht执行tmp。也就是说,rehash的粒度是整个ht。
采用两个表后,rehash时的粒度最小可降到一个桶(而每个桶中元素的平均个数为前面提到的dict_force_resize_ratio=5)。很显然,rehash被分散处理了。另外,由于rehash只是移动dictEntry从ht[0]到ht[1],整个使用的内存并没有增加。
最后,简单介绍下在rehash进行时,元素插入、查找、删除的过程(没有rehash时,所有的增删查改都指向ht[0])。
插入会直接插入到ht[1]中。查找、删除会查找两个表ht[0]、ht[1]。
redis中用到的整数hash、字符串hash算法如下,做个备份:
/* Thomas Wang's 32 bit Mix Function */ unsigned int dictIntHashFunction(unsigned int key) { key += ~(key << 15); key ^= (key >> 10); key += (key << 3); key ^= (key >> 6); key += ~(key << 11); key ^= (key >> 16); return key; } /* Generic hash function (a popular one from Bernstein). * I tested a few and this was the best. */ unsigned int dictGenHashFunction(const unsigned char *buf, int len) { unsigned int hash = 5381; while (len--) hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */ return hash; }