跳到主要内容编程语言算法
Redis 哈希(Hash)深度解析:Field-Value 层级、原子性与内部编码
综述由AI生成深入解析 Redis 哈希(Hash)结构,对比了 Field-Value 与 Key-Value 的层级嵌套关系及原子性差异。介绍了 HSET、HGET 等核心命令的使用场景与性能风险,并详细阐述了 ziplist、listpack 及 hashtable 三种内部编码的触发条件、特点及转换规则,为优化 Redis 内存使用与性能提供实践指导。
赛博行者24 浏览 
引言
在 Redis 中,哈希结构具有独特的深层意义,不同于常规的 Key-Value 结构,这里引入了 Field 的概念。
Redis 自身已经是键值对结构,通过哈希方式组织。当 key 这一层组织完成后,value 的其中一种类型还可以再是哈希。
形如 key="key",value={{field1, value1}, …, {fieldN, valueN}},Redis 键值对和哈希类型的关系如下所示。

Field-Value 与 Key-Value 的深层次解析
在 Redis 中,Field-Value 和 Key-Value 是层级嵌套关系。其中 key 是全局唯一标识符,指向一个哈希(Hash)结构;而 field 是该哈希内部的字段名,与对应的 value 组成键值对,存储在哈希中。
1. 层级关系:全局 Key → 哈希结构 → 多个 Field-Value
- 全局 Key:Redis 中的每个数据对象(如字符串、列表、哈希等)都通过唯一的
key 标识。例如,user:1001 可能是一个全局 Key,指向某个用户的数据。
- 哈希结构:当全局 Key 对应的数据类型是哈希(Hash)时,其值是一个字段 - 值对的集合。例如,
user:1001 可能存储为一个哈希,包含用户的多个属性。
- Field-Value:哈希内部的每个属性由
field(字段名)和 value(字段值)组成。例如,name: "Alice"、age: 30 等。
2. Field 的定义与作用
- Field 的本质:Field 是哈希内部的键(局部键),用于区分哈希中的不同属性。它类似于编程语言中的对象属性名或数据库表中的列名。
- 为什么需要 Field:
- 结构化存储:Field 允许将多个相关属性组织在一个哈希中,避免为每个属性创建单独的全局 Key(如
user:1001:name、user:1001:age),从而减少 Key 数量,提升管理效率。
- 原子性操作:Redis 支持对哈希的单个 Field 进行原子操作(如
HINCRBY 递增数值),无需锁定整个哈希或全局 Key,适合高并发场景。
- 内存优化:哈希在存储多个小字段时比单独的字符串更节省内存,尤其是当字段数量较多时。
3. 示例说明
假设需要存储用户信息:
- 缺点(无 Field):Key 数量多,管理复杂;修改年龄需操作独立 Key,无法保证原子性。
- 优点(有 Field):Key 数量少;可通过
HSET user:1001 age 31 原子性更新年龄;内存占用更低。
Key: user:1001 Field: name, Value: "Alice"
Field: age, Value: 30
Key: user:1001:name, Value: "Alice"
Key: user:1001:age, Value: 30
4. 核心区别总结
| 特性 | Key-Value(全局) | Field-Value(哈希内部) |
|---|
| 作用域 | 整个 Redis 数据库 | 单个哈希结构内部 |
| 唯一性 | 全局唯一 | 在哈希内唯一 |
| 典型操作 | GET key、SET key value | HGET key field、HSET key field value |
| 设计目的 | 标识数据对象 | 描述对象属性 |
5. 适用场景
- 使用 Field-Value(哈希):
- 存储对象(如用户、商品、订单)。
- 需要原子性更新部分属性。
- 字段数量较多且需节省内存。
- 使用 Key-Value(字符串):
- 存储简单数据(如配置项、计数器)。
- 需要独立生命周期或全局唯一标识的场景。
为什么 Field-Value 是原子性的,而 Key-Value 不是原子性的
在 Redis 中,Field-Value(哈希内部的字段值)的原子性操作是相对于哈希结构而言的,而Key-Value(全局键值对)的原子性操作是针对整个键的。两者的原子性范围不同,导致它们的特性有所差异。
1. 原子性的定义
- 原子性:指一个操作要么完全执行,要么完全不执行,中间不会因其他操作或故障而中断。在 Redis 中,原子性通常由单命令保证(如
SET、HSET、INCR 等)。
2. 为什么 Field-Value 是原子性的?
(1)哈希结构的原子性操作
Redis 为哈希(Hash)提供了针对单个字段的原子操作命令,例如:
HSET key field value:设置字段值。
HGET key field:获取字段值。
HINCRBY key field increment:原子性递增字段的数值。
HDEL key field:删除字段。
这些命令直接操作哈希中的某个字段,Redis 保证它们的执行是原子的。例如:
即使多个客户端同时执行此命令,Redis 也会通过内部锁机制确保最终结果正确(如 age 从 30 变为 31,不会出现中间状态)。
(2)原子性范围
- 哈希的原子性仅限于单个字段:如果需要同时修改多个字段(如
name 和 age),必须使用 HMSET 或事务(MULTI/EXEC),此时原子性扩展到整个命令或事务。
- 哈希本身不是全局原子性的:如果其他客户端修改了哈希的其他字段(如
email),不会影响当前字段的操作。
3. 为什么 Key-Value(字符串)的原子性表现不同?
(1)字符串的原子性操作
Redis 对字符串(String)也提供原子操作,例如:
SET key value:设置键值。
GET key:获取键值。
INCR key:原子性递增数值。
DECR key:原子性递减数值。
这些命令直接操作整个键,原子性范围是全局的。例如:
(2)关键区别:操作范围
- 字符串的原子性是全局的:操作
counter 时,其他客户端无法同时修改它,直到当前操作完成。
- 哈希的原子性是局部的:操作
user:1001:age 时,其他客户端可以同时修改 user:1001:name,两者互不干扰。
(3)误解澄清:Key-Value 本身也是原子性的
- 字符串的
SET/GET 等操作本身就是原子性的,但问题可能源于以下场景:
- 复合操作非原子性:如果需要先
GET 再 SET(如 value = GET key; SET key value+1),这不是原子性的,需改用 INCR。
- 与哈希的对比:哈希的原子性是针对字段的,而字符串的原子性是针对键的。如果比较的是'修改哈希的多个字段' vs '修改字符串的单个键',前者需要事务,后者天然原子。
命令篇
注意 H 系列的命令必须要保证 key 对应的 value 是哈希类型的!!!
HSET
设置 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"
HGET
redis> HSET myhash field1 "foo" (integer) 1
redis> HGET myhash field1 "foo"
redis> HGET myhash field2 (nil)
HEXIST
redis> HSET myhash field1 "foo" (integer) 1
redis> HEXISTS myhash field1 (integer) 1
redis> HEXISTS myhash field2 (integer) 0
HDEL
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
HKEYS
时间复杂度: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~
HVALS
时间复杂度: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 服务器被阻塞。
HGETALL
时间复杂度: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"
HMGET
HMGET key field [field ...]
时间复杂度:只查询一个元素为 O(1),查询多个元素为 O(N),N 为查询元素个数。
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 的思想
敲一次命令,遍历一小部分,
再敲一次,再遍历一小部分
化整为零
连续执行多次,就可以完成整个的遍历过程了
哈希内部编码
在 Redis 中,哈希(Hash)类型的内部编码主要有 ziplist(压缩列表) 和 hashtable(哈希表) 两种,在 Redis 7.0 及以后版本中,ziplist 被 listpack(紧凑列表)替代,但核心设计思想类似。以下是具体说明:
1. ziplist(压缩列表)
- 适用场景:当哈希的元素数量较少且单个元素较小时,Redis 会使用 ziplist 作为内部编码。
- 触发条件:
- 元素数量小于
hash-max-ziplist-entries(默认 512 个)。
- 所有元素的值大小均小于
hash-max-ziplist-value(默认 64 字节)。
- 特点:
- 内存紧凑:ziplist 通过连续内存存储多个字段和值,减少指针开销,节省内存。
- 顺序存储:字段和值按插入顺序连续存储,适合小规模数据。
- 性能权衡:当元素数量或大小超过阈值时,读写效率会下降(需扩容或遍历)。
127.0.0.1:6379> HMSET user:1 name "Alice" age 30 OK
127.0.0.1:6379> OBJECT ENCODING user:1 "ziplist"
2. hashtable(哈希表)
- 适用场景:当哈希的元素数量较多或单个元素较大时,Redis 会切换到 hashtable 作为内部编码。
- 触发条件:
- 元素数量超过
hash-max-ziplist-entries。
- 任意元素的值大小超过
hash-max-ziplist-value。
- 特点:
- O(1) 时间复杂度:哈希表通过哈希函数直接定位字段,读写效率高。
- 内存开销较大:需维护哈希表结构(如数组、链表或红黑树),指针开销较高。
- 动态扩容:当负载因子超过阈值时,哈希表会自动扩容(rehash)。
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"
3. listpack(紧凑列表,Redis 7.0+)
- 背景:ziplist 在极端情况下(如连续更新大元素)可能引发'连锁更新'问题,导致性能抖动。Redis 7.0 引入 listpack 替代 ziplist,优化内存布局和更新效率。
- 特点:
- 内存更高效:通过改进节点编码(如前驱节点长度存储优化),减少内存碎片。
- 避免连锁更新:节点长度变更时,仅影响相邻节点,而非整个列表。
- 兼容性:与 ziplist 的 API 兼容,无需修改上层命令。
内部编码转换规则
- 单向性:编码转换仅从小内存编码(如 ziplist/listpack)向大内存编码(如 hashtable)进行,不可逆。
- 动态调整:Redis 根据配置参数和实际数据量自动选择编码,无需手动干预。
配置参数优化
可通过修改 Redis 配置文件(redis.conf)或运行时使用 CONFIG SET 命令调整哈希的内部编码行为:
hash-max-ziplist-entries 1024
hash-max-ziplist-value 128
应用场景建议
- 使用 ziplist/listpack:存储字段数量少、值较小的哈希(如用户基本信息、配置项)。
- 使用 hashtable:存储字段数量多或值较大的哈希(如商品详情、日志数据)。
缓存方式对比
原生字符串类型
set user:1:name James
set user:1:age 23
set user:1:city Beijing
缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,所以这种方案基本没有实用性。
序列化字符串类型
set user:1 经过序列化后的用户对象字符串
优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
缺点:本身序列化和反序列化需要一定开销,同时如果总是操作个别属性则非常不灵活。
哈希类型
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。
哈希的两种编码为什么需要自动转换?
Redis 根据哈希的 字段数量 和 字段值大小 动态选择内部编码,目的是:
(1)避免极端情况下的性能或内存问题
- 如果始终用 ziplist:
当哈希字段数量或值过大时,ziplist 的插入/删除操作会变得非常低效(需频繁移动内存),甚至可能引发内存碎片或 OOM(内存不足)。
- 如果始终用 hashtable:
对小数据使用 hashtable 会浪费内存(指针开销占比高),且哈希表的初始化成本高于 ziplist。
(2)适应不同业务场景的需求
- 读多写少的小数据:
ziplist 的内存优势更明显,适合配置类、缓存类数据。
- 高并发写或大数据:
hashtable 的 O(1) 操作效率更关键,适合热点数据或频繁更新的场景。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online