Redis Hash 数据结构:从命令到应用的全面攻略
Redis Hash 数据结构:从命令到应用的全面攻略
Redis中的Hash数据结构是一种非常实用的数据类型,它允许用户在单个键下存储多个字段和值的映射关系。本文将详细介绍Redis Hash的各种命令、底层编码方式以及实际应用场景,帮助读者全面掌握这一重要数据结构。
Redis本身已经是键值对的结构了,那value这一层,又可以是哈希结构(套娃)。我们可以看这张图理解一下:
那在哈希类型中,映射关系通常称为field-value。因为Redis整体的键值对映射关系叫做key-value,为了进行区分,哈希类型的映射关系就叫做field-value。
一. 常见命令
1.1 hset 和 hget
hset的作用就是设置hash中指定的字段(field)的值(value)。
语法:hset key field1 value1 [field2 value2 …]
value只能为字符串
返回值代表设置成功的键值对的个数(也就是field的个数)
hget的作用就是获取hash中指定字段的值。
语法:hget key field
那我们举一个例子来看看hset和hget的用法:
127.0.0.1:6379> hset key k1 v1 # 设置哈希类型
(integer) 1
127.0.0.1:6379> hset key k2 v2 k3 v3 k4 v4 # 可以设置多组哈希
(integer) 3 # 返回值代表设置成功的键值对的个数(field的个数)
127.0.0.1:6379> hget key k1 # 获取哈希类型
"v1"
127.0.0.1:6379> hget key k2
"v2"
127.0.0.1:6379> hget key k100 # 如果获取的field不存在就会返回nil
(nil)
127.0.0.1:6379> hget key2 k1 # 如果获取到的key不存在也会返回nil
(nil)
1.2 hexists
hexists的作用就是判断hash中是否有指定的字段(也就是判断field是否存在)。
语法:hexists key field
返回值:1代表存在,0代表不存在
时间复杂度:O(1)
127.0.0.1:6379> hexists key k1 # 判断key中的k1是否存在
(integer) 1 # 1代表存在
127.0.0.1:6379> hexists key k100 # 如果field不存在是查询不到数据的
(integer) 0 # 0代表不存在
127.0.0.1:6379> hexists key2 k1 # 如果key不存在也是查询不到数据的
(integer) 0
1.3 hdel
hdel的作用就是删除hash中指定的字段。
del删除的是key,hdel删除的是field
语法:hdel key field1 [field2 …]
时间复杂度:O(1)
返回值代表本次删除成功的字段个数
127.0.0.1:6379> hdel key k1 # 删除key中的k1
(integer) 1 # 返回值代表删除成功的个数
127.0.0.1:6379> hexists key k1 # 此时key中的k1就不存在了
(integer) 0
127.0.0.1:6379> hdel key k2 k3 # 可以删除多个field
(integer) 2
127.0.0.1:6379> hdel key2 k1 # 如果删除不存在的key就会删除失败
(integer) 0
127.0.0.1:6379> hdel key k100 # 如果删除不存在的field也会删除失败
(integer) 0
1.4 hkeys
hkeys可以获取到哈希中所有的字段(field)。
语法:hkeys key
这个操作底层的实现是会先根据key找到对应的field,然后遍历field集合。
时间复杂度:O(N),N指的是哈希的元素个数(field的个数)
127.0.0.1:6379> hset key f1 111 f2 222 f3 333 f4 444 # 设置多个哈希
(integer) 4
127.0.0.1:6379> hkeys key # 获取哈希中所有的字段
1) "f1"
2) "f2"
3) "f3"
4) "f4"
那这个操作还是有风险的,类似于之前的keys *。主要是咱们也不知道某个hash中是否会存在大量的field,如果查询数据过多就会发送阻塞,就会导致大量请求转移到数据库,最终数据库也支撑不住,那系统就会出现故障。
1.5 hvals
hvals与hkeys相对,他能获取到hash中所有的value。
语法:hvals key
时间复杂度:O(N),N指的是哈希的元素个数(field的个数)
127.0.0.1:6379> hset key f1 111 f2 222 f3 333 f4 444 # 设置多个哈希
(integer) 4
127.0.0.1:6379> hvals key # 获取哈希中所有的value
1) "111"
2) "222"
3) "333"
4) "444"
如果哈希存储的元素也非常多,同样会导致Redis服务器被阻塞住。
1.6 hgetall、hmget
hgetall就相当于把hkeys和hvals结合起来了。
语法:hgetall key
127.0.0.1:6379> hset key f1 111 f2 222 f3 333 f4 444 # 设置多个哈希
(integer) 4
127.0.0.1:6379> hgetall key # 获取所有的field和value
1) "f1"
2) "111"
3) "f2"
4) "222"
5) "f3"
6) "333"
7) "f4"
8) "444"
如果哈希存储的元素也非常多,同样会导致Redis服务器被阻塞住。
那我们一般多数情况下并不需要查询所有的field,只需要查询当中的几个field,我们就可以使用hmget,一次可以查询多个field。
语法:hmget key field1 [field2 …]
127.0.0.1:6379> hmget key f1 f2 f3 # 一次查询多个field
# 多个value的顺序和field的顺序是匹配的
1) "111"
2) "222"
3) "333"
相对应的hmset也存在,但是hset就已经能够一次设置多个键值对了,所以我们一般不会去使用hmset。
那我们之前说hkeys、hvals、hgetall都是存在风险的,hash的元素个数太多,那执行的耗时就会比较长,从而阻塞Redis。那我们就可以使用hscan遍历Redis的hash,它属于“渐进式遍历”,也就是一次遍历一点点,多使用几次就可以完成整个遍历过程了,我们有机会再介绍。
1.7 hlen
hlen是用来获取hash中所有字段的个数。
语法:hlen key
时间复杂度:O(1)
内部并不是通过遍历的方式去计算个数,可以通过变量存储元素个数。
127.0.0.1:6379> hset key f1 111 f2 222 f3 333 f4 444
(integer) 4
127.0.0.1:6379> hlen key # 获取hash中所有字段的个数
(integer) 4
1.8 hsetnx
与之前讲过的setnx类似,不存在的时候才能设置成功。如果存在,则设置失败。
语法:hsetnx key field value
127.0.0.1:6379> hset key f1 111 f2 222 f3 333 f4 444
(integer) 4
# 目前已经有f1 f2 f3 f4了
127.0.0.1:6379> hsetnx key f5 555 # 不存在才能设置成功
(integer) 1
127.0.0.1:6379> hsetnx key f5 666 # 存在就会设置失败
(integer) 0
127.0.0.1:6379> hget key f5 # f5的值还是555,代表设置失败
"555"
1.9 hincrby、hincrbyfloat
hash中的value同样可以当做数字来处理。所以hincrby就可以加减整数,hincrbyfloat就可以加减小数。
时间复杂度:O(1)
127.0.0.1:6379> hincrby key f1 10 # 对key中的f1进行+10操作
(integer) 121
127.0.0.1:6379> hget key f1 # 查看key中的f1的值
"121"
127.0.0.1:6379> hincrby key f1 -10 # 对key中的f1进行-10操作
(integer) 111
127.0.0.1:6379> hincrbyfloat key f1 3.14 # 对key中的f1进行+3.14操作
"114.14"
127.0.0.1:6379> hincrbyfloat key f1 -3.14 # 对key中的f1进行-3.14操作
"111"
二. hash的编码方式
哈希内部的编码方式有两种:
ziplist(压缩列表):他的目的是节省内存空间,但是ziplist付出的代价就是进行读写元素速度是比较慢的,如果元素个数少,那慢的就不太明显;如果元素个数比较多,那ziplist速度就会比较慢。
hashtable(哈希表)
所以我们采取的策略是:
如果哈希中的元素个数比较少,使用ziplist存储;如果哈希中的元素个数比较多,使用hashtable来表示。
如果每个value的值长度都比较短,使用ziplist来去存储;如果某个value的长度太长,还是会转化成hashtable。
也就是说,只有哈希中的元素个数比较少&&每个value的值长度都比较短的情况下,才会使用ziplist去存储。
那我们也可以在配置文件中配置hash-max-ziplist-entries(默认512个)来去设置哈希中的元素个数的标准,配置hash-max-ziplist-value(默认64字节)来去设置每个value的值的长度的标准。
那我们同样可以通过object encoding来去查看具体的编码方式。
127.0.0.1:6379> hset key f1 111 # 设置一个短的value
(integer) 1
127.0.0.1:6379> object encoding key # 获取哈希的具体编码方式
"ziplist" # 长度比较短的时候使用ziplist
# 设置一个长的value
127.0.0.1:6379> hset key2 k2 2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
(integer) 1
127.0.0.1:6379> object encoding key2
"hashtable" # 长度比较长的时候使用hashtable
三. 使用场景:作为缓存
我们之前讲过,string也是可以作为缓存使用的。那如果要存储结构化的数据,使用hash会更适合一些。
比如我们想要存储这个数据:
那我们通过哈希类型存储到Redis的缓存中就是这个样子:
那通过哈希,就很容易的将关系型的数据表示出来保存到缓存中。
那上述场景我们通过string类型也能做到,就需要使用到JSON这样的结构来去存储。但是如果使用string(JSON)的格式来去表示相关信息,那万一我们只想获取/修改其中的某个field,就需要把整个JSON读取出来,解析成对象然后进行修改,修改完还需要再转化回JSON格式的字符串写入到Redis中,非常麻烦。
如果使用hash的方式来表示相关信息,就可以使用field表示对象的相关属性(也就是数据表的每个列),此时就可以非常方便的修改/获取任何一个属性的值了。
但是也有可能会存在一个问题,我们需要控制哈希在ziplist和hashtable两种内部编码的转换,这样就可能会造成内存的消耗(比如转化成哈希表类型,就会有大量元素空缺导致内存浪费)。
那除了string类型、hash类型能够存储字符串以外,我们还可以使用原生字符串类型来去表示一个复杂的key。我们刚才讲的string的处理方式是通过value来去表示信息,使用原生字符串类型来去表示一个复杂的key这种是通过key来去表示信息。
比如:set user:1:name James,set user:1:age 23……
但是我们并不推荐大家这样做,他把同一个数据的各个属性给分散开了。不满足高内聚的特点。
那我们比对一下MySQL和Redis的哈希类型存储数据的区别。那我们MySQL对于数据的格式要求特别严格,如果某列不存在我们就需要置为null。而Redis则不是,某列不存在那我们就不去存储他,比如user:1的James就没存储age的信息。
另外,关系型数据库是可以做复杂的关系查询的,比如:联表查询、聚合查询等等,而Redis想要进行这样的功能就需要我们自己的代码手动实现。
那我们的key也存储了用户的ID,而field中也存储了ID,那我们field-value中的uid能不能不写?其实可以不写,但是在工程中一般都会把uid在key和value中再存一份,后续编写代码操作数据会比较方便。