Redis 数据类型 - hash (哈希)

Redis 数据类型 - hash (哈希)

dict (字典),类似于Java 中的 map,k-v 型的数据结构,每个键都是唯一的。底层分别采用了 ziplist 和 hashtable 作为底层数据结构。当元素过少时,使用 ziplist,否则使用 hashtable

1 ziplist 在 hash 对象

每当有 hash 对象需要放入键值对时,就会依次先插入到 ziplist 的尾部,首先插入 key 值,然后插入 value 值,使得 key 和 value 紧挨着,示例如下:

Tian:0>hmset person name andy age 18
"OK"
Tian:0>object encoding person
"ziplist"

hash 对象的编码可以是 ziplist 或者 hashtable
当 hash 对象可以同时满足一下条件,则为 ziplist 编码:

  1. 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  2. 哈希对象保存的键值对数量小于 512个;
    不满足上述情况,否则使用 hashtable 编码。

2 hashtable (哈希表)结构

// 节点结构
struct dictEntry {
    void* key;
    void* val;
    // 链接下一个entry,解决 hash键 冲突
    dictEntry* next;
}

// 字典结构
typedef struct dictht {
    // 哈希表数组
    dictEntry ** table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

3 字典数据结构

结构中包含了两个 hashtable,通常情况下只有一个 ht[0] 是有值的,但是在 dict 扩容缩容的时,需要分配新的 hashtable 进行“渐进式搬迁”,rehashidx 记录 rehash 的进度,待搬迁结束后,旧的 ht[0] 被删除,新的 ht[1] 成为被使用的。一直反复交替,有点类似与垃圾回收的 survivor 区。

typedef struct dict {
    // 类型特定的函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 内部有两个 dictht 结构,也就是上面的 hash 表
    dictht ht[2];
    // rehash 索引,当 rehash 不再进行时,值为 -1
    long rehashidx; 
    // 如果 > 0, rehash 暂停
    int16_t pauserehash; 
}

4 hash 冲突

redis 采用的链地址法来解决键冲突,每个 hash 节点都有个 next 指针,多个 hash 节点可以用链表组成一个单向的链表,类似于 Java 1.7 中的 hashmap 解决 hash 冲突的方法。冲突的节点,放在整个链表的头节点,复杂度O(1),如果用尾插法,那么就需要遍历到尾节点,然后进行指向。

5 扩容条件

  1. 当 redis 没有执行 BGSAVE 或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1,扩容的新数组是旧数组的2倍。
  2. 当 redis 正在做 BGSAVE 或者 BGREWRITEAOF 命令,为了减少内存页的过多分离,redis 不进行扩容, 除非哈希表的负载因子大于等于 5进行强制扩容。

负载因子 = hash 表已保存节点数量 / hash 表大小 = ht[0].used / ht[0].size

6 缩容条件

当负载因子小于 0.1时,会进行缩容,也就是元素的个数低于数组长度的 10%。

7 rehash

当 ht[0]为正在使用的 hash 表,

  • 执行扩容时,ht[1]的空间大小为第一个 >= ht[0].used2 的 2^n。例如 ht[0].used=5,52=10<=2^4(16),所以 ht[1]的空间大小为 16。
  • 执行缩容时,ht[1]的空间大小为第一个 >= ht[0].used 的 2^n。例如 ht[0].used=5,22(4)<=5<=23(8),所以 ht[1]的空间大小为 8。

最后释放 ht[0], 使其变为逻辑意义上的 ht[1]。

8 rehash 渐进式过程

  1. 为ht [ 1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash 至ht [1],这时程序将rehashidx属性的值设为-1,表示 rehash操作已完成。
    渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。
  5. 补充: 当 rehash 过程中,字典进行查找操作时,首先会从ht[0]中查询,如果没有则会从ht[1]中,而新添加的键则会直接保存到ht[1]中。

在每一次的 hset / hdel 指令会进行搬迁,如果没有指令触发,则会在定时任务中对字典进行主动的搬迁。

猜你喜欢

转载自blog.csdn.net/LarrYFinal/article/details/121071467