Redis 常见数据类型-Hash 类型
类型简介
⼏乎所有的主流编程语⾔都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中,哈希类型是指值本⾝
⼜是⼀个键值对结构,形如 key = “key”,value = { {field1, value1 }, …, {fieldN, valueN } },Redis 键值对和哈希类型⼆
者的关系可以⽤下面这张图来表⽰
flowchart TD
subgraph String_Storage
A[key: user:1:name] --> B[value: James]
C[key: user:1:age] --> D[value: 28]
end
subgraph Hash_Storage
E[key: user:1] --> F[field: name]
F --> G[value: James]
E --> H[field: age]
H --> I[value: 28]
end
相关指令
不同于以往的 set 和 get,Hash 会采用专门的指令来设置 Hash 或获取 Hash 中特定 field 所对应的 value
1、hset 指令
语法格式如下:
HSET key field value [field value ...]
和 String 类型一样,利用该命令可同时生成多组键值对(field-value),其中返回值对应的是成功添加的字段的个数
# 生成含有一个键值对的 hash 数据
127.0.0.1:6379> hset k1 f1 v1
(integer) 1
# 生成含有多个键值对的 hash 数据
127.0.0.1:6379> hset k2 f1 v1 f2 v2 f3 v3
(integer) 3
2、hget 指令
该指令用于获取 hash 中指定字段的值
语法格式如下:
HGET key field
返回值:若字段存在则返回字段所对应的值,否则返回 nil
使用示例如下:
# 查询 k1 中 f1 所对应的值
127.0.0.1:6379> hget k1 f1
"v1"
# 查询不存在的值(返回nil)
127.0.0.1:6379> hget k1 f2
(nil)
# 查询 k2 中 f3 所对应的值
127.0.0.1:6379> hget k2 f3
"v3"
3、hexists
该指令用于判断 hash 中某个字段是否存在
语法格式如下:
HEXISTS key field
返回值:1 表示存在,0 表示不存在
使用示例:
127.0.0.1:6379> hexists k1 f1
(integer) 1
127.0.0.1:6379> hexists k1 f2
(integer) 0
4、hdel
该指令用于删除 hash 中某个字段
返回值:本次操作删除的字段个数
语法格式如下:
HDEL key field [field ...]
若一个 hash 值中的所有字段都被删除了,那么其 hash 也会被删除
使用示例如下:
# 删除一个字段
127.0.0.1:6379> hdel k2 f2
(integer) 1
# 删除两个字段,因为 f2 字段刚刚已经被删掉了
127.0.0.1:6379> hdel k2 f1 f2 f3
(integer) 2
# k2 已经被删除
127.0.0.1:6379> exists k2
(integer) 0
注意:不要混淆 exists 和 hexists 以及 del 和 hdel,前者是用来操作 key 值的,而后者是用来操作 key 中指定字段的
示例如下:
# 之前的 k2 已经被删除,直接生成一个字符串类型 k2
127.0.0.1:6379> set k2 v2
OK
# exists 和 del 不管用户给的是什么数据类型,直接操控和访问的是 key
127.0.0.1:6379> exists k1 k2
(integer) 2
127.0.0.1:6379> del k1 k2
(integer) 2
5、hkeys、hvals、hgetall
- hkeys 用于查询指定的 hash 中的所有字段
- hvals 用于查询指定的 hash 中的所有字段所对应的值
- hgetall 综合了 hkeys 和 hvals 两者,返回指定的 hash 中的所有字段及字段所对应的值
格式如下:
# hkeys
HKEYS key
# hvals
HVALS key
# hgetall
HGETALL key
使用示例如下:
127.0.0.1:6379> hset key f1 v1 f2 v2 f3 v3
(integer) 3
# hkeys
127.0.0.1:6379> hkeys key
1) "f1"
2) "f2"
3) "f3"
# hvals
127.0.0.1:6379> hvals key
1) "v1"
2) "v2"
3) "v3"
# hgetall
127.0.0.1:6379> hgetall key
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
6、hmget
相较于 hget,该指令可以一次获取多个值
格式如下:
HMGET key field [field ...]
用法和 hget 类似,下面是使用示例:
# 书接上回
127.0.0.1:6379> hmget key f1 f2 f3
1) "v1"
2) "v2"
3) "v3"
7、hlen
顾名思义,这个命令是用来获取 hash 中字段个数的,格式如下:
HLEN key
使用示例如下:
# 书接上回
127.0.0.1:6379> hlen key
(integer) 3
8、hsetnx
该命令用于在字段不存在的情况下,设置 hash 中的字段和值,格式如下:
HSETNX key field value
返回值:1 表示成功,0 表示失败
用法如下:
# 成功
127.0.0.1:6379> hsetnx k1 f1 v1
(integer) 1
# 成功
127.0.0.1:6379> hsetnx k1 f2 v2
(integer) 1
# 失败
127.0.0.1:6379> hsetnx k1 f2 v2
(integer) 0
9、hincrby、hincrbyfloat
hincrby 用于整数进行加减运算,而 hincrbyfloat 用于浮点数运算
返回值:计算之后的结果
格式如下:
# hincrby
HINCRBY key field increment
# hincrbyfloat
HINCRBYFLOAT key field increment
需要注意的是,hincrby 操作时 value 必须是 int 类型,但若通过 hincrbyfloat 对 int 类型进行操作后,
即便操作的是整数的加减,也会导致 value 变成非 int 类型
使用示例如下:
127.0.0.1:6379> hset k1 f1 5
(integer) 1
127.0.0.1:6379> hincrby k1 f1 6
(integer) 11
127.0.0.1:6379> hincrbyfloat k1 f1 6
"17"
127.0.0.1:6379> hincrbyfloat k1 f1 6.5
"23.5"
127.0.0.1:6379> hincrbyfloat k1 f1 3.3
"26.8"
127.0.0.1:6379> hincrbyfloat k1 f1 -4.4
"22.4"
# value 已不再是 int 类型,无法使用 hincrby 进行操作
127.0.0.1:6379> hincrby k1 f1 -6
(error) ERR hash value is not an integer
hash 相关指令汇总表
| 命令 | 执行效果 | 时间复杂度 |
|---|---|---|
| hset key field value | 设置值 | O(1) |
| hget key field | 获取值 | O(1) |
| hdel key field [field …] | 删除 field | O(k), k 是 field 个数 |
| hlen key | 计算 field 个数 | O(1) |
| hgetall key | 获取所有的 field-value | O(k), k 是 field 个数 |
| hmget field [field …] | 批量获取 field-value | O(k), k 是 field 个数 |
| hmset field value [field …] | 批量设置 field-value | O(k), k 是 field 个数 |
| hexists key field | 判断 field 是否存在 | O(1) |
| hkeys key | 获取所有的 field | O(k), k 是 field 个数 |
| hvals key | 获取所有的 value | O(k), k 是 field 个数 |
| hsetnx key field value | 设置值,但必须在 field 不存在时才能设置成功 | O(1) |
| hincrby key field n | 对应 field-value +n | O(1) |
| hincrbyfloat key field n | 对应 field-value +n | O(1) |
| hstrlen key field | 计算 value 的字符串长度 | O(1) |
内部编码
哈希的内部编码有两种:
ziplist(压缩列表):当哈希类型元素个数⼩于 hash-max-ziplist-entries 配置(默认 512 个)、同时所有值都
⼩于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使⽤ ziplist 作为哈希的内部实现,ziplist 使
⽤更加紧凑的结构实现多个元素的连续存储,所以在节省内存⽅⾯⽐ hashtable 更加优秀。hashtable(哈希表):当哈希类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ hashtable 作为哈希
的内部实现,因为此时 ziplist 的读写效率会下降,⽽ hashtable 的读写时间复杂度为 O(1)。
若哈希中字段数量过多或者 value 过长都会使得存储方式变为 hashtable,而具体数值是可以在 conf 文件中被配置的:
# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-listpack-entries 512
hash-max-listpack-value 64
下面是不同长度的 value 存储方式的区别:
127.0.0.1:6379> hset k1 f1 v1
(integer) 1
127.0.0.1:6379> OBJECT ENCODING k1
"listpack"
127.0.0.1:6379> hset k1 f2 vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv22222222222222222222222222222222222222222222222222222222
(integer) 1
# value 太长,类型变为 hashtable
127.0.0.1:6379> OBJECT ENCODING k1
"hashtable"
使用场景:缓存
在关系型数据库当中,数据的存储是必须争对每一列属性都有对应的值的,而通过哈希表的方式存储,可以按需插入数据,且看起来更加直观,并且
更新起数据更加灵活。
二者各有各的好处,若使用关系型数据库进行数据的存储,虽然会消耗更多的空间,但相对于着可以进行更多复杂的操作,若使用哈希进行数据的操作,
操作可行性更低(能做的操作更加有限),当然,好处就是哈希所消耗的空间更少。因此,具体使用哪种方式得根据具体实际去讨论。当然,这两者并
不冲突,因为哈希是用作缓存的,而关系型数据库是直接用于存储的,很多时候是两者相结合着一起使用,对于复杂的表操作固然选择使用关系型数据库
进行操作,而对于一些简单数据操作,则选择直接对缓存进行操作,因此,我们对这两者的比较并不是为了分出胜负来,因为我们正是因为它们各有优劣
才选择将两者结合起来使用的,不要忘了我们的初衷,Redis 更多是为了帮助忙不过来的关系型数据库服务的。
关系型数据表保存用户信息:
| uid | name | age | city |
|---|---|---|---|
| 1 | James | 28 | Beijing |
| 2 | Johnathan | 30 | Xian |
映射关系表示用户信息
| key | field | value |
|---|---|---|
| user:1 | uid | 1 |
| name | James | |
| age | 28 | |
| city | Beijing | |
| user:2 | uid | 2 |
| name | Johnathan | |
| age | 30 | |
| city | Xian |
不同缓存方式之间的比较
下面我们需要讨论三种不同的缓存方式:
原生字符串类型⸺使⽤字符串类型,每个属性⼀个键
set user:1:name James
set user:1:age 23
set user:1:city Beijing
优点:实现简单,针对个别属性变更也很灵活。
缺点:占⽤过多的键,内存占⽤量较⼤,同时⽤⼾信息在 Redis 中⽐较分散,缺少内聚性,所以这种
⽅案基本没有实⽤性。
序列化字符串类型,例如 JSON 格式
set user:1 经过序列化后的⽤⼾对象字符串
优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内
存的使⽤效率很⾼。
缺点:本⾝序列化和反序列需要⼀定开销,同时如果总是操作个别属性则⾮常不灵活。
哈希类型
hmset user:1 name James age 23 city Beijin
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较⼤消耗。
相⽐于使⽤ JSON 格式的字符串缓存⽤⼾信息,哈希类型变得更加直观,并且在更新操作上变得更灵活。可以将每个⽤⼾的 id 定义
为键后缀,多对 field-value 对应⽤⼾的各个属性,类似如下伪代码
/**
* 根据用户ID获取用户信息(缓存优先)
* @param uid 用户ID
* @return 用户信息对象,未找到时返回null
*/
UserInfo getUserInfo(long uid) {
// 1. 构建Redis键
String key = "user:" + uid;
// 2. 尝试从Redis获取缓存数据
Map<String, String> userInfoMap = Redis执行命令:hgetall key;
// 3. 缓存命中处理
if (userInfoMap != null && !userInfoMap.isEmpty()) {
// 将Redis哈希数据转换为UserInfo对象
UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
return userInfo;
}
// 4. 缓存未命中,查询数据库
UserInfo userInfo = MySQL执行SQL:select * from user_info where uid = <uid>;
// 5. 处理未找到用户的情况
if (userInfo == null) {
// 记录日志或响应404
return null;
}
// 6. 写入Redis缓存
Redis执行命令:hmset key
name userInfo.name
age userInfo.age
city userInfo.city;
// 7. 设置缓存过期时间(1小时)
Redis执行命令:expire key 3600;
// 8. 返回用户信息
return userInfo;
}