四、Redis源码数据结构之哈希表Hash
Redis作为一款内存数据库,解决内存性能问题就显得尤为重要。作为Hash表这种应用数据结构,当数据量大的时候,就会出现2大问题:哈希冲突和rehash开销
一、什么是哈希冲突以及redis如何解决哈希冲突
哈希表是基于数组的一种存储方式.它主要由哈希函数和数组构成。
当要存储一个数据的时候,首先用一个函数计算数据的地址,然后再将数据存进指定地址位置的数组里面。这个函数就是哈希函数,而这个数组就是哈希表。
哈希表的优势在于:相比于简单的数组以及链表,它能够根据元素本身在第一时间,也就是时间复杂度为0(1)内找到该元素的位置。这使得它在查询和删除、插入上会比数组和链表要快很多。因为他们的时间复杂度为o(n)。
哈希冲突是指哈希函数算出来的地址被别的元素占用了,也就是,这个位置有人了。而解决哈希冲突的办法之一就是开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法。
哈希函数用的就是链地址法,也就是数组+链表的方法,hashMap用的就是链地址法.。链地址法就是,当没有发生哈希冲突的时候hashmap主要只有数组。但是当发生冲突的时候,它会在哈希函数找到的当前数组内存地址位置下添加一条链表。
Redis所采用的正是这样链地址法-所谓的链式哈希
相关源码文件:dict.c 和 dict.h
/* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ typedef struct dictht { // 哈希表定义 dictEntry **table; 二维数组 unsigned long size; 表大小 unsigned long sizemask; hash取模的用的 unsigned long used; 元素个数 } dictht; typedef struct dictEntry { // 哈希实体 void *key; key键 union { value值 void *val; uint64_t u64; 无符合 int64_t s64; 有符号 double d; } v; 联合体V struct dictEntry *next; 下一个哈希实体 } dictEntry;
补充一句:伴随着链表的增长性能也就随之变慢了
二、rehash详细介绍以及Redis如何解决rehash造成的性能问题
rehash是在hash表的大小不能满足需求,造成过多hash碰撞后需要进行的扩容hash表的操作,
其实通常的做法确实是建立一个额外的hash表,将原来的hash表中的数据在新的数据中进行重新输入,从而生成新的hash表。
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; 2个hash表,交替使用 long rehashidx; /* rehashing not in progress if rehashidx == -1 */ -1标识无需rehash unsigned long iterators; /* number of iterators currently running */ } dict;
1、触发rehash:
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{ /* Incremental rehashing already in progress. Return. */ 正在进行rehashing 则直接返回 if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ 条件一:如果哈希表为空,将其扩展为初始大小4。 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. */
// 如果我们达到1:1的比例,我们可以调整哈希标的的大小(全局设置)或我们应该避免它但之间的比率元素/桶超过“安全”阈值时,我们将大小加倍桶的数量。
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
条件二: 承载的元素个数大于等于表大小,且允许扩容 dict_can_resize 1
条件三: 承载元素与表大小比率大于安全阈值 dict_force_resize_ratio 5 (负载因子factor) { return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }
void dictEnableResize(void) { dict_can_resize = 1; 启用 } void dictDisableResize(void) { dict_can_resize = 0; 禁止 }
/* This function is called once a background process of some kind terminates, * as we want to avoid resizing the hash tables when there is a child in order * to play well with copy-on-write (otherwise when a resize happens lots of * memory pages are copied). The goal of this function is to update the ability * for dict.c to resize the hash tables accordingly to the fact we have o not * running childs. */ void updateDictResizePolicy(void) { if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) 该调用方法在server.c文件中,条件:没有rdb快照、aof重写场景才启用rehash dictEnableResize(); else dictDisableResize(); } int hasActiveChildProcess() { return server.rdb_child_pid != -1 || server.aof_child_pid != -1; }
2、rehash扩容大小:
dictExpand函数入参:扩容的表,扩容容量
dictExpand(d, DICT_HT_INITIAL_SIZE); DICT_HT_INITIAL_SIZE 4 dictExpand(d, d->ht[0].used*2);
/* 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); /* 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; }
/* Our hash table capability is a power of two */ static unsigned long _dictNextPower(unsigned long size) { unsigned long i = DICT_HT_INITIAL_SIZE; if (size >= LONG_MAX) return LONG_MAX + 1LU; while(1) { if (i >= size) return i; i *= 2; } }
3、rehash执行过程:
int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ .....while(n-- && d->ht[0].used != 0) { 根据变量n,循环拷贝迁移数据 .... 具体见rehashidx变量讲解 } /* Check if we already rehashed the whole table... */ if (d->ht[0].used == 0) { 判断ht[0]数据是否迁移完成 zfree(d->ht[0].table); 迁移完成释放ht[0]内存空间 d->ht[0] = d->ht[1]; 赋值 _dictReset(&d->ht[1]); 重置ht[1]大小为0 d->rehashidx = -1; 全局rehash标识 -1标识rehash结束 return 0; 返回0 所有元素迁移完 } /* More to rehash... */ return 1; ht[0]元素未迁移完 }
rehashidx变量讲解:变量表示当前rehash在对哪个桶bucket做数据迁移,从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; 当检查的空桶(桶中没有元素)数量超过设定的值,停止rehash,转而继续做其他事情,渐进式rehash的由来,从而优化Redis性能 } 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++;
static void _dictRehashStep(dict *d) { if (d->iterators == 0) dictRehash(d,1); // 循环次数为1 从而执行完一次rehash,redis既可以处理其他请求,不会一直占用线程导致主线程阻塞 ---渐进式rehash }
对于C语言不是很感冒 的我也只能这么简单的看下源码,没有太细化,对于hash有兴趣的可以自行查找资料学习下,感谢。
本文来自博客园,作者:chch213,转载请注明原文链接:https://www.cnblogs.com/chch213/p/16260299.html