redis 数据结构基础 (三) hash表
hash表是最常见的数据结构,有趣的是,虽然hash函数被如此多的地方所使用,然而hash函数的原理却是一个不折不扣的数学问题。
http://blog.csdn.net/youyiyang/article/details/48522979 是一篇介绍hash函数很好的文章,非常值得一读。本文仅仅涉及hash函数的实现和应用。
hash函数本质上是一种单向函数映射,即y = f(x)的计算,当x确定时,可以得出唯一的y值,然而,当y确定时,却不一定能得到x值,就算你知道f(x)的形式(就是hash算法)也不可以(比如5次方程)
通常情况下,hash函数可以将任意的数据映射成固定的
在工程应用中,hash函数最常见的有以下运用场景:
1、数据校验: 有点类似于CRC32冗余校验,比如传输文件后,可以通过hash算法来计算出文件的hash值,再和文件的发送方公布的hash值进行对比
2、单向性运用: 最常见的场合是密码存储,服务器端存储用户的密码,实际上存储的是用户密码的hash值,这样,就算破解得到了服务器方的密码库,也无法反推出用户的明文密码。
3、搜索:比如C++11所提供的新的unordered_map,在处理查找算法时,据称效率比原有的map类速度提高了5倍,不过相比较map而言,其劣势在于每个key 不支持 operator < 运算,存储的数据是无序的,不能进行范围查找。
显然, hash函数在redis中主要用的是第三种用途,比较坑的是,redis中的hash相关的文件有2个,都叫做dict.c & dict.h, 里面的很多接口也长的比较像,后来才明白,一个用作客户端,一个用作服务器端
redis中的hash函数不是一个,而是由多个组成的,我们看一个最简单的:
1 static unsigned int dictGenHashFunction(const unsigned char *buf, int len) { 2 unsigned int hash = 5381; 3 4 while (len--) 5 hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */ 6 return hash; 7 }
上述算法又被称为djb hash算法,这个算法有以下特点:
1、只涉及位移运算和加法运算,时间复杂度同len成正比,执行速度那是杠杠的
2、对字符串的效果非常好,(不要问我为什么是33,我猜测是个经验值),6个字符以内的小写字母组成的字符串可以做到完全不冲突
3、从算法中我们也可以得出,有hash值反推原始字符串是不可能的
掌握了以上的hash函数,基本上,就没多大问题了。不过,redis中的hash函数可不止这一个,还有一个,就是MurmurHash64A函数,这个就是大名鼎鼎的MurmurHash函数,这个函数是由Austin Appleby创建的,这位大神后来也去了Google,目前Redis中使用的版本是MurmurHash2的64位版本,而MurmurHash函数目前已经发展到了V3,据说效率比v2要高一些。遗憾的是,仅有32bit和128bit两个版本。
下面给出MurmurHash64的源代码,算法具体是什么原理,我也不清楚
1 uint64_t MurmurHash64A (const void * key, int len, unsigned int seed) { 2 const uint64_t m = 0xc6a4a7935bd1e995; 3 const int r = 47; 4 uint64_t h = seed ^ (len * m); 5 const uint8_t *data = (const uint8_t *)key; 6 const uint8_t *end = data + (len-(len&7)); 7 8 while(data != end) { 9 uint64_t k; 10 11 #if (BYTE_ORDER == LITTLE_ENDIAN) 12 #ifdef USE_ALIGNED_ACCESS 13 memcpy(&k,data,sizeof(uint64_t)); 14 #else 15 k = *((uint64_t*)data); 16 #endif 17 #else 18 k = (uint64_t) data[0]; 19 k |= (uint64_t) data[1] << 8; 20 k |= (uint64_t) data[2] << 16; 21 k |= (uint64_t) data[3] << 24; 22 k |= (uint64_t) data[4] << 32; 23 k |= (uint64_t) data[5] << 40; 24 k |= (uint64_t) data[6] << 48; 25 k |= (uint64_t) data[7] << 56; 26 #endif 27 28 k *= m; 29 k ^= k >> r; 30 k *= m; 31 h ^= k; 32 h *= m; 33 data += 8; 34 } 35 36 switch(len & 7) { 37 case 7: h ^= (uint64_t)data[6] << 48; 38 case 6: h ^= (uint64_t)data[5] << 40; 39 case 5: h ^= (uint64_t)data[4] << 32; 40 case 4: h ^= (uint64_t)data[3] << 24; 41 case 3: h ^= (uint64_t)data[2] << 16; 42 case 2: h ^= (uint64_t)data[1] << 8; 43 case 1: h ^= (uint64_t)data[0]; 44 h *= m; 45 }; 46 47 h ^= h >> r; 48 h *= m; 49 h ^= h >> r; 50 return h; 51 }
这个算法由以下的特点:
1、涉及到了位移,乘法、异或三种运算,
2、提供了一个种子数,显然,种子数对hash值有重大的影响
3、高性能、低碰撞, 据google公司的实际测验的结果表明,在10位以上的字符串hash过程中,效率比djb算法更高。(所以说,效率这个东西,一定要测试才来的准,没有性能测试的软件都是~~~)
2011年,Google公司受到murmurhash算法的启发,又发明了cityhash算法,据称cityhash算法效率较murmur算法效率更高,利用了CPU的一些指令提高了算法的效率。
city算法更加复杂,而且不是redis所采用的hash算法,此处就此略过。
此外,redis还有一个hash函数,
下面再来看redis中,dict(字典)的源代码:
在redis中,服务器端的代码dict.c和客户端的dict.c的代码逻辑差不了太多,dict的代码复杂度明显要高一些,下面我们来看相关代码:
redis中,字典中存储每个键值对的数据结构叫做dictEntry:
1 typedef struct dictEntry { 2 void *key; 3 union { 4 void *val; 5 uint64_t u64; 6 int64_t s64; 7 double d; 8 } v; 9 struct dictEntry *next; 10 } dictEntry;
各个成员的意思很明白,其中,next是为了出现碰撞冲突而设定的,目的是将所有hash'值相同的键值映射到一个单向链表上去,同时,新增一个节点时,会放在该节点的首部
有了键值对的数据结构之后,我们再来看字典数据结构:
1 typedef struct dictht { 2 dictEntry **table; 3 unsigned long size; 4 unsigned long sizemask; 5 unsigned long used; 6 } dictht;
其中,table是一个可以理解为一个数组,数组中的每一个元素是一个链表。
size是dictht中存储的键值对的数量
下面2个变量比较麻烦,往下看之前,请先深呼吸.
sizemask = 4*(2n)-1, 我们可以很容易的想象出,sizemask的二进制值的形式为(111111...111),采用这个值,是在计算完hash值之后,需要将hash值映射到数组的某个链表上,
该链表在数组中的索引,就是由hash&sizemask来确定的,不难得出,table数组的元素数量(其实就是hash算法中桶的数量)就是sizemask+1, 这里采用&运算,想必是因为&运算效率比取模运算的效率要来的高。
1 static int _dictKeyIndex(dict *d, const void *key, unsigned int hash, dictEntry **existing) 2 { 3 unsigned int idx, table; 4 dictEntry *he; 5 if (existing) *existing = NULL; 6 7 /* Expand the hash table if needed */ 8 if (_dictExpandIfNeeded(d) == DICT_ERR) 9 return -1; 10 for (table = 0; table <= 1; table++) { 11 idx = hash & d->ht[table].sizemask; 12 /* Search if this slot does not already contain the given key */ 13 he = d->ht[table].table[idx]; 14 while(he) { 15 if (key==he->key || dictCompareKeys(d, key, he->key)) { 16 if (existing) *existing = he; 17 return -1; 18 } 19 he = he->next; 20 } 21 if (!dictIsRehashing(d)) break; 22 } 23 return idx; 24 }
第13行就是获取索引的代码, 从上面的代码中,我们可以看出,在一个dict中字典其实有2个,其中,90%以上的情况下,我们只会用到第一个,第二个hash表是在对dict中的元素进行重新布局时才会用到。
hash字典为何需要重新布局,什么情况下会重新布局?
字典中的元素一般情况下,数量是会逐渐变化的,因此,table数组的size,会随着 元素的数量的变化而变化,我们来看下面的代码
1 /* Expand or create the hash table */ 2 int dictExpand(dict *d, unsigned long size) 3 { 4 dictht n; /* the new hash table */ 5 unsigned long realsize = _dictNextPower(size); ① 6 7 /* the size is invalid if it is smaller than the number of 8 * elements already inside the hash table */ 9 if (dictIsRehashing(d) || d->ht[0].used > size) 10 return DICT_ERR; 11 12 /* Rehashing to the same table size is not useful. */ 13 if (realsize == d->ht[0].size) return DICT_ERR; 14 15 /* Allocate the new hash table and initialize all pointers to NULL */ 16 n.size = realsize; ② 17 n.sizemask = realsize-1; 18 n.table = zcalloc(realsize*sizeof(dictEntry*)); 19 n.used = 0; 20 21 /* Is this the first initialization? If so it's not really a rehashing 22 * we just set the first hash table so that it can accept keys. */ 23 if (d->ht[0].table == NULL) { 24 d->ht[0] = n; 25 return DICT_OK; 26 } 27 28 /* Prepare a second hash table for incremental rehashing */ 29 d->ht[1] = n; ③ 30 d->rehashidx = 0; 31 return DICT_OK; 32 }
上述代码中,①处计算出大于等于size的最小的2的N次方值 ②处设置字典的基本参数 ③处建立临时字典,临时字典是扩张后的字典,即d->ht[1], 由标记位rehashidx来表示其有效性(rehashidx值为-1时表示其无效)
注意在redis中, rehash的过程不是一蹴而就的,而是渐进式的,如果一次性的将太多的键值rehash,占用CPU过多,会对其性能造成影响
那么如何进行rehash呢,代码在server.c中 的incrementallyRehash函数中,我们忽略其代码实现,仅仅看一下注释:
1 int dictRehashMilliseconds(dict *d, int ms) { 2 long long start = timeInMilliseconds(); 3 int rehashes = 0; 4 5 while(dictRehash(d,100)) { 6 rehashes += 100; 7 if (timeInMilliseconds()-start > ms) break; 8 } 9 return rehashes; 10 }
incrementallyRehash函数在后台任务中被调用,可以参阅databasesCron函数,包括rdb存储的一些操作也会在该函数中运行
在redis的readme.md文件中由如下描述:
1 /* Expand the hash table if needed */ 2 static int _dictExpandIfNeeded(dict *d) 3 { 4 /* Incremental rehashing already in progress. Return. */ 5 if (dictIsRehashing(d)) return DICT_OK; 6 7 /* If the hash table is empty expand it to the initial size. */ 8 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); 9 10 /* If we reached the 1:1 ratio, and we are allowed to resize the hash 11 * table (global setting) or we should avoid it but the ratio between 12 * elements/buckets is over the "safe" threshold, we resize doubling 13 * the number of buckets. */ 14 if (d->ht[0].used >= d->ht[0].size && 15 (dict_can_resize || 16 d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) 17 { 18 return dictExpand(d, d->ht[0].used*2); 19 } 20 return DICT_OK; 21 }
上述代码中的第14-16,确定了rehash的条件:
条件是根据负载因子来的, 负载因子由used/size来确定,负载因子的值是5时会强制rehash,负载因子>=1的时候,此时若是没有rdb或者aof子进程,则可以进行rehash
除了在后台偷偷的搬运键值对之外,redis同时,也在读取、修改、添加,删除某个键的时候,顺便进行搬迁。我们以添加操作为例,顺便引出对hash表进行操作的基本流程:
1 dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) 2 { 3 int index; 4 dictEntry *entry; 5 dictht *ht; 6 7 if (dictIsRehashing(d)) _dictRehashStep(d); 8 9 /* Get the index of the new element, or -1 if 10 * the element already exists. */ 11 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) 12 return NULL; 13 14 /* Allocate the memory and store the new entry. 15 * Insert the element in top, with the assumption that in a database 16 * system it is more likely that recently added entries are accessed 17 * more frequently. */ 18 ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; 19 entry = zmalloc(sizeof(*entry)); 20 entry->next = ht->table[index]; 21 ht->table[index] = entry; 22 ht->used++; 23 24 /* Set the hash entry fields. */ 25 dictSetKey(d, entry, key); 26 return entry; 27 }
以上代码,第7行移动了一个键值对,而后判定是否在rehashing中,如果在rehashing中,那么直接将新元素插入到ht[1]中去,否则插入到ht[0]中去
其它类似函数,如Delete、Find等操作,限于篇幅,不解释
本文基本上解释清楚了hash表的工作原理,但是,hash表作为redis中最重要的数据结构,还有其它值得一读的地方,这就是dictScan函数,下一篇就来分析它