今天学习一下 Redis 里字典的实现,也就是哈希表。一个哈希表里可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。
哈希表
定义如下:
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemark;
// 该哈希表已有节点数量
unsigned long used;
} dictht;
table属性是一个数组,数组中的每个元素都是一个指向哈希表节点对应结构体的指针,每个哈希表节点里保存着一个键值对。size属性记录了哈希表的大小,也就是table数组的大小,used属性记录了目前已有节点(键值对)的数量。sizemark属性的值总是等于size - 1,这个属性和某个键的哈希值一起用来计算这个键的索引。
画个图好理解一点,这是一个大小为 4 的空哈希表:
哈希表节点
定义如下:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
key属性保存着键值对中的键v属性保存键值对中的值,值可以是指针,可以是uint64_t或者int64_t整数。next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,解决键冲突(collision)问题。
画图举例子,下图里 k1 和 k0 索引一样,用 next 指针来连接它俩:
看到这里应该明白这里至少有两个坑在等着,一个是 sizemark 为什么要单独设置一个变量,另一个是 used 的值可能比 size 要大。然后应该能大概猜到一些原因,比如说 size 扩容是怎么扩的。
字典
定义如下:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 没有进行 rehash 时值是 -1
int rehashidx; /*rehashing not in progress if rehashidx == -1*/
} dict;
-
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:type属性是一个指向dictType结构的指针,每个dictType结构保存了很多用于操作特定类型键值对的函数,Redis 会给用途不同的字典设置不同的类型特定函数。privdata属性保存的是类型特定函数的可选参数,是实际的参数值,不是可选参数可以取的值。
这两个属性相关的定义如下:
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction) (const void *key);
// 复制键的函数
void *(*keyDup) (void *privdata, const void *key);
// 复制值的函数
void *(*valDup) (void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare) (void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor) (void *privdata, void *key);
// 销毁值的函数
void (*valDestructor) (void *privdata, void *obj);
} dictType;
ht属性是一个包含两项的数组,每一项都是一个哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]进行 rehash 时使用。rehashidx属性记录当前 rehash 的进度,没有在 rehash 这个值就是 -1。
依然要画个图,这是普通状态下的字典(还没 rehash):
这里可以看到第二个哈希表里 size 和 sizemark 都是 0,这个时候就和前面规定的不一样了,或许单独拉出来一个 sizemark 变量也有这个原因。
然后这里也可以看到为了方便我的图是一步一步往上加东西的,而且到了最后这张图里其实有点问题了,虽然用链表解决了哈希冲突,但是一个索引上挂上太多的键值对查询效率是会受到影响的,我明明还有空的索引,不需要全都链到一个索引下。到这刚刚好,作为一个引子引出下一篇笔记里的 rehash 操作。