【Redis】哈希类型详解及缓存方式对比:从命令操作到实际应用场景

目录


Hash 哈希

⼏乎所有的主流编程语⾔都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中,哈希类型是指值本身又是⼀个键值对结构(即 value 也是一个键值对),形如 key = "key",value = { {field1, value1 }, ..., {fieldN, valueN } },Redis 键值对和哈希类型⼆者的关系可以⽤图 2-15 来表⽰。

图 2-15 字符串和哈希类型对⽐

哈希类型中的映射关系通常称为 field-value(里层value),⽤于区分 Redis 整体的键值对(key-value(外层value)),注意这⾥的 value 是指 field(可以理解为,这个 field 就是 外层value 里的 key,这里的 value 是 里层value)对应的值,不是键(key)对应的值,请注意 value 在不同上下⽂的作⽤。

命令

HSET

hset

设置 hash 中指定的字段(field)的值(value)。这里的 value 只能是字符串

语法:

cmd 复制代码
HSET key field value [field value ...]

命令有效版本:2.0.0 之后

时间复杂度:插⼊⼀组 field 为 O(1), 插⼊ N 组 field 为 O(N)

返回值:添加的字段的个数。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HGET myhash field1
"Hello"

HGET

hget

获取 hash 中指定字段的值。

语法:

cmd 复制代码
HGET key field

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:字段对应的值或者 nil。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "foo"
(integer) 1
redis> HGET myhash field1
"foo"
redis> HGET myhash field2
(nil)

HEXISTS

hexists

判断 hash 中是否有指定的字段。

语法:

cmd 复制代码
HEXISTS key field

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:1 表⽰存在,0 表⽰不存在。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "foo"
(integer) 1
redis> HEXISTS myhash field1
(integer) 1
redis> HEXISTS myhash field2
(integer) 0

HDEL

hdel

删除 hash 中指定的字段。

语法:

cmd 复制代码
HDEL key field [field ...]

命令有效版本:2.0.0 之后

时间复杂度:删除⼀个元素为 O(1). 删除 N 个元素为 O(N).

返回值:本次操作删除的字段个数。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "foo"
(integer) 1
redis> HDEL myhash field1
(integer) 1
redis> HDEL myhash field2
(integer) 0

HKEYS

hkeys

获取 hash 中的所有字段。

语法:

cmd 复制代码
 HKEYS key

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数。这个操作是先根据 key 找到对应的 hash,然后再遍历 hash。

返回值:字段列表。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HKEYS myhash
1) "field1"
2) "field2"

HVALS

hvals

获取 hash 中的所有的值。

语法:

cmd 复制代码
HVALS key

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数.

返回值:所有的值。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HVALS myhash
1) "Hello"
2) "World"

HGETALL

hgetall

获取 hash 中的所有字段以及对应的值。

语法:

cmd 复制代码
HGETALL key

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数.

返回值:字段和对应的值。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HGETALL myhash
1) "field1"
2) "Hello"
3) "field2"
4) "World"

HMGET

hmget

⼀次获取 hash 中多个字段的值。

语法:

cmd 复制代码
HMGET key field [field ...]

命令有效版本:2.0.0 之后

时间复杂度:只查询⼀个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.

返回值:字段对应的值或者 nil。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HMGET myhash field1 field2 nofield
1) "Hello"
2) "World"
3) (nil)

上述 hkeys、hvals、hgetall 都是存在一定风险的,这些都是一条命令就能完成所有的遍历操作,hash 元素过多时就很容易阻塞 Redis。

在使⽤ HGETALL 时,如果哈希元素个数⽐较多,会存在阻塞 Redis 的可能。如果开发⼈员只需要获取部分 field,可以使⽤ HMGET,如果⼀定要获取全部 field,可以尝试使⽤ HSCAN 命令,该命令采⽤渐进式遍历哈希类型,HSCAN 会在后续章节介绍。
有 hmget,那有 hmset 一次设置多个 field 和 value 吗?

有的,但是 hset 已经支持这样了,所以用不到

HLEN

hlen

获取 hash 中的所有字段的个数。这个获取 hash 的元素个数,不需要遍历的

语法:

cmd 复制代码
HLEN key

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:字段个数。

⽰例:

cmd 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HLEN myhash
(integer) 2

HSETNX

hsetnx

在字段不存在的情况下,设置 hash 中的字段和值。不存在才能设置成功,存在就设置失败

语法:

cmd 复制代码
HSETNX key field value

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:1 表⽰设置成功,0 表⽰失败。

⽰例:

cmd 复制代码
redis> HSETNX myhash field "Hello"
(integer) 1
redis> HSETNX myhash field "World"
(integer) 0
redis> HGET myhash field
"Hello"

HINCRBY

hincrby

将 hash 中字段对应的数值添加指定的值。hash 这里的 value,也可以当做数字来处理

语法:

cmd 复制代码
HINCRBY key field increment

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:该字段变化之后的值。

⽰例:

cmd 复制代码
redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HINCRBY myhash field -10
(integer) -5

HINCRBYFLOAT

hincrbyfloat

HINCRBY 的浮点数版本。

语法:

cmd 复制代码
HINCRBYFLOAT key field increment

命令有效版本:2.6.0 之后

时间复杂度:O(1)

返回值:该字段变化之后的值。

⽰例:

cmd 复制代码
redis> HSET mykey field 10.50
(integer) 1
redis> HINCRBYFLOAT mykey field 0.1
"10.6"
redis> HINCRBYFLOAT mykey field -5
"5.6"
redis> HSET mykey field 5.0e3
(integer) 0
redis> HINCRBYFLOAT mykey field 2.0e2
"5200"
命令⼩结

表 2-4 是哈希类型命令的效果、时间复杂度,开发⼈员可以参考此表,结合⾃⾝业务需求和数据⼤⼩选择合适的命令。

表 2-4 哈希类型命令⼩结

命令 执⾏效果 时间复杂度
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个数
hmset field [field value ...] 批量获取 field-value O(k), k 是 field个数
hmset field value [field value ...] 批量获取 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)

下⾯的⽰例演⽰了哈希类型的内部编码,以及响应的变化。

  1. 当 field 个数⽐较少且没有⼤的 value 时,内部编码为 ziplist:
cmd 复制代码
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
OK
127.0.0.1:6379> object encoding hashkey
"ziplist"
  1. 当有 value ⼤于 64 字节时,内部编码会转换为 hashtable:
cmd 复制代码
127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... 省略 ..."
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
  1. 当 field 个数超过 512 时,内部编码也会转换为 hashtable:
cmd 复制代码
127.0.0.1:6379> hmset hashkey f1 v1 h2 v2 f3 v3 ... 省略 ... f513 v513
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
使⽤场景

图 2-16 为关系型数据表记录的两条⽤⼾信息,⽤⼾的属性表现为表的列,每条⽤⼾信息表现为⾏。如果映射关系表⽰这两个⽤⼾信息,则如图 2-17 所⽰。

图 2-16 关系型数据表保存⽤⼾信息

图 2-17 映射关系表⽰⽤⼾信息

相⽐于使⽤ JSON 格式的字符串缓存⽤⼾信息,哈希类型变得更加直观,并且在更新操作上变得更灵活。可以将每个⽤⼾的 id 定义为键后缀,多对 field-value 对应⽤⼾的各个属性,类似如下伪代码:

java 复制代码
UserInfo getUserInfo(long uid) {
    // 根据 uid 得到 Redis 的键
    String key = "user:" + uid;

    // 尝试从 Redis 中获取对应的值
    userInfoMap = Redis 执⾏命令:hgetall key;

    // 如果缓存命中(hit)
    if (value != null) {
        // 将映射关系还原为对象形式
        UserInfo userInfo = 利⽤映射关系构建对象(userInfoMap);
        return userInfo;
    }

    // 如果缓存未命中(miss)
    // 从数据库中,根据 uid 获取⽤⼾信息
    UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>

    // 如果表中没有 uid 对应的⽤⼾信息
    if (userInfo == null) {
        响应 404
        return null;
    }

    // 将缓存以哈希类型保存
    Redis 执⾏命令:hmset key name userInfo.name age userInfo.age city userInfo.c

    // 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
    Redis 执⾏命令:expire key 3600

    // 返回⽤⼾信息
    return userInfo;
}

但是需要注意的是哈希类型和关系型数据库有两点不同之处:

  • 哈希类型是稀疏的 ,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,⽽关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为 null,如图 2-18 所⽰。
  • 关系数据库可以做复杂的关系查询,⽽ Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本⾼。

图 2-18 关系型数据库稀疏性

为什么这里存储用户的时候,key 里已经有个1了,为什么 value 里还要再存个 uid?

如果确实不想存也可以,但是在工程实践,都会把 uid 在 value 中再存一份,这样后续写到相关的代码使用起来会比较方便

缓存方式对比

截⾄⽬前为⽌,我们已经能够⽤三种⽅法缓存⽤⼾信息,下⾯给出三种⽅案的实现⽅法和优缺点分析。

  1. 原⽣字符串类型⸺使⽤字符串类型,每个属性⼀个键。
cmd 复制代码
set user:1:name James
set user:1:age 23
set user:1:city Beijing

优点:实现简单,针对个别属性变更也很灵活。

缺点:占⽤过多的键,内存占⽤量较⼤,同时⽤⼾信息在 Redis 中⽐较分散,缺少内聚性,所以这种⽅案基本没有实⽤性。

  1. 序列化字符串类型,例如 JSON 格式
cmd 复制代码
set user:1 经过序列化后的⽤⼾对象字符串

优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内存的使⽤效率很⾼。

缺点:本⾝序列化和反序列需要⼀定开销,同时如果总是操作个别属性则⾮常不灵活。

  1. 哈希类型
cmd 复制代码
hmset user:1 name James age 23 city Beijing

优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。

缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较⼤消耗。

高内聚:把有关联的东西放在一起,最好能放在指定的地方

低耦合:两个模块/代码之间的关联关系,关系越小,越容易相互影响,就认为耦合越小。避免"牵一发动全身"

相关推荐
极客天成ScaleFlash4 小时前
极客天成NVFile:无缓存直击存储性能天花板,重新定义AI时代并行存储新范式
人工智能·缓存
爱的叹息5 小时前
RedisTemplate 的 6 个可配置序列化器属性对比
算法·哈希算法
morris1315 小时前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
爱的叹息7 小时前
spring boot集成reids的 RedisTemplate 序列化器详细对比(官方及非官方)
redis
weitinting8 小时前
Ali linux 通过yum安装redis
linux·redis
纪元A梦9 小时前
Redis最佳实践——首页推荐与商品列表缓存详解
数据库·redis·缓存
爱的叹息16 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
松韬17 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
天上掉下来个程小白17 小时前
Redis-14.在Java中操作Redis-Spring Data Redis使用方式-操作列表类型的数据
java·redis·spring·springboot·苍穹外卖
·云扬·18 小时前
深度剖析 MySQL 与 Redis 缓存一致性:理论、方案与实战
redis·mysql·缓存