Skip to content

Hash详解

一、Hash类型是什么?

Redis的Hash(哈希)是一种键值对的集合,但它的值(Value)本身也是一个键值对结构(Field-Value)。换句话说,Hash类型是“键(Key)→ 字段(Field)→ 值(Value)”的三级映射。

  • 示例:存储用户信息时,user:1000是Hash的键,nameageemail是字段(Field),对应的值是"张三"25"zhangsan@example.com"

    bash
    HSET 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 / sizeused是已存储的节点数,size是数组长度)超过阈值(默认1)时,Redis会触发rehash:

  1. 分配新哈希表ht[1]的大小是第一个大于等于ht[0].used * 2的2的幂(如ht[0].used=100,则ht[1].size=256);
  2. 逐步迁移数据:Redis在每次读写操作时,将ht[0]中的部分数据迁移到ht[1](避免一次性迁移导致的阻塞);
  3. 切换哈希表:当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的性能优势。