02.3 - Hash 底层实现
定位: Redis 的哈希类型适用于对象存储(如用户信息、商品详情)和计数器场景
面试高频度: ⭐⭐⭐⭐⭐
考查方式: 编码转换(ziplist→hashtable)、渐进式 rehash 原理、rehash 过程
一、这是什么?为什么需要它?
是什么
Redis Hash 是一个键值对集合(field-value pairs),底层在小数量时用 ziplist 紧凑存储,大数量时自动切换到 dict(哈希表)。
为什么需要 Hash?
存储模型
二、原理拆解
2.1 编码切换策略
2.2 Dict(哈希表)结构
// Redis 哈希表结构(简化)
struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 表大小
unsigned long sizemask; // size - 1(用于索引计算)
unsigned long used; // 已用节点数
};
struct dict {
dictht ht[2]; // 两个哈希表(用于渐进式 rehash)
long rehashidx; // rehash 进度,-1 表示未进行
// ...
};
struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
dictEntry *next; // 链表解决冲突
};哈希表关键设计:
1. 链表法解决哈希冲突
table[idx] → entry1 → entry2 → entry3(同 hash 的 key)
2. 负载因子触发 rehash
负载因子 = used / size
扩容:负载因子 > 1(有 BGSAVE)或 > 5(无 BGSAVE)
缩容:负载因子 < 0.1
3. MurmurHash2 哈希算法
高效、分布均匀2.3 渐进式 Rehash 详解
rehash 期间的访问策略:
增:只在 ht[1] 新增(保证 ht[0] 只减不增,最终变空)
删:先在 ht[0] 删,不存在则在 ht[1] 删
改:先找 ht[0],再找 ht[1]
查:先找 ht[0],再找 ht[1]三、图解全景
四、实战验证
4.1 验证编码转换
redis-cli
# 1. 小 Hash → ziplist 编码
127.0.0.1:6379> HSET user:42 name "Alice" age 30 city "Beijing"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING user:42
"listpack" # Redis 7.x 用 listpack 替代 ziplist
# 或 "ziplist"(Redis 6.x)
# 2. 插入大量 field → 触发编码转换
127.0.0.1:6379> EVAL "for i=1,600 do redis.call('HSET',KEYS[1],i,i) end" 1 bighash
(nil)
127.0.0.1:6379> HLEN bighash
(integer) 600
127.0.0.1:6379> OBJECT ENCODING bighash
"hashtable"
# 3. 混合条件:某个 value 超过 64 字节也会触发转换
127.0.0.1:6379> HSET smallhash longfield "$(printf 'A%.0s' {1..100})"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING smallhash
"hashtable"4.2 渐进式 rehash 观察
# 渐进式 rehash 是渐进式的,在大量 key 时可以通过
# 多次访问观察内存变化来间接验证
# 创建一个较大 Hash
for i in $(seq 1 10000); do
redis-cli HSET largehash "field:$i" "value:$i" > /dev/null 2>&1
done
# 查看编码
redis-cli OBJECT ENCODING largehash
# hashtable
# 删除大量 key(触发缩容 rehash)
for i in $(seq 1 9500); do
redis-cli HDEL largehash "field:$i" > /dev/null 2>&1
done
# 查看 Hash 信息
redis-cli HLEN largehash
# (integer) 500
# 注意:rehash 过程对调用者透明,INFO 没有直接暴露 rehash 状态
# 可以通过 redis-rdb-tools 分析 RDB 重建哈希表看到分桶情况4.3 Hash 的性能基准测试
# 对比 Hash 存取 vs String 存取
# Hash 方式:存入 1000 个用户(每个用户 5 个字段)
time for i in $(seq 1 1000); do
redis-cli HSET "user:$i" name "user$i" age "$i" city "Beijing" > /dev/null
done # ~2 秒
# String 方式:存入 1000 个用户(每个用户 5 个单独的 key)
time for i in $(seq 1 1000); do
redis-cli MSET "user:$i:name" "user$i" "user:$i:age" "$i" > /dev/null
done # ~3-4 秒(更多的 key 管理开销)
# 结论:对象场景 Hash 比 String 更省内存、更快五、面试视角
| 追问 | 答案要点 |
|------|---------|
| Hash 什么时候用 ziplist,什么时候用 hashtable? | 小数据时(默认 entry≤512 且 value≤64B)用 ziplist,超阈值转 hashtable(不可逆)|
| 渐进式 rehash 怎么保证不阻塞? | 每次操作搬一个 bucket,分摊成本;同时操作两个表(ht[0]查/ht[1]增)|
| rehash 期间怎么保证数据不错? | 读查两个表;写只写新表(ht[1])而旧表只减不增;最终 ht[0] 变空后释放 |
| rehash 的触发条件? | 扩容:负载因子>1(有子进程)或 >5(无子进程);缩容:负载因子<0.1 |
| 为什么 rehash 是不可逆的? | ziplist→hashtable 后不会转为 ziplist,因为假设业务增长后不会再缩回小数据量。可用 OBJECT ENCODING 验证 |
| 相同的 field 多次 HSET 会怎样? | 覆盖旧值,ziplist 中可能需要重新分配内存(连锁更新风险)|
📚 相关链接
**Set 底层** — 也使用 dict 但有差异
**数据结构概览** — Hash 在整体中的位置
**内存优化** — ziplist/listpack 参数调优
← 返回 **核心数据结构索引**