前言字典, 又称符号表(symbol table)、关联数组(associative array)或者映射(map), 是一种用于保存键值对(key-value pair)的抽象数据结构。 在字典中, 一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就被称为键值对。 字典中的每个键都是独一无二的, 程序可以在字典中根据键查找与之关联的值, 或者通过键来更新值, 又或者根据键来删除整个键值对, 等等。 字典经常作为一种数据结构内置在很多高级编程语言里面, 但 Redis 所使用的 C 语言并没有内置这种数据结构, 因此 Redis 构建了自己的字典实现。 字典在 Redis 中的应用相当广泛, 比如 Redis 的数据库就是使用字典来作为底层实现的, 对数据库的增、删、查、改操作也是构建在对字典的操作之上的。 因此,了解字典对我们了解Redis数据库有很大的帮助。同时可以跟Java的HashMap进行对比,看看孰好孰坏。
字典的定义1 typedef struct dict { 2 3 // 类型特定函数 4 dictType *type; 5 6 // 私有数据 7 void *privdata; 8 9 // 哈希表 10 dictht ht[2]; 11 12 // rehash 索引 13 // 当 rehash 不在进行时,值为 -1 14 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ 15 16 } dict; 主要看ht,和rehashidx两个参数。
除了
1 typedef struct dictht { 2 3 // 哈希表数组 4 dictEntry **table; 5 6 // 哈希表大小 7 unsigned long size; 8 9 // 哈希表大小掩码,用于计算索引值 10 // 总是等于 size - 1 11 unsigned long sizemask; 12 13 // 该哈希表已有节点的数量 14 unsigned long used; 15 16 } dictht;
而
1 typedef struct dictEntry { 2 3 // 键 4 void *key; 5 6 // 值 7 union { 8 void *val; 9 uint64_t u64; 10 int64_t s64; 11 } v; 12 13 // 指向下个哈希表节点,形成链表 14 struct dictEntry *next; 15 16 } dictEntry;
可以明显的看出来,redis是通过链表来解决hash冲突的。
因此,redis的字典大概如下:
Rehash随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。 也就是我们常说的,扩容,再次hash。 Redis rehash过程:
但其实rehash是非常的耗时间的。假设ht[0]非常的大呢? 40W,400W,甚至4000W呢? 一次rehash甚至可能导致redis宕机,所以出现了渐进式hash。
渐进式Rehash这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将
扩容代码大致如下: 1 int dictRehash(dict *d, int n) { 2 int empty_visits = n*10; /* Max number of empty buckets to visit. */ 3 4 // 判断是否正在扩容 5 if (!dictIsRehashing(d)) return 0; 6 7 while(n-- && d->ht[0].used != 0) { 8 dictEntry *de, *nextde; 9 10 /* Note that rehashidx can't overflow as we are sure there are more 11 * elements because ht[0].used != 0 */ 12 assert(d->ht[0].size > (unsigned long)d->rehashidx); 13 14 // 找到一个不为空的桶,进行迁移 15 while(d->ht[0].table[d->rehashidx] == NULL) { 16 d->rehashidx++; 17 if (--empty_visits == 0) return 1; 18 } 19 // 找到这个桶第一个指针节点 20 de = d->ht[0].table[d->rehashidx]; 21 // 将这个桶中的所有的key节点转移到新的数组中。while循环链表 22 while(de) { 23 uint64_t h; 24 25 nextde = de->next; 26 /* Get the index in the new hash table */ 27 h = dictHashKey(d, de->key) & d->ht[1].sizemask; 28 de->next = d->ht[1].table[h]; 29 d->ht[1].table[h] = de; 30 d->ht[0].used--; 31 d->ht[1].used++; 32 de = nextde; 33 } 34 // 旧的桶节点置为null,并且rehashidx+1 35 d->ht[0].table[d->rehashidx] = NULL; 36 d->rehashidx++; 37 } 38 39 /* Check if we already rehashed the whole table... */ 40 if (d->ht[0].used == 0) { 41 zfree(d->ht[0].table); 42 d->ht[0] = d->ht[1]; 43 _dictReset(&d->ht[1]); 44 d->rehashidx = -1; 45 return 0; 46 } 47 48 /* More to rehash... */ 49 return 1; 50 }
在进行渐进式 rehash 的过程中, 字典会同时使用 另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到
所遇到问提问题一: 要在字典里面查找一个键的话, 程序会先在 这一点其实我比较有疑惑?为何要先去ht[0]中查找,找不到再去ht[1]中查找,这样岂不是效率低下,查找了两张表。不能根据hash值和rehashidx进行比较判断么?只查一张表不行么? 为此我翻阅了不少资料: 这是redis官方其他人的pull request:https://github.com/antirez/redis/pull/5692 暂时还没有merge 同时这是我和一位大佬的讨论:https://github.com/Junnplus/blog/issues/35 最终得出结论 还是看源码:(源码真是一个好东西) 1 for (table = 0; table <= 1; table++) { 2 // 找到key的index 3 idx = h & d->ht[table].sizemask; 4 // 拿到key值对应的value 5 he = d->ht[table].table[idx]; 6 while(he) { 7 if (key==he->key || dictCompareKeys(d, key, he->key)) 8 return he; 9 he = he->next; 10 } 11 if (!dictIsRehashing(d)) return NULL; 12 } 其实只有两种情况
针对第一种情况,他在第一层的循环已经找到了key值,并且返回(第八行),不再继续后面操作,因此不存在效率问题。 第二种情况,看第五行,he此时为null,根本不会循环链表。然后第二次循环才能找到key。而第一次是做了一次hash,复杂度为O(1)。效率几乎是没有损失,因此也不存在效率问题。 综上:我得出的结论是。可以拿idx和rehashidx进行对比,同时也可以像redis这样写,不会损失效率。redis可能为了代码的简洁以及统一,不想写那么多的判断条件,因此没有比较idx和rehashidx。 当我认为提前结束会更好,毕竟不用后续判断了,也比较清楚。类似这个request:https://github.com/antirez/redis/pull/5692/files
问题二: 假如在redis准备进行rehash的时候,他需要为ht[1]申请一块内存,这块内存可是ht[0]的两倍哦,那次是计算机内存不存会如何? 梳理一下哈希表大小和内存申请大小的对应关系: 正常状态下,redis为ht[1]分配完内存后,会持续一段时间进行rehash。rehash完毕后,就会释放ht[0]内存了。类似如下图: 内存先上升,后下降。
但此时内存不存的话,Redis会进行大量的Key驱逐,也就是会淘汰掉很久未使用的key,跟LRU有点类似。 那么此时可能就会影响到了业务,这是非常大的问题呢。 那么针对在Redis满容驱逐状态下,如何避免因Rehash而导致Redis抖动的这种问题。
参考文献: 《redis设计与实现》 http:///preview/dict/incremental_rehashing.html 《美团针对Redis Rehash机制的探索和实践》 https://tech.meituan.com/2018/07/27/redis-rehash-practice-optimization.html 《Redis源码分析》 https://github.com/Junnplus/blog/issues/35
|
|