首先,redis中的哈希表的数据结构是这样的。
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
table成员是一个dictEntry类型的二级指针,为什么是二级指针呢?dictht又是什么类型呢?
size成员是目前哈希表的总大小
sizemask是什么?这个后面会讲到。
used是目前这个哈希表中以及容纳了多少数据。
dictht的类型如下:
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
从dictEntry.next元素中就可以看出,这是一个链表,并且,key和val分别对应哈希表的键和值。因此,可以很清晰地推断除redis是采用开链法来解决哈希冲突的。
回到刚才的问题来,dictht.table成员是个什么东西呢?我这边画了一个图。
这样的话,dictht的结构就很清楚了。
因为我们知道,redis中哈希表的元素可能是非常多的,如果采用常规的rehash的方式,可能会导致进程阻塞,这样就没办法很快的响应用户的请求了。为了解决这个问题,redis采用了渐进式rehash。redis在dictht上又做了一层封装,可以看下面的代码:
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict;
ht[2]表示有两个dictht,这也是渐进式rehash的由来。src/dict.c文件中存在一个私有的函数,这个函数就是用来判断是否需要进行rehash。重点在14行。
/* Expand the hash table if needed */ static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* 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].used*2); // 二倍扩容, } return DICT_OK; }
这个函数的逻辑是这样的,如果如果used >= size,说明负载因子大于1了,并且dict_can_resize标志不为0或者是used超过size的5倍,那么这个时候就进行rehash。这个时候可能还是有点不太清楚,下面再看一段代码和注释就很清楚了。
/* Using dictEnableResize() / dictDisableResize() we make possible to * enable/disable resizing of the hash table as needed. This is very important * for Redis, as we use copy-on-write and don't want to move too much memory * around when there is a child performing saving operations. * * Note that even when dict_can_resize is set to 0, not all resizes are * prevented: a hash table is still allowed to grow if the ratio between * the number of elements and the buckets > dict_force_resize_ratio. */ static int dict_can_resize = 1; static unsigned int dict_force_resize_ratio = 5;
**前面一段表达的意思是 使用dictEnableResize 或者 dictDisableResize函数可以对 全局变量dict_can_resize进行设置。另外还有一点就是,如果redis正在进行持久化的时候,不希望进行resize,这个时候会调用dictDisableResize函数对rehash操作进行抑制。但也不是禁止rehash,如果超过dict_force_resize_ratio设定的阈值,那么这个时候就要强制进行rehash了。因此dict_force_resize_ratio这个变量是一个容忍阈值。**也可以看一下这段英文注释,还是很幽默的哈哈哈哈。
expand函数主要是创建一个新的hashht,并且分配初始值。
/* Expand or create the hash table */ int dictExpand(dict *d, unsigned long size) { /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; dictht n; /* the new hash table */ unsigned long realsize = _dictNextPower(size); // 这个函数就是对4不断乘2,直到超过size后返回,当然需要考虑溢出 /* Rehashing to the same table size is not useful. */ if (realsize == d->ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } /* Prepare a second hash table for incremental rehashing */ d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
_dictNextPower函数返回的是一个realsize,它的值等于2的整数幂,16行中,把realsize的值赋给了size,然后将realsize-1的值赋给了sizemask,到这里,sizemask的用处就比较清楚了。举个例子,比如有一个二进制的值 100000, 减一就得到011111, 那么这个时候用一个k & sizemask就得到哈希槽的索引了。当然,sizemask还有其他的用处,比较复杂。
设置好dictht结构体中成员的初始值之后,就将其赋值给了d->ht[1]了,作为新的hash表,同时rehashidx设置为0,表示这个时候有正在进行渐进式rehash的处理。在这个过程中,ht[0]不会被释放,也不会进行insert插入数据,所有的插入数据会插入到ht[1]中去。redis会在特定的时候对ht[0]中的元素搬运到ht[1]中去,知道ht[0]的used变量为0,这个时候才会进行释放。
以下是这个过程的代码:
/* Performs N steps of incremental rehashing. Returns 1 if there are still * keys to move from the old to the new hash table, otherwise 0 is returned. * * Note that a rehashing step consists in moving a bucket (that may have more * than one key as we use chaining) from the old to the new hash table, however * since part of the hash table may be composed of empty spaces, it is not * guaranteed that this function will rehash even a single bucket, since it * will visit at max N*10 empty buckets in total, otherwise the amount of * work it does would be unbound and the function may block for a long time. */ int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashidx); while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ while(de) { uint64_t 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++; } /* Check if we already rehashed the whole table... */ if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* More to rehash... */ return 1; }
从注释来看,n指的是桶的个数,并且不管桶里面是否有元素,都只遍历n * 10个桶就结束(因为如果很多桶都为空的话,那么可能要遍历很久,会对性能产生影响)。43行的代码就是检查gt[0]是否为空,如果为空,那么就将原来的空间释放掉,将ht[1]赋值为ht[0],rehashidx设置为-1,表示rehash过程结束。
另外还有一点,rehashidx并不是只有0 和 -1这两个值的, rehashidx还用作桶的索引,仔细阅读14行开始的代码就可以发现。