字典
字典, 又称符号表(symbol table)、关联数组(associative array)或者映射(map), 是一种用于保存键值对(key-value pair)的抽象结构。
用途
- 存放数据库的键(例如:set的键存在字典里)
- 哈希键的底层实现:当hash包含的键值对比较多或者键值中比较长的字符串时
实现
hash表节点:
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
} v;
//指向下一个hash节点,形成链表来解决hash冲突
struct dictEntry *next;
}
hash表结构:
typedef struct dictht{
//哈希表数组,数组中每一个元素指向dictEntry结构(保存一个键值对)
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,总是等于size-1
unsigned long sizemark;
//哈希表已有节点的数量
unsigned long used;
}
字典结构:
typedef struct dict{
//指向dictType结构的指针,每个dictType结构保存一簇用于操作特定类型键值对的函数
dictType *type;
//保存传给类型特定函数的可选参数
void *privdata;
//哈希表,数组中每一项都是一个dictht哈希表,一般情况下只使用ht[0],ht[1]只会对ht[0]哈希表进行rehash时使用
dictht ht[2];
//记录rehash进度,没有进行rehash时进度为-1
int rehashidx;
}
hash算法
redis使用MurmurHash2算法来计算键的hash值,新版本中使用MurmurHash3算法,这种算法优点是即使输入的键有规律,算法仍能给出一个很好的随机分布性
- 根据键计算出hash值
- 使用hash值和dictht中的sizemask属性计算出索引值
- 根据不同情况插入到ht[1]或ht[2]中
解决键冲突
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。
redis的哈希表使用链地址法,每个hash表都有一个next指针,多个节点可以构成一个单向链表,新节点会被插入到链表表头
例如:k1和k2经过哈希算法后被分到相同的卡槽,那k1和k2就会产生冲突,而解决冲突的办法就是使用 next 指针将键 k2 和 k1 所在的节点连接起来。为了速度考虑, 程序总是将新节点添加到链表的表头位置(复杂度为 O(1)), 排在其他已有节点的前面。
rehash
为了让hash表的负载因子(used/size)维持在一个合理范围之内,当hash表保存的键值对数量太多或者太少时,程序需要对hash表的大小进行相应的扩展和收缩,通过执行rehash来完成
- 当负载因子小于0.1时执行收缩
- 当大于等于5并且正在执行BGSAVE命令或BGREWRITEAOF命令,扩展
- 当负载因子大于等于1并且没有执行BGSAVE命令或BGREWRITEAOF命令,扩展
rehash步骤:
- 为字典ht[1]hash表分配空间,大小取决于执行的操作和ht[0]中used的属性。
- 将所有键值对从ht[0]rehash到ht[1]上,rehash指的是重新计算键的哈希值和索引值
- 当ht[0]所有键值对迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白的哈希表,为下次rehash做准备
渐进式rehash
rehash动作不是一次完成,而是分多次、渐进式完成,因为哈希表中键值对较多时,rehash操作庞大的计算量可能会导致服务器一段时间停止服务,为了避免造成影响采用慢慢的rehash到ht[1]中。
rehash期间,新增操作直接在ht[1]中完成,删除查找更新会在两个表中同时完成
- 为ht[1]分配空间,字典同时持有ht[0]和ht[1]
- 将字典中的索引计数器rehashidx值设置为0,表示rehash工作正式开始
- rehash期间,新增操作直接在ht[1]中完成,删除查找更新操作完成后,会将数据同步到ht[1],rehashidx属性值减一
- 随着字典操作不断执行,最终在某个时间点ht[0]所有数据会被同步到ht[1]中,这时将rehashidx值设为-1,表示rehash工作完成