Redis 的哈希 hash是什么?
重要内容
在 Redis 中,哈希(Hash)是一种键值对的集合,其中每个键 对应的值是多对字段 - 值(field - value)的映射
如:Key → { Field1: Value1, Field2: Value2, ... }
- 内部表示 :哈希本质上是一个二维的数据结构,一个键对应多个字段,每个字段关联一个值
- 操作特性:可以独立地对每个字段进行操作,而不需要对整个哈希进行操作。这使得哈希非常适合存储对象,因为对象的每个属性可以作为一个字段
扩展知识
Hash 常用命令
命令 | 简述 | 使用 |
---|---|---|
HSET | 设置哈希表中字段的值 | HSET key field value |
HGET | 获取字段值 | HGET key field |
HGETALL | 获取所有字段和值 | HGETALL key |
HDEL | 删除指定字段 | HDEL key field |
Hash 底层实现解析
Hash是一种数据基础数据结构,类似于数据结构中的哈希表,一个哈希可以存储2的32次方-1个键值对
底层结构需要分成两个情况
- Redis6及之前,Hash的底层是压缩列表加上哈希表的数据结构(ziplist+hashtable)
- Redis7之后,Hash的底层是紧凑列表加上哈希表的数据结构(Listpack+hashtable)
压缩列表和紧凑列表的比较
两者时间复杂度都是O(n),其主要区别就在于 listpack 解决了 ziplist 的级联更新问题
紧凑列表、压缩列表 与 哈希表转换的条件
哈希键的字段个数(默认512)以及每个字段名和字段值的长度(默认64)
注意:在使用hashtable结构之后,就不会再退化成ziplist或listpack,之后都是使用hashtable进行存储
Hashtable 底层数据结构
Redis 的 hashtable
通过字典(dict
)实现,字典包含两个哈希表(dictht
)和渐进式 rehash 机制
字典的结构
c
typedef struct dict {
dictType *type; // 类型特定函数(哈希计算、键值复制等)
void *privdata; // 私有数据
dictht ht[2]; // 两个哈希表(用于渐进式 rehash)
long rehashidx; // rehash 进度索引(-1 表示未进行)
unsigned long iterators; // 当前运行的迭代器数量
} dict;
哈希表的结构
c
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希表已有的节点数量
unsigned long used;
} dictht;
哈希节点的结构
c
typedef struct dictEntry {
//键值对中的键
void *key;
//键值对中的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
结构图
通过图片能够完整的展示 hashtable 内部的数据结构
哈希表的核心实现机制
哈希函数与索引计算
-
哈希函数 :Redis 使用 MurmurHash2 算法(非加密型哈希)将键(key)转换为 64 位或 32 位哈希值
-
索引计算
cindex = hash & d->ht[table].sizemask; // 等价于 hash % size
通过位掩码(
sizemask
)将哈希值映射到哈希表数组的索引位置 -
示例:假设哈希表的大小为 8,哈希值为 101
- 位掩码为
7
(即 8−1) - 计算索引:
index = 101 & 7 = 5
- 因此,键 101 被映射到索引 5
- 位掩码为
负载因子与扩容/缩容
- 负载因子 : used(已用节点数)/ size(桶总数)
- 扩容触发条件
- 负载因子 >= 1,这个时候说明空间非常紧张,新数据是在哈希节点的链表上找到的,这个时候如果服务器没有执行 RDB快照或者AOF重写这两个持久化机制的时候,就会进行rehash操作
- 当负载因子 >= 5,这个时候说明哈希冲突非常严重了,这个时候无论有没有进行AOF重写或者RDB快照,都会强制执行rehash操作
- 缩容触发条件
- 当
used < size / 10
(负载因子 < 0.1)时,缩容至used
最近的 2 的幂
- 当
渐进式 Rehash 机制
为避免一次性迁移大量键值对导致服务阻塞,Redis 采用 渐进式 rehash,将迁移分摊到多次操作中
Rehash 流程
- 准备阶段
- 分配新哈希表
ht[1]
,大小为满足扩容/缩容条件的 2 的幂 - 设置
rehashidx = 0
,表示开始从索引 0 迁移数据
- 分配新哈希表
- 迁移阶段
- 每次操作触发迁移 :在增删改查操作中,每次迁移
ht[0].table[rehashidx]
桶内的所有节点到ht[1]
- 更新索引 :
rehashidx++
,直至所有桶迁移完成
- 每次操作触发迁移 :在增删改查操作中,每次迁移
- 完成阶段
- 释放
ht[0]
的内存,将ht[1]
设置为ht[0]
- 重置
ht[1]
为空哈希表,设置rehashidx = -1
- 释放