dict
-
结构
// 哈希表结构 typeof struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值, 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } // 哈希节点结构 typeof struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; double d; } v; // 指向下一个哈希表节点,形成链表 // 该指针可以将多个哈希值相同的键值对连接在一起,以此来解决 hash 冲突的问题 struct dictEntry *next; } dictEntry; // dict 结构 typeof struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash索引,当rehash不在进行时值为-1 long rehashidx; }
-
说明
dict 中的 type 属性和 privdata 属性是针对不同类型的键值对,为创建多态字典而设置的;
type 属性指向的是 dictType 结构的指针,它保存了一簇用于操作特定类型键值对的函数,而 privdata 则是保存用于传给这些函数的可选参数。
ht[1] 只有对 ht[0] 进行 rehash 的过程是才会被使用。
rehashidx 记录了 rehash 的进度。 -
渐进式 rehash
因为对内存扩容操作会涉及到数据的迁移操作O(N),对 redis 很难承受这样耗时的过程(大字典表)。
因此采取了 rehash 操作,其过程为:1. 主动 rehash(dictRehashMilliseconds 服务器定时任务) 或 被动 rehash(_dictRehashStep 负责)时,生成一个新的 ht[1], 并设置 rehashidx 为 0。 2. rehash 期间当有新键值时,将添加到 ht[1] 中;搜索会先从 ht[0] 中查,如果找不到再从 ht[1] 中查。 3. rehash 过程是渐进的,默认情况下,单次最少转移空桶数量为 10 次(版本 5.0)。 4. 当 rehash 过程完全结束,那么修改 ht[0] 指针引用,让他指向新的字典表 ht[1],并设置 rehashidx 为 -1,标记整个字典 rehash 结束。 5. 需要注意的是,每次 CURD 操作时,如果当前为 rehash 状态,需要去完成一个桶的转移,然后才能返回(参考 dictAddRaw)。
-
扩容条件
正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。
不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),
但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。
没有在执行 BGSAVE 或 BGREWRITEAOF 命令时,负载因子大于1进行扩容
在执行 BGSAVE 或 BGREWRITEAOF 命令时,负载因子大于5进行扩容
拓展操作给 ht[1] 分配第一个大于等于 ht[0].used*2 的 2 的 n 次方幂的空间 -
缩容条件
当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考虑 Redis 是否正在做 bgsave。
负载因子小于 0.1 时进行缩容
缩容操作给 ht[1] 分配第一个大于等于 ht[0].used 的 2 的 n 次方幂的空间 -
set 的结构
set 的结构底层实现也是字典,只不过所有的 value 都是 NULL,其它的特性和字典一模一样。