一、前言#
Redis提供了5种数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每种数据类型的特点对于redis的开发和运维非常重要。
Redis中的hash是我们经常使用到的一种数据类型,根据使用方式的不同,可以应用到很多场景中。
二、实现分析#
由上述结构图可知,Hash类型有以下两种实现方式:
1、ziplist编码的哈希对象使用压缩列表作为底层实现
2、hashtable编码的哈希对象使用字典作为底层实现
1.ziplist编码作为底层实现#
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
例如,我们执行以下HSET命令,那么服务器将创建一个列表对象作为profile键的值:
redis>HSETprofilename"Tom"
(integer)1
redis>HSETprofileage25
(integer)1
redis>HSETprofilecareer"Programmer"
(integer)1
profile键的值对象使用的是ziplist编码,其中对象所使用的压缩列表结构如下图所示。
2.hashtable编码作为底层实现#
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
字典的每个键都是一个字符串对象,对象中保存了键值对的键;
字典的每个值都是一个字符串对象,对象中保存了键值对的值。
例如,如果前面profile键创建的不是ziplist编码的哈希对象,而是hashtable编码的哈希对象,那么这个哈希对象结构如下图所示。
三、命令实现#
因为哈希键的值为哈希对象,所以用于哈希键的所有命令都是针对哈希对象来构建的,下表列出了其中一部分哈希键命令,以及这些命令在不同编码的哈希对象下的实现方法。
命令 | ziplist 编码实现方法 | hashtable 编码的实现方法 |
---|
HSET | 首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用 ziplistPush 函数, 将值推入到压缩列表的表尾。 | 调用 dictAdd 函数, 将新节点添加到字典里面。 |
HGET | 首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。 | 调用 dictFind 函数, 在字典中查找给定键, 然后调用dictGetVal 函数, 返回该键所对应的值。 |
HEXISTS | 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 | 调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 |
HDEL | 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。 | 调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。 |
HLEN | 调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。 | 调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。 |
HGETALL | 遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。 | 遍历整个字典, 用 dictGetKey 函数返回字典的键, 用dictGetVal 函数返回字典的值。 |
四、编码转换#
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
哈希对象保存的键值对数量小于512个;
不能满足这两个条件的哈希对象需要使用hashtable编码。
注意:这两个条件的上限值是可以修改的,具体请看配置文件中关于hash-max-ziplist-value选项和hash-max-ziplist-entries选项的说明。
对于使用ziplist编码的列表对象来说,当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行:原本保存在压缩列表里的所有键值对都会被转移并保存到字典里面,对象的编码也会从ziplist变为hashtable。
以下代码展示了哈希对象编码转换的情况:
1.键的长度太大引起编码转换#
#哈希对象只包含一个键和值都不超过64个字节的键值对
redis>HSETbookname"MasteringC++in21days"
(integer)1
redis>OBJECTENCODINGbook
"ziplist"
#向哈希对象添加一个新的键值对,键的长度为66字节
redis>HSETbooklong_long_long_long_long_long_long_long_long_long_long_description"content"
(integer)1
#编码已改变
redis>OBJECTENCODINGbook
"hashtable"
2.值的长度太大引起编码转换#
#哈希对象只包含一个键和值都不超过64个字节的键值对
redis>HSETblahgreeting"helloworld"
(integer)1
redis>OBJECTENCODINGblah
"ziplist"
#向哈希对象添加一个新的键值对,值的长度为68字节
redis>HSETblahstory"manystring...manystring...manystring...manystring...many"
(integer)1
#编码已改变
redis>OBJECTENCODINGblah
"hashtable"
3.键值对数量过多引起编码转换#
#创建一个包含512个键值对的哈希对象
redis>EVAL"fori=1,512doredis.call('HSET',KEYS[1],i,i)end"1"numbers"
(nil)
redis>HLENnumbers
(integer)512
redis>OBJECTENCODINGnumbers
"ziplist"
#再向哈希对象添加一个新的键值对,使得键值对的数量变成513个
redis>HMSETnumbers"key""value"
OK
redis>HLENnumbers
(integer)513
#编码改变
redis>OBJECTENCODINGnumbers
"hashtable"
总结1.Hash类型两种编码方式,ziplist与hashtable
总结2.hashtable编码的哈希对象使用字典作为底层实现
总结3.ziplist与hashtable编码方式之间存在转换