开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
Redis无需介绍。它是一个非常流行的键值存储,提供了各种内存存储(数据也可以持久化在磁盘上-有后台保存和仅追加文件选项来实现相同的功能),如列表,Set, Sorted Set, String, HyperLogLog, Geospatial结构等。Redis利用内存实现速度,它在一个线程中执行所有操作,尽管AOF操作在一个单独的后台线程中运行。
这篇文章打算解释内部redis哈希表是如何设计的&如何调整它们的大小。这篇文章没有解释如何计算键的哈希值或桶索引。
每个redis数据库实例(数据库的索引从0到最大配置)都有一个与之相关的键空间,它只是哈希表实现的包装器。无论redis存储什么数据,无论是字符串,redis集还是redis哈希,所有数据都保存在哈希表中。下面的代码片段取自redis github存储库,展示了如何定义dict数据类型。结构体dict包含一个包含2个dictht实例的数组。我们一会儿再讲为什么数组中有2个实例。dictht,哈希表实现,其中包含dictEntry的数组(在代码中称为table)。dictEntry是一个链表节点的表示,它包含键、值和指向下一个节点的指针。Dictht还有其他成员,比如- size:哈希表中桶的总数,大小是在创建或扩展哈希表时提供的,sizemask与key的哈希值一起使用,以确定键的正确索引,通常sizemask≤size,使用的成员实际上跟踪哈希表中当前存在的元素总数。
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;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
哈希表dictht的初始大小为4。随着越来越多的密钥进入系统,哈希表的大小也会增加。什么时候redis调整哈希表的大小?Redis可以在以下2种情况下调整哈希表的大小或简单地rehash:
Total_elements / total_buckets = 1并且启用了dict resize。启用或禁用字典大小调整由redis内部处理。Redis试图避免重散列当一些后台进程运行做一些重操作,如保存数据库到磁盘,因为重散列涉及大量的内存页的移动。因此,简单地说,当后台进程运行时,字典大小调整通常是禁用的,否则启用。
Total_elements / total_buckets > 5(强制调整大小比例,强制调整大小完成)
现在有趣的部分来了。正如我已经提到的,redis是单线程的。所以它必须执行像哈希表大小调整和重哈希这样的操作,这样它就不会被阻塞。因为重哈希一个大哈希表总是会阻塞redis,这是不可接受的。
为了实现这一点,redis以增量方式执行重散列操作。对于每个操作,如GET, SET等,redis检查是否需要重新散列。如果需要重新散列,redis会检查是否需要首先扩展散列表。Expand只是调整哈希表的大小。正如我在上面的代码片段中提到的,struct dict包含一个包含2个dictht实例的数组ht,在这里它们出现在画面中。Redis通常将数据存储在第一个dictht实例中(ht[0])。在重哈希时,它会创建一个大小为2次幂的扩展哈希表,其大小仅大于或等于当前哈希表(ht[0])的大小,并且新的哈希表实际上存储在ht[1]实例中。因此,在增量重哈希过程中,redis不断将桶(实际上是一个链表,因为redis在发生碰撞时使用单独的链)从ht[0]移动到ht[1],而不是一次移动所有内容。通过计算当前哈希表中键的位置来确定要移动哪个桶(链接的节点)。结构体dict的成员rehashindex在重散列期间将≥0。Rehashindex只是一个变量,用于遍历哈希表,以确定索引位置是否包含要移动的桶。它从0开始,直到哈希表中的所有索引都被覆盖。读取该成员的值,redis知道是否正在进行重散列。因此,为了在rehashing期间为GET请求服务,redis必须读取这两个哈希表以找出数据。就速度而言,这有点不利。当重哈希完成时,rehashindex被设置为-1,并且驻留在ht[1]实例中的新哈希表被分配回ht[0]。
你可以参考下面的代码片段,它显示了redis表展开和rehash:
/* Resize the table to the minimal size that contains all the elements,
* but with the invariant of a USED/BUCKETS ratio near to <= 1 */
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);
}
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(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;
/* 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;
}
/* 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) {
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++;
}
/* 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;
}