Appearance
Hash详解
一、Hash类型是什么?
Redis的Hash(哈希)是一种键值对的集合,但它的值(Value)本身也是一个键值对结构(Field-Value)。换句话说,Hash类型是“键(Key)→ 字段(Field)→ 值(Value)”的三级映射。
示例:存储用户信息时,
user:1000
是Hash的键,name
、age
、email
是字段(Field),对应的值是"张三"
、25
、"zhangsan@example.com"
。bashHSET user:1000 name "张三" age 25 email "zhangsan@example.com" HGET user:1000 name # 返回"张三"
应用场景:
- 存储对象(如用户、商品、订单的属性);
- 统计信息(如文章的阅读量、点赞数、评论数);
- 替代多个String键(减少键前缀的内存浪费)。
二、Hash类型的底层原理(核心)
Redis的Hash类型没有固定的底层结构,而是根据数据量大小和数据长度动态选择两种编码方式:
- 压缩列表(ziplist):适合小数据量的Hash(默认场景);
- 哈希表(hashtable):适合大数据量的Hash(当数据超过阈值时转换)。
这种自适应编码是Redis优化内存和性能的关键策略。
1. 压缩列表(ziplist):小数据的内存优化方案
定义:ziplist是Redis设计的一种紧凑的链表结构,用于存储多个小数据(字符串或整数)。它将所有数据连续存储在一块内存中,减少了内存碎片和元数据开销(如指针)。
结构:ziplist的每个节点(entry)由三部分组成:
prevlen
:前一个节点的长度(1或5字节),用于反向遍历;encoding
:数据的编码方式(1字节),表示数据类型(字符串/整数)和长度;data
:实际存储的数据(字符串或整数的二进制表示)。
示例:存储field1="val1"
、field2="val2"
的ziplist结构(简化):
[prevlen][encoding][data:field1] → [prevlen][encoding][data:val1] → [prevlen][encoding][data:field2] → [prevlen][encoding][data:val2]
为什么用ziplist存储小Hash?
- 内存高效:连续存储,没有指针开销(每个节点的
prevlen
仅1-5字节); - 适合小数据:当Hash的字段数量少(默认≤512)且每个字段/值的长度短(默认≤64字节)时,ziplist的访问时间(O(n))是可接受的。
触发ziplist的条件(可配置):
hash-max-ziplist-entries
:Hash的字段数量不超过此值(默认512);hash-max-ziplist-value
:每个字段或值的长度不超过此值(默认64字节)。
2. 哈希表(hashtable):大数据的高性能方案
当Hash的字段数量超过阈值或字段/值长度超过阈值时,Redis会将ziplist转换为hashtable(字典结构)。hashtable是Redis中最常用的底层结构(如String的底层也是hashtable),用于实现高效的查找、插入、删除操作。
结构:Redis的hashtable由以下部分组成:
- 字典(dict):包含两个哈希表(
ht[0]
和ht[1]
),用于渐进式rehash(避免一次性迁移数据导致的阻塞); - 哈希表(hash table):由数组(bucket)组成,每个数组元素是一个链表(解决哈希冲突,链地址法);
- 节点(entry):链表中的每个节点存储
field
(字段)、value
(值)和next
指针(指向链表下一个节点)。
示例:hashtable的结构(简化):
dict:
ht[0]: 哈希表(数组)→ 每个元素是链表(存储entry)
ht[1]: 用于rehash的临时哈希表
rehashidx: rehash的进度(-1表示未进行)
哈希冲突解决:
当两个不同的field
经过哈希函数计算后得到相同的数组索引(桶位置)时,Redis会用链表将它们连接起来(链地址法)。此时,查找该field
的时间复杂度变为O(k)(k为链表长度),因此需要通过rehash来减少冲突。
渐进式rehash(核心优化):
当哈希表的负载因子(used / size
,used
是已存储的节点数,size
是数组长度)超过阈值(默认1)时,Redis会触发rehash:
- 分配新哈希表:
ht[1]
的大小是第一个大于等于ht[0].used * 2
的2的幂(如ht[0].used=100
,则ht[1].size=256
); - 逐步迁移数据:Redis在每次读写操作时,将
ht[0]
中的部分数据迁移到ht[1]
(避免一次性迁移导致的阻塞); - 切换哈希表:当
ht[0]
中的数据全部迁移完成后,ht[1]
成为新的ht[0]
,ht[0]
被释放,rehashidx
设为-1。
rehash的优势:
- 避免了大对象迁移导致的Redis阻塞(渐进式迁移);
- 保持哈希表的负载因子在合理范围(≤1),减少哈希冲突的概率。
3. 编码转换:从ziplist到hashtable
当Hash的字段数量超过hash-max-ziplist-entries
或字段/值长度超过hash-max-ziplist-value
时,Redis会自动将ziplist转换为hashtable。
注意:
- 转换是不可逆的(即使之后字段数量减少到阈值以下,也不会转回ziplist);
- 转换过程是同步的(会阻塞Redis,因此应避免在大Hash上频繁修改字段数量)。
三、Hash类型的命令与时间复杂度
Hash类型的常用命令及其时间复杂度(基于编码):
命令 | 描述 | ziplist时间复杂度 | hashtable时间复杂度 |
---|---|---|---|
HSET | 设置字段值 | O(n)(遍历找字段) | O(1)(平均) |
HGET | 获取字段值 | O(n) | O(1) |
HDEL | 删除字段 | O(n) | O(1) |
HMSET | 批量设置字段值 | O(n) | O(k)(k为字段数) |
HMGET | 批量获取字段值 | O(n) | O(k) |
HGETALL | 获取所有字段和值 | O(n) | O(n) |
HLEN | 获取字段数量 | O(1)(ziplist有长度记录) | O(1)(hashtable有used记录) |
HEXISTS | 判断字段是否存在 | O(n) | O(1) |
注意:
HGETALL
命令在大数据量的Hash(如10万条字段)中会阻塞Redis(因为要遍历所有字段),应使用HSCAN
(渐进式遍历)替代;HSCAN
的时间复杂度是O(1)(每次扫描部分数据),适合处理大Hash。
四、Hash类型的优缺点
优点:
- 内存优化:用ziplist存储小对象时,比多个String键节省内存(减少键前缀的开销);
- 高效访问:用hashtable存储大数据时,查找、插入、删除的时间复杂度是O(1)(平均情况);
- 对象存储友好:天然适合存储对象的属性(如用户信息、商品属性)。
缺点:
- 大Hash的遍历问题:
HGETALL
会阻塞Redis,需用HSCAN
替代; - 编码转换的开销:从ziplist转换为hashtable时会阻塞Redis(应避免频繁修改大Hash的字段数量);
- 不适合复杂对象:如果对象的属性需要嵌套(如用户的订单列表),Hash类型无法直接存储(需用其他结构如List或Set)。
五、总结
Redis的Hash类型是对象存储的首选结构,其底层通过ziplist(小数据)和hashtable(大数据)的自适应编码,实现了内存优化和高性能的平衡。关键原理包括:
- ziplist:紧凑的链表结构,适合小数据量的Hash,减少内存开销;
- hashtable:字典结构,适合大数据量的Hash,实现高效的查找、插入、删除;
- 渐进式rehash:避免大对象迁移导致的阻塞,保持哈希表的性能;
- 编码转换:根据数据量动态调整底层结构,优化存储和访问效率。
理解这些原理,可以帮助我们合理设计Hash的结构(如控制字段数量和长度)、选择合适的命令(如用HSCAN
替代HGETALL
),从而充分发挥Redis的性能优势。