Redis哈希对象之hashtable和ziplist实现原理分析
前言
上一篇我们分析了列表对象的底层存储结构linkedlist
,ziplist
和quicklist
的实现原理,并将这三种数据结构进行了分析对比。这一篇我们继续分析Redis中5种常用数据类型的第3种基本数据类型哈希对象
的底层存储结构。
哈希对象
哈希对象本身也是一个key-value存储结构,底层的存储结构也可以分为两种:ziplist
和hashtable
。而我们知道,Redis本身也是一个key-value的数据库,但是Redis本身的key-value只能采用hashtable
形式(也可以称之为外部哈希),而内部哈希的两种存储结构和其他数据类型一样,也是通过编码来区分的:
编码属性 | 描述 | object encoding命令返回值 |
---|---|---|
OBJ_ENCODING_ZIPLIST | 使用压缩列表实现哈希对象 | ziplist |
OBJ_ENCODING_HT | 使用字典实现哈希对象 | hashtable |
hashtable
Redis中的key-value是通过dictEntry
对象来实现的,而哈希表就是将dictEntry
对象进行了再一次的包装得到的,这就是哈希表对象dictht
:
typedef struct dictht {
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//掩码大小,用于计算索引值,总是等于size-1
unsigned long used;//哈希表中的已有节点数
} dictht;
PS:table是一个数组,其每个元素都是一个dictEntry对象。
字典
字典,又称为符号表(symbol table),关联数组(associative array)或者映射(map),字典的内部嵌套了哈希表dictht
对象,下面就是一个字典ht
的定义:
typedef struct dict {
dictType *type;//字典类型的一些特定函数
void *privdata;//私有数据,type中的特定函数可能需要用到
dictht ht[2];//哈希表(注意这里有2个哈希表)
long rehashidx; //rehash索引,不在rehash时,值为-1
unsigned long iterators; //正在使用的迭代器数量
} dict;
其中dictType
内部定义了一些常用函数,其数据结构定义如下:
typedef struct dictType {
uint64_t (*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;
所以当创建一个哈希对象
时,可以得到如下简图(部分属性被省略):
PS:最后哈希表中的k和v保存的是一个字符串对象。
rehash操作
ht[2]定义了两个哈希表,ht[0]和ht[1]。而Redis在默认情况下使用的是ht[0],不会为ht[1]初始化分配空间。
当设置一个哈希对象时,具体会落到哈希数组(上图中的dictEntry*[3])中的哪个下标,是通过计算哈希值来确定的,如果发生哈希碰撞,那么同一个下标就会有多个dictEntry
,从而形成一个链表(最后插入的总是落在链表的最前面),链表越长,性能越差。所以为了保证哈希表的性能,需要在满足以下两个条件中的一个时,对哈希表进行rehash(重新散列)操作:
- 1、负载因子大于等于1且dict_can_resize设置为1时
- 2、负载因子大于等于安全阈值(dict_force_resize_ratio=5)时
PS:负载因子=哈希表已使用节点数/哈希表大小(即:h[0].used/h[0].size)。
rehash步骤
扩展哈希和收缩哈希都是通过执行rehash来完成,主要经过以下五步:
- 1、为字典
dict
的ht[1]哈希表分配空间,其大小取决于当前哈希表已保存节点数(即:ht[0].used)。
(a)、扩展操作则ht[1]的大小为2n中第一个大于等于ht[0].used * 2属性的值(比如used=3,此时23就是第一个大于used * 2 的值(22<6且23>6))。
(b)、收缩操作则ht[1]大小为2n中第一个大于等于ht[0].used的值。 - 2、将字典中的属性rehashix的值设置为0,表示正在执行rehash操作。
- 3、将ht[0]中所有的键值对依次重新计算哈希值,并放到ht[1]数组对应位置,完成一个键值对的rehash之后rehashix的值需要加1。
- 4、当ht[0]中所有的键值对都迁移到ht[1]之后,释放ht[0],并将ht[1]修改为ht[0],然后再创建一个新的ht[1]数组,为下一次rehash做准备。
- 5、将字典中的属性rehashix设置为-1,表示rehash已经结束
渐进式rehash
上面介绍的这种方式因为不是一次性全部rehash,而是分多次来慢慢的将ht[0]中的键值对rehash到ht[1]的操作就称之为渐进式rehash。渐进式rehash可以避免了集中式rehash带来的庞大计算量,采用了分而治之的思想。
在渐进式rehash过程中,因为还可能会有新的键值对存进来,此时Redis的做法是新添加的键值对统一放入ht[1]中,这样就确保了ht[0]键值对的数量只会减少。
当执行rehash操作时需要执行查询操作,此时会先查询ht[0],查找不到结果再到ht[1]中查询。
ziplist
关于ziplist
的一些特性,在上一篇讲述列表底层数据结构的时候已经进行过了详细分析(想要详细了解的,可以点击这里)。但是哈希对象中的ziplist
和列表对象中ziplist
的不同之处在于哈希对象是一个key-value形式,所以其ziplist
中也表现为key-value,key和value紧挨在一起:
ziplist和hashtable的编码转换
当一个哈希对象可以满足以下两个条件中的任意一个,哈希对象会选择使用ziplist
编码来进行存储:
- 1、哈希对象中的所有键值对总长度(包括键和值)小于64字节(这个阈值可以通过参数
hash-max-ziplist-value
来进行控制)。 - 2、哈希对象中的键值对数量小于512个(这个阈值可以通过参数
hash-max-ziplist-entries
来进行控制)。
一旦不满足这两个条件中的任意一个,哈希对象就会选择使用hashtable
来存储。
总结
本文主要介绍了Redis中5种常用数据类型中的哈希类型
底层的存储结构,主要介绍了其底层数据结构hashtable
的使用,而ziplist
编码除了key和value放在一起之外其余特性和列表对象中的ziplist
是一致的,最后讲述了哈希对象中两种编码的转换条件。
下一篇,我们将分析Redis中5种常用数据类型中的第4种集合对象
的底层存储结构。
请关注我,和孤狼一起学习进步。