- 字典作用
1、数据库
2、哈希键
- 字典实现
哈希表作为底层实现,一个哈希表里面有多个哈希表节点,每个哈希表节点保存了一个字典中的键值对
- 字典结构
字典用dict表示
typedef struct dict {
dictType *type; /* 类型特定函数 */
void *privdata; /* 私有数据 */
dictht ht[2]; /* 哈希表 */
int rehashidx; /* rehash 索引 当 rehash 不在进行时,值为 -1 */
int iterators; /* 目前正在运行的安全迭代器的数量 */
} dict;哈希表用dictht表示
typedef struct dictht {
dictEntry **table; /* 哈希表数组 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /*哈希表大小掩码,用于计算索引值 总是等于 size - 1*/
unsigned long used; /* 该哈希表已有节点的数量 */
} dictht;哈希表节点用dictEntry表示
typedef struct dictEntry {
void *key; /* 键 */
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; /* 指向下个哈希表节点,形成链表,hash键冲突时才被用到 */
} dictEntry;- 哈希算法
// 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
// 使用哈希表的 sizemask 属性和哈希值,计算出索引值
// 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;使用MurmurHash2算法来计算键的哈希值
- 解决键冲突
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
因为dictEntry节点组成的链表没有指向链表表尾部的指针,程序将新节点添加到链表的表头位置(复杂度O(1))
- rehash
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
扩展和收缩哈希表的工作可以通过执行 rehash (重新散列)操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下:
- 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
- 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
- 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。
- 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
- 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
- 哈希表的扩展与收缩
两种情况下,任意一个情况符合时,程序会自动对哈希表进行扩展操作:
1)服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,哈希表负载因子大于等于1
2)服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,哈希表负载因子大于等于5
负载因子公式
#负载因子 = 哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used/ht[0].size- 渐进式rehash
渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除(delete),查找(find),更新(update)等操作会在两个哈希表进行。查找一个键的话,程序会先在ht[0]查找,找不到就继续查找ht[1]。
rehash过程中,新增的数据会添加到ht[1]中,保证ht[0]包含的键值对数量只减不增