第四章 字典
在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值),这些关联的键和值就称为键值对。
4.1 字典的实现
Redis的字段使用哈希表作为底层实现。
4.1.1 哈希表
typedef struct ditch{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值,等于size-1 unsigned long sizemask; //该哈希表已有节点的数量 unsigned long used; }dictht;
table是一个数组,其中每个元素是指向一个哈希表节点的指针。
size记录了table的大小。sizemask和哈希值一起决定一个键应该被放到table数组的哪个索引上。
4.1.2 哈希表节点
每个dictEntry结构保存着一个键值对。其中值可以是一个指针,或者uint64_t整数,或者int64_t整数。next属性指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,解决哈希冲突的问题。
typedef struct dictEntry{ //键 void *key; //值 union{ void *val; uint64_t u64; int64_t s64; }v; //指向下一个哈希表节点,形成链表 struct dictEntry *next; }dictEntry;
4.1.3 字典
Redis中的字典结构:
typedef struct dict{ //类型特定函数 dictType *type; //私有数据 void *privdata; //哈希表 dicht ht[2]; //rehash 索引,当rehash不在进行时,值为-1 int trehashidx; }dict
type和privdata是为了多态字典而设计的,其中type指向了一个dictType结构,里面定一个了一系列操作键值对的函数,而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属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在rehash时使用,rehashidx记录了rehash的进度。
4.2 哈希算法
先计算key的哈希值,根据哈希值和sizemask计算索引值。根据索引值将键存放到指定位置。
4.3 解决键冲突
当有两个及以上的键被分到同一个索引上时,产生了哈希冲突,Redis的解决方法是链地址法,将冲突的部分排成链表。因为没有指向链表表尾的指针,出于速度考虑,最新插入的放在表头位置。
4.4 rehash
- 为ht[1]分配空间
- 如果是扩展操作,取第一个大于等于ht[0].used*2的2的n次方幂
- 如果是收缩操作,取第一个大于等于ht[0].used的2的n次方幂
- 将ht[0]中的键值对重新映射到ht[1]中
- 将ht[0]空间释放,将ht[1]设置为h[0],将ht[0]设置为ht[1],并在ht[1]处创建一个新的空白哈希表,为下次rehash做准备
4.5 渐进式rehash
渐进是指当键值对太多的情况,并不会一次全部重新分配完。
- 为ht[1]分配空间,字典同是持有ht[0]和ht[1]两个哈希表
- 将rehashidx初始化为0
- 每次对字典执行增删改查操作时,程序还会额外将ht[0]表在rehashidx索引上所有的键值对rehash到ht[1]中,当操作完毕时,将rehashidx+1
- 最终,ht[0]的所有键值对全部迁移完,将rehashidx重置为-1
在渐进rehash期间,新增的键值对,一律保存在ht[1]中。删改查则先在ht[0]中查找,未找到再去ht[1]中查找。
人生就像蒲公英,看似自由,其实身不由己。