前言Redis是一个键值对数据库,它的键是通过哈希存储的。整个Redis可以认为是一个外层哈希,之所以称为外层哈希,是因为Redis在内部也提供了一种哈希类型,可以称为内层哈希。当我们使用哈希对象进行数据存储时,对于整个Redis来说,有两层哈希存储。哈希对象哈希对象本身也是一种key-value存储结构,底层存储结构也可以分为ziplist(压缩列表)和hashtable(哈希表)两种。这两种存储结构也是通过编码来区分的:编码属性描述对象编码命令返回值OBJ_ENCODING_ZIPLIST使用压缩列表实现hash对象ziplistOBJ_ENCODING_HT使用字典实现hash对象hashtablekey-value在hashtableRedis中被dictEntry对象封装,hash表再次封装dictEntry对象得到,即哈希表对象dicttht:typedefstructdict{dictEntry**table;//哈希表数组unsignedlongsize;//哈希表大小unsignedlongsizemask;//掩码大小,用于计算索引值,始终等于size-1unsignedlongused;//哈希表中已存在的节点数}dicttht;注意:上面结构体定义中的table是一个数组,其中的每一个元素都是一个dictEntry对象。字典字典,也称为符号表、关联数组或映射,具有嵌套在字典中的哈希表dictht对象。下面是字典ht的定义:typedefstructdict{dictType*type;//字典类型的一些具体函数void*privdata;//私有数据,type中的具体函数可能需要用到dictththt[2];//哈希表(注意这里有2个hash表)longrehashidx;//rehash索引,不在rehash时,值为-1unsignedlongiterators;//使用的迭代器个数}dict;其中dictType内部定义了一些常用的函数,其数据结构定义如下:constvoid*key);//复制键函数void*(*valDup)(void*privdata,constvoid*obj);//复制值函数int(*keyCompare)(void*privdata,constvoid*key1,constvoid*key2);//比较键函数void(*keyDestructor)(void*privdata,void*key);//销毁键函数void(*valDestructor)(void*privdata,void*obj);//销毁值函数}dictType;当我们创建一个哈希对象时,可以得到如下图(部分属性省略):在rehash操作dict中定义了一个数组ht[2],在ht[2]中定义了两个哈希表:ht[0]和HT[1].默认情况下,Redis只会使用ht[0],不会使用ht[1],也不会为ht[1]初始化分配空间。在设置一个hash对象时,hash数组(上图中的dictEntry[3])中的下标落在哪个下标是通过计算hash值来决定的。如果出现hash冲突(计算出的hash值一致),那么同一个下标就会有多??个dictEntry,从而形成一个链表(上图中最右边的点指向NULL位置),但应该是注意,最后插入的元素总是落在链表的最前面(即发生哈希冲突时,该节点总是放在链表的头部)。读取数据时,遇到有多个元素的结点,需要遍历链表,所以链表越长,性能越差。为了保证哈希表的性能,当满足以下两个条件之一时,需要对哈希表进行rehash:当加载因子大于等于1且dict_can_resize为1时。当加载因子为大于或等于安全阈值(dict_force_resize_ratio=5)。PS:负载因子=哈希表使用的节点数/哈希表的大小(即:h[0].used/h[0].size)。rehash步骤扩展哈希和收缩哈希是通过执行rehash完成的,涉及到空间的分配和释放,主要通过以下五个步骤:1.为dictionarydict的ht[1]哈希表分配空间,其大小取决于当前哈希表保存的节点数(即:ht[0].used):如果是扩展操作,ht[1]的大小为2的n次方,第一个大于或等于ht[0].used*2属性的值(比如used=3,此时ht[0].used*2=6,所以2的3次方为8,也就是第一个值大于使用*2(2的二次方<6和2的三次方>6))。如果是收缩操作,ht[1]的大小是第一个大于或等于ht[0]的值。在2的n次方中使用。2.设置字典中属性rehashix的值为0,表示正在执行rehash操作。3、将ht[0]中的所有键值对一一重新计算hash值,放到ht[1]数组的相应位置。每次rehash一个键值对完成后,rehashix的值需要加1。4.ht[0]中的所有键值对迁移到ht[1]后,释放ht[0],将ht[1]改为ht[0],然后新建一个ht[1]]数组,为下一次rehash做准备。5、将字典中的属性rehashix设置为-1,表示rehash操作结束,等待下一次rehash。Redis中的rehash操作并不是一次性全部rehash,而是将ht[0]到ht[1]中的键值对分多次慢慢rehash,所以这种操作也叫它progressiverehash。Progressiverehash可以避免集中式rehash带来的巨大计算量,这是一种分而治之的思想。在progressiverehash的过程中,因为可能有新的key-valuepair存入,此时**Redis的做法是将新加入的key-valuepair放入ht[1],从而保证ht[0]键值对的数量只会减少**。在进行rehash操作时,如果服务端收到客户端的命令请求操作,会先查询ht[0],如果找不到结果再查询ht[1]。ziplistziplist的一些特性在上一篇文章中已经单独分析过了。如果您想了解更多信息,可以单击此处。但是需要注意的是,hash对象中的ziplist和list对象中的ziplist是有区别的。hash对象是key-value形式,所以ziplist也用key-value表示,key和value靠得很近:对象会选择使用ziplist编码进行存储:hash对象中所有键值对(包括key和value)的总长度小于等于64字节(这个阈值可以通过参数hash-max-控制ziplist值)。哈希对象中键值对的个数小于等于512(这个阈值可以通过参数hash-max-ziplist-entries来控制)。一旦这两个条件中的任何一个不满足,哈希对象就会选择使用哈希表编码进行存储。哈希对象常用命令hsetkeyfieldvalue:设置单个字段(哈希对象的键值)。hmsetkeyfield1value1field2value2:设置多个字段(哈希对象的键值)。hsetnxkeyfieldvalue:设置哈希表key中field字段的值为value,如果该字段已经存在,则什么都不做。hgetkeyfield:获取哈希表key中field字段对应的值。hmgetkeyfield1field2:获取哈希表key中多个field字段对应的values。hdelkeyfield1field2:删除哈希表key中的一个或多个字段。hlenkey:返回哈希表中字段的个数键。hincrbykeyfieldincrement:对哈希表的key中field字段的值增加increment。增量可以是负数。如果该字段不是数字,则会报错。hincrbyfloatkeyfieldincrement:对哈希表的key中field字段的值增加increment。增量可以是负数。如果字段不是float类型,会报错。hkeyskey:获取哈希表中所有字段的key。hvalskey:获取哈希表中所有字段的值。知道了操作哈希对象的常用命令,我们就可以验证上面提到的哈希对象的类型和编码了。在测试之前,为了防止其他键值的干扰,我们先执行flushall命令清空Redis数据库。然后依次执行以下命令:hsetaddresscountrychinatypeaddressobjectencodingaddress得到如下效果:可以看到当我们的hash对象只有一对键值对时,底层编码是ziplist。现在我们将hash-max-ziplist-entries参数改为2,重启Redis,最后输入如下命令进行测试:hmsetkeyfield1value1field2value2field3value3objectencodingkey输出结果如下:可以看到,编码已经变成hashtable了。总结本文主要介绍了Redis中常用的5种数据类型中hash类型的底层存储结构hashtable的使用,以及当hash分布不均匀时Redis是如何重新哈希的。最后,我了解了一些散列对象。常用的命令,并通过一些例子来验证本文的结论。
