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 编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于 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 扩容条件
- 当 redis 没有执行 BGSAVE 或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1,扩容的新数组是旧数组的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 渐进式过程
- 为ht [ 1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash 至ht [1],这时程序将rehashidx属性的值设为-1,表示 rehash操作已完成。
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。 - 补充: 当 rehash 过程中,字典进行查找操作时,首先会从ht[0]中查询,如果没有则会从ht[1]中,而新添加的键则会直接保存到ht[1]中。
在每一次的 hset / hdel 指令会进行搬迁,如果没有指令触发,则会在定时任务中对字典进行主动的搬迁。