Redis 哈希(Hash)深度解析:Field-Value 层级、原子性与内部编码
深入解析 Redis 哈希(Hash)结构,对比了 Field-Value 与 Key-Value 的层级嵌套关系及原子性差异。介绍了 HSET、HGET 等核心命令的使用场景与性能风险,并详细阐述了 ziplist、listpack 及 hashtable 三种内部编码的触发条件、特点及转换规则,为优化 Redis 内存使用与性能提供实践指导。

深入解析 Redis 哈希(Hash)结构,对比了 Field-Value 与 Key-Value 的层级嵌套关系及原子性差异。介绍了 HSET、HGET 等核心命令的使用场景与性能风险,并详细阐述了 ziplist、listpack 及 hashtable 三种内部编码的触发条件、特点及转换规则,为优化 Redis 内存使用与性能提供实践指导。


在 Redis 中,哈希结构具有独特的深层意义,不同于常规的 Key-Value 结构,这里引入了 Field 的概念。
Redis 自身已经是键值对结构,通过哈希方式组织。当 key 这一层组织完成后,value 的其中一种类型还可以再是哈希。
形如 key="key",value={{field1, value1}, …, {fieldN, valueN}},Redis 键值对和哈希类型的关系如下所示。

在 Redis 中,Field-Value 和 Key-Value 是层级嵌套关系。其中 key 是全局唯一标识符,指向一个哈希(Hash)结构;而 field 是该哈希内部的字段名,与对应的 value 组成键值对,存储在哈希中。
key 标识。例如,user:1001 可能是一个全局 Key,指向某个用户的数据。user:1001 可能存储为一个哈希,包含用户的多个属性。field(字段名)和 value(字段值)组成。例如,name: "Alice"、age: 30 等。user:1001:name、user:1001:age),从而减少 Key 数量,提升管理效率。HINCRBY 递增数值),无需锁定整个哈希或全局 Key,适合高并发场景。假设需要存储用户信息:
HSET user:1001 age 31 原子性更新年龄;内存占用更低。有 Field 的设计(使用哈希):
Key: user:1001 Field: name, Value: "Alice"
Field: age, Value: 30
无 Field 的设计:
Key: user:1001:name, Value: "Alice"
Key: user:1001:age, Value: 30
| 特性 | Key-Value(全局) | Field-Value(哈希内部) |
|---|---|---|
| 作用域 | 整个 Redis 数据库 | 单个哈希结构内部 |
| 唯一性 | 全局唯一 | 在哈希内唯一 |
| 典型操作 | GET key、SET key value | HGET key field、HSET key field value |
| 设计目的 | 标识数据对象 | 描述对象属性 |
在 Redis 中,Field-Value(哈希内部的字段值)的原子性操作是相对于哈希结构而言的,而Key-Value(全局键值对)的原子性操作是针对整个键的。两者的原子性范围不同,导致它们的特性有所差异。
SET、HSET、INCR 等)。Redis 为哈希(Hash)提供了针对单个字段的原子操作命令,例如:
HSET key field value:设置字段值。HGET key field:获取字段值。HINCRBY key field increment:原子性递增字段的数值。HDEL key field:删除字段。这些命令直接操作哈希中的某个字段,Redis 保证它们的执行是原子的。例如:
HINCRBY user:1001 age 1 # 原子性将 age 字段的值 +1
即使多个客户端同时执行此命令,Redis 也会通过内部锁机制确保最终结果正确(如 age 从 30 变为 31,不会出现中间状态)。
name 和 age),必须使用 HMSET 或事务(MULTI/EXEC),此时原子性扩展到整个命令或事务。email),不会影响当前字段的操作。Redis 对字符串(String)也提供原子操作,例如:
SET key value:设置键值。GET key:获取键值。INCR key:原子性递增数值。DECR key:原子性递减数值。这些命令直接操作整个键,原子性范围是全局的。例如:
INCR counter # 原子性将 counter 的值 +1
counter 时,其他客户端无法同时修改它,直到当前操作完成。user:1001:age 时,其他客户端可以同时修改 user:1001:name,两者互不干扰。SET/GET 等操作本身就是原子性的,但问题可能源于以下场景:
GET 再 SET(如 value = GET key; SET key value+1),这不是原子性的,需改用 INCR。注意 H 系列的命令必须要保证 key 对应的 value 是哈希类型的!!!
设置 hash 中指定的字段(field)的值(value)。
HSET key field value [field value ...]
时间复杂度:插入一组 field 为 O(1),插入 N 组 field 为 O(N)。
返回值:添加的字段的个数。
示例:
redis> HSET myhash field1 "Hello" (integer) 1
redis> HGET myhash field1 "Hello"
获取 hash 中指定字段的值。
HGET key field
时间复杂度 O(1)。
返回值:字段对应的值或者 nil。
示例:
redis> HSET myhash field1 "foo" (integer) 1
redis> HGET myhash field1 "foo"
redis> HGET myhash field2 (nil)
判断 hash 中是否有指定的字段。
HEXISTS key field
时间复杂度:O(1)。
返回值:1 表示存在,0 表示不存在。
示例:
redis> HSET myhash field1 "foo" (integer) 1
redis> HEXISTS myhash field1 (integer) 1
redis> HEXISTS myhash field2 (integer) 0
删除哈希中指定的字段。
HDEL key field [field ...]
时间复杂度:删除一个元素为 O(1),删除 N 个元素为 O(N)。
返回值:本次操作删除的字段个数。
示例:
redis> HSET myhash field1 "foo" (integer) 1
redis> HDEL myhash field1 (integer) 1
redis> HDEL myhash field2 (integer) 0
获取 hash 中所有的字段。
HKEYS key
时间复杂度:O(N),N 为 field 的个数。
返回值:字段列表。
示例:
redis> HSET myhash field1 "Hello" (integer) 1
redis> HSET myhash field2 "World" (integer) 1
redis> HKEYS myhash
1) "field1"
2) "field2"
注意:这个操作也是存在一定的风险的!!!类似于之前介绍过的 keys 命令。 主要是咱们也不知道某个 hash 中是否会存在大量的 field~
获取 hash 中的所有的值。
语法:
HVALS key
时间复杂度:O(N),N 为 field 的个数。
返回值:所有的值。
示例:
redis> HSET myhash field1 "Hello" (integer) 1
redis> HSET myhash field2 "World" (integer) 1
redis> HVALS myhash
1) "Hello"
2) "World"
注意如果哈希非常大,这个操作就可能导致 redis 服务器被阻塞。
获取 hash 中所有字段以及对应的值。
HGETALL key
时间复杂度:O(N),N 为 field 的个数。
返回值:字段和对应的值。
示例:
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"
一次获取 hash 中多个字段的值。
HMGET key field [field ...]
时间复杂度:只查询一个元素为 O(1),查询多个元素为 O(N),N 为查询元素个数。
返回值:字段对应的值或者 nil。

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 命令,该命令采用渐进式遍历哈希类型~
敲一次命令,遍历一小部分, 再敲一次,再遍历一小部分 化整为零 连续执行多次,就可以完成整个的遍历过程了
在 Redis 中,哈希(Hash)类型的内部编码主要有 ziplist(压缩列表) 和 hashtable(哈希表) 两种,在 Redis 7.0 及以后版本中,ziplist 被 listpack(紧凑列表)替代,但核心设计思想类似。以下是具体说明:
hash-max-ziplist-entries(默认 512 个)。hash-max-ziplist-value(默认 64 字节)。示例:
127.0.0.1:6379> HMSET user:1 name "Alice" age 30 OK
127.0.0.1:6379> OBJECT ENCODING user:1 "ziplist"
hash-max-ziplist-entries。hash-max-ziplist-value。示例:
127.0.0.1:6379> HSET user:2 info "This is a long string that exceeds 64 bytes..." OK
127.0.0.1:6379> OBJECT ENCODING user:2 "hashtable"
可通过修改 Redis 配置文件(redis.conf)或运行时使用 CONFIG SET 命令调整哈希的内部编码行为:
# 调整 ziplist/listpack 的最大元素数量(默认 512)
hash-max-ziplist-entries 1024
# 调整 ziplist/listpack 中单个元素的最大值大小(默认 64 字节)
hash-max-ziplist-value 128
使用字符串类型,每个属性一个键。
set user:1:name James
set user:1:age 23
set user:1:city Beijing
优点:实现简单,针对个别属性变更也很灵活。
缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,所以这种方案基本没有实用性。
例如 JSON 格式。
set user:1 经过序列化后的用户对象字符串
优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
缺点:本身序列化和反序列化需要一定开销,同时如果总是操作个别属性则非常不灵活。
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。
Redis 根据哈希的 字段数量 和 字段值大小 动态选择内部编码,目的是:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online