前一篇文章,我们学习的是 Redis的数据结构 list, 学习了其基本的操作和使用内部数据结构是quicklist
和ziplist
,这两种数据结构虽然起得名字是list
,但是其内部结构确实链表。如果不记得了其内部构成, 就再看看看着上篇文章吧。现在我们继续学习下一个数据类型 hash
hash
简介hash
是一个键值对集合. 是 string
类型的 key
和 value
的映射表, hash 特别适合用于存储对象, 每个hash
类型可以存储 2^32-1
个键值对。
hash
实际上就是一个 哈希表。类似于 Java
里的HashTable
。
但是 Redis
的哈希是有两种数据结构(内部编码)来表示的。
一种是 ziplist
,上篇文章中我们简单的介绍了ziplist
的内部构成,见 Redis的数据结构 list, 以及ziplist
的编码方式, 可以看这篇文章 10-Redis的数据结构之ziplist.md. Redis
什么时候会使用ziplist
这种编码方式呢?
hash
类型的元素的个数小于 hash-max-ziplist-enties
配置,默认512
.hash-max-ziplist-value
的值,默认是64
个字节的时候。ziplist
这种结构。这种方式最大的优点就是节约空间。
hashtable
来编码了。当不满足上面提及的两个条件时,就会使用 hashtable
来编码。实际上是 dict
这种数据结构。这里我们又可以学习到一个新的数据结构 dict
hash
的一个键值对。改变的时候,只需要改变对应的某个filed-value
即可。id
为key
, 商品的id
为 field
. 商品的数量为value
。 比如: hset userId productId productCount
hash
的基本命令hset
hset key field value
将哈希表 hash
中域 field
的值设置为 value
。
如果给定的哈希表并不存在, 那么一个新的哈希表将被创建并执行 HSET
操作。
如果域 field
已经存在于哈希表中, 那么它的旧值将被新值 value
覆盖。
## 设置一个hash结构
127.0.0.1:6379> HSET k38 f1 v38
(integer) 1
# 获取一个字段
127.0.0.1:6379> HGET k38 f1
"v38"
# 设置一个已经存在的值, 注意返回的值。
127.0.0.1:6379> HSET k38 f1 v38v38
(integer) 0
127.0.0.1:6379> HGET k38 f1
"v38v38"
hsetnx
HSETNX key field value
当且仅当域 field
尚未存在于哈希表的情况下, 将它的值设置为 value
。
如果给定域已经存在于哈希表当中, 那么命令将放弃执行设置操作。
如果哈希表 hash
不存在, 那么一个新的哈希表将被创建并执行 HSETNX
命令。
# 设置一个不存在的 key
127.0.0.1:6379> HSETNX k39 f1 v39
(integer) 1
127.0.0.1:6379> HGET k39 f1
"v39"
# 再次设置
127.0.0.1:6379> HSETNX k39 f1 v39v39
(integer) 0
127.0.0.1:6379> HGET k39 f1
"v39"
hget
这个命令上面已经用到了。这里就不浪费时间了。
HGET key field
获取对应的 key
下的域 field
的值。不存在的时候,返回 nil
hgetall
HGETALL key
返回哈希表 key
中,所有的域和值。
在返回值里,紧跟每个域名(field name
)之后是域的值(value
),所以返回值的长度是哈希表大小的两倍。
127.0.0.1:6379> HGETALL k39
1) "f1"
2) "v39"
127.0.0.1:6379> hset k39 f2 v39_2
(integer) 1
127.0.0.1:6379> HGETALL k39
1) "f1"
2) "v39"
3) "f2"
4) "v39_2"
hexists
HEXISTS key field
检查给定域 field
是否存在于哈希表 hash
当中。
存在返回1
,不存在返回0
。
127.0.0.1:6379> HEXISTS k40 f1
(integer) 0
127.0.0.1:6379> HSET k40 f1 v40
(integer) 1
127.0.0.1:6379> HEXISTS k40 f1
(integer) 1
del
HDEL key field [field ...]
删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
127.0.0.1:6379> HSET k41 f1 v41_1
(integer) 1
127.0.0.1:6379> HSET k41 f2 v41_2
(integer) 1
127.0.0.1:6379> HSET k41 f3 v41_3
(integer) 1
127.0.0.1:6379> HGETALL k41
1) "f1"
2) "v41_1"
3) "f2"
4) "v41_2"
5) "f3"
6) "v41_3"
127.0.0.1:6379> HDEL k41 f1 f3 f4
(integer) 2
127.0.0.1:6379> HGETALL k41
1) "f2"
2) "v41_2"
hlen
HLEN key
返回哈希表 key
中域的数量。
127.0.0.1:6379> HSET k42 f1 v42_1
(integer) 1
127.0.0.1:6379> HSET k42 f2 v42_2
(integer) 1
127.0.0.1:6379> HSET k42 f3 v42_3
(integer) 1
127.0.0.1:6379> hlen k42
(integer) 3
hstrlen
HSTRLEN key field
返回哈希表 key
中, 与给定域 field
相关联的值的字符串长度(string length
)。
如果给定的键或者域不存在, 那么命令返回 0
。
127.0.0.1:6379> HSET k43 f1 "Hello World"
(integer) 1
127.0.0.1:6379> HSTRLEN k43 f1
(integer) 11
127.0.0.1:6379> HSTRLEN k43 f2
(integer) 0
HINCRBY key field increment
为哈希表 key
中的域 field
的值加上增量 increment
。
增量也可以为负数,相当于对给定域进行减法操作。
如果 key
不存在,一个新的哈希表被创建并执行 HINCRBY
命令。
如果域 field
不存在,那么在执行命令前,域的值被初始化为 0
。
对一个储存字符串值的域 field
执行 HINCRBY
命令将造成一个错误。
本操作的值被限制在 64
位(bit
)有符号数字表示之内。
# 不存在的key与域 field
127.0.0.1:6379> HINCRBY k45 f1 100
(integer) 100
127.0.0.1:6379> HINCRBY k45 f1 -200
(integer) -100
127.0.0.1:6379> HINCRBY k45 f1 200
(integer) 100
# 错误的类型
127.0.0.1:6379> HSET k45 f2 v45
(integer) 1
127.0.0.1:6379> HINCRBY k45 f2 100
(error) ERR hash value is not an integer
hincrbyfloat
HINCRBYFLOAT key field increment
为哈希表 key
中的域 field
加上浮点数增量 increment
。
如果哈希表中没有域 field
,那么 HINCRBYFLOAT
会先将域 field
的值设为 0
,然后再执行加法操作。
如果键 key
不存在,那么 HINCRBYFLOAT
会先创建一个哈希表,再创建域 field
,最后再执行加法操作。
127.0.0.1:6379> HINCRBYFLOAT k46 f1 100.5
"100.5"
127.0.0.1:6379> HINCRBYFLOAT k46 f1 100.5
"201"
127.0.0.1:6379> HINCRBYFLOAT k46 f1 -100.5
"100.5"
127.0.0.1:6379> HSET k46 f2 v46_2
(integer) 1
hmset
HMSET key field value [field value ...]
同时将多个 field-value
(域-值)对设置到哈希表 key
中。
此命令会覆盖哈希表中已存在的域。
如果 key
不存在,一个空哈希表被创建并执行 HMSET
操作。
127.0.0.1:6379> HMSET k47 f1 v47_1 f2 v47_2 f3 v47_3
OK
127.0.0.1:6379> HGETALL k47
1) "f1"
2) "v47_1"
3) "f2"
4) "v47_2"
5) "f3"
6) "v47_3"
hmget
HMGET key field [field ...]
返回哈希表 key
中,一个或多个给定域的值。
如果给定的域不存在于哈希表,那么返回一个 nil
值。
因为不存在的 key
被当作一个空哈希表来处理,所以对一个不存在的 key
进行 HMGET
操作将返回一个只带有 nil
值的表。
127.0.0.1:6379> HMSET k48 f1 v1 f2 v2 f3 v3 f4 v4
OK
127.0.0.1:6379> hmget k48 f1 f3 f4
1) "v1"
2) "v3"
3) "v4"
127.0.0.1:6379>
hkeys
HKEYS key
返回哈希表 key
中的所有域。
当 key
不存在时,返回一个空表。
127.0.0.1:6379> HMSET k49 f1 v1 f2 v2 f3 v3 f4 v4
OK
127.0.0.1:6379> HKEYS k49
1) "f1"
2) "f2"
3) "f3"
4) "f4"
hvals
HVALS key
返回 key
对应的所有的value
127.0.0.1:6379> HMSET k50 f1 v1 f2 v2 f3 v3 f4 v4
OK
127.0.0.1:6379> HVALS k50
1) "v1"
2) "v2"
3) "v3"
4) "v4"
hscan
HSCAN key cursor [MATCH pattern] [COUNT count]
这是一个查询命令。 同 SCAN 命令. 可以参考这篇文章 010-其他命令
SCAN
命令是一个基于游标的迭代器(cursor based iterator
): SCAN
命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN
命令的游标参数, 以此来延续之前的迭代过程。
127.0.0.1:6379> HMSET k51 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6 f7 v7 f8 v8
OK
127.0.0.1:6379> hscan k51 0
1) "0"
2) 1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
7) "f4"
8) "v4"
9) "f5"
10) "v5"
11) "f6"
12) "v6"
13) "f7"
14) "v7"
15) "f8"
16) "v8"
以上,就是 Redis
中hash
类型相关的15
个命令了。务必熟记~
hash
的内部结构在 hash
类型简介的时候,我们就说过 hash
是用两种数据结构来编码的。
ziplist
hashtable
(dict
)
ziplist
之前已经分享过了。具体参考之前的文章吧。 [链接]
这里我们就简单的来看下 hashtable
.
我们直接搜索 hash
,可以发现 t_hash.c
这个文件,引入了 server.h
. 大体看了一下,都是函数的实现。那我们看下 server.h
,应该存在对 hastable
的定义吧。然而,并没有。
那我们来看下t_hash.c
中添加方法的实现吧. int hashTypeSet(robj *o, sds field, sds value, int flags)
源码太长了,这里就不粘了, 可以看源码
通过查看源码可以得出:
hash
类型的默认编码是 OBJ_ZIPLIST
. 即默认是使用 ziplist
这种数据结构进行编码存储的。robj *createHashObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
hash
元素的个数大于 hash_max_ziplist_entries
时会,转换成 hashTable
(OBJ_ENCODING_HT
),...
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
...
但是在 redis 5.0.7
中暂时不支持这种方式, 还没有实现。(没有实现从ziplist
编码转化成hash
编码。)
void hashTypeConvert(robj *o, int enc) {
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
hashTypeConvertZiplist(o, enc);
}
/// 这里!!!
else if (o->encoding == OBJ_ENCODING_HT) {
serverPanic("Not implemented");
} else {
serverPanic("Unknown hash encoding");
}
}
hash
类型是 hashtable
编码(OBJ_ENCODING_HT
)时,是使用dict
这种类型存储的./// dict类型
typedef struct dict {
dictType *type;
void *privdata;
/// 2个哈希表来实现
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
/// 哈希表实现
typedef struct dictht {
dictEntry **table; /// 哈希表节点指针数据(java源码中的桶的概念)
unsigned long size; /// 指针数组的大小
unsigned long sizemask; /// 指针数据的长度掩码,用于计算索引值
unsigned long used; /// 哈希表现有的节点数量
} dictht;
///哈希表的节点
typedef struct dictEntry {
/// 键
void *key;
/// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
/// 下一个节点: dictht 是使用链地址法来处理hash冲突。
struct dictEntry *next;
} dictEntry;
整个 dict
结构就可以这么表示:
到这里,我们就知道了 hash
这种类型,是如何存储的了。 如果你还想了解
dict
是如何 rehash
, 扩容,缩容。以及 dict api
相关实现的话,移驾这篇文章吧。 起驾 ~
hash
结构,是一种哈希表结构。通过两种数据结构ziplist
和 hashtable
(dict
)实现。hash
相关的15
个命令。hashtable
的编码格式, 实际上就是使用的 dict
这种编码方式。我们简单的学习了Redis
中dict
结构的实现。还有一篇专门的文章,来介绍 dict
的详细内容。希望和你成为朋友!我们一起学习~
最新文章尽在公众号【方家小白】,期待和你相逢在【方家小白】