Redis Dict(字典/哈希表)技术文档

Redis Dict(字典/哈希表)技术文档

一、概述

Dict(字典)是 Redis 中最核心、使用最广泛 的数据结构。它是 Redis 的哈希表实现,用于实现键值对存储,支持 O(1) 的查找、插入、删除操作。

1.1 为什么叫 Dict?

Redis 对外暴露的 API 是 Hash(如 HSETHGET),但底层实现叫做 dict。这是因为:

  • Hash:对外的数据结构接口
  • Dict:底层的哈希表实现

1.2 使用场景

场景 说明
全局键空间 存储所有数据库键值对(db->dict
Hash 类型 实现 Redis Hash 数据类型
Set 类型 底层编码(当元素为字符串时)
模块系统 模块内部数据结构
集群模式 存储节点信息、槽位映射
发布订阅 存储频道订阅关系
Lua 脚本 脚本缓存

二、核心设计思想

2.1 三大特性

  1. 渐进式 Rehash:使用两个哈希表,逐步迁移数据,避免一次性迁移阻塞主线程
  2. 链表法解决冲突:每个桶是一个链表,冲突时插入链表
  3. 指针标记优化:对 Set(无 value)优化,直接存储 key 指针,减少内存分配

2.2 整体架构

lua 复制代码
        +-------------------+
        |       dict        |
        |  +-------------+  |
        |  |  dictType   |  |  -- 类型配置(哈希函数、比较函数等)
        |  +-------------+  |
        |  | ht_table[0] |  |  -- 当前使用的主哈希表
        |  | ht_table[1] |  |  -- 渐进式 Rehash 时的目标表
        |  +-------------+  |
        |  | rehashidx   |  |  -- Rehash 进度(-1 表示不在 rehash)
        |  | pauserehash |  |  -- Rehash 暂停标记
        |  +-------------+  |
        +-------------------+

三、数据结构详解

3.1 dict 结构体

c 复制代码
struct dict {
    dictType *type;           // 类型配置
    dictEntry **ht_table[2];  // 两个哈希表
    unsigned long ht_used[2]; // 已使用的节点数
    long rehashidx;           // rehash 进度,-1 表示未在 rehash
    unsigned pauserehash;     // rehash 暂停标记
    signed char ht_size_exp[2]; // 大小指数(size = 1 << exp)
    int16_t pauseAutoResize;  // 禁止自动扩容
    void *metadata[];         // 额外元数据
};

关键字段解析

字段 类型 说明
ht_table[0] dictEntry** 当前使用的主哈希表
ht_table[1] dictEntry** 渐进式 Rehash 时的目标表
ht_used[2] unsigned long 两个表的已用节点数
rehashidx long -1 表示不在 rehash,>=0 表示当前进度
pauserehash unsigned >0 表示暂停 rehash(迭代器使用)
ht_size_exp[2] signed char 哈希表大小的指数(size = 1 << exp)

3.2 dictEntry(哈希节点)

c 复制代码
typedef struct dictEntry {
    void *key;     // 键
    union {        // 值(联合体)
        void *val;
        uint64_t u64;
        int64_t i64;
        double d;
    } v;
    struct dictEntry *next;  // 链表指针(解决冲突)
} dictEntry;

内存布局

lua 复制代码
+------------+-------------+-------------+
|    key     |  val/u64/   |    next     |
|   (void*)  |  i64/d      |  (dictEntry*)
+------------+-------------+-------------+

3.3 dictType(类型配置)

c 复制代码
typedef struct dictType {
    // 哈希函数
    uint64_t (*hashFunction)(const void *key);
    
    // 键复制/比较/销毁
    void *(*keyDup)(dict *d, const void *key);
    int (*keyCompare)(dictCmpCache *cache, const void *key1, const void *key2);
    void (*keyDestructor)(dict *d, void *key);
    
    // 值复制/销毁
    void *(*valDup)(dict *d, const void *obj);
    void (*valDestructor)(dict *d, void *obj);
    
    // 扩展/收缩许可
    int (*resizeAllowed)(size_t moreMem, double usedRatio);
    
    // 元数据
    void *userdata;
    
    // 标志
    unsigned int no_value:1;        // 无 value 优化(Set 使用)
    unsigned int keys_are_odd:1;    // key 地址为奇数优化
    unsigned int force_full_rehash:1; // 强制完整 rehash
} dictType;

四、哈希算法

4.1 哈希函数选择

Redis 使用 SipHash(一种加密安全的哈希算法):

c 复制代码
uint64_t dictGenHashFunction(const void *key, size_t len) {
    return siphash(key, len, dict_hash_function_seed);
}

为什么选 SipHash?

  • 防止哈希碰撞攻击(Hash DoS)
  • 良好的随机分布性
  • 高性能

4.2 桶索引计算

c 复制代码
index = hash(key) & mask
// 等价于
index = hash(key) % size

由于哈希表大小始终是 2 的幂次,所以可以用位运算代替取模:

c 复制代码
#define DICTHT_SIZE_MASK(exp) (((unsigned long)1 << (exp)) - 1)
// 如 exp=4: mask = 0b1111 = 15,size = 16

五、渐进式 Rehash(核心机制)

5.1 为什么需要 Rehash?

当哈希表中的元素越来越多,负载因子(used / size)会增大,导致链表变长,查询效率下降。

Redis 通过 rehash(重新哈希)来扩容或缩容,保持效率。

5.2 什么是渐进式 Rehash?

一次性 rehash 的问题

  • 如果有 100 万个键,全部重新哈希需要很长时间
  • Redis 是单线程的,会阻塞所有操作

渐进式 Rehash 解决方案

  • 每次操作(查找、插入、删除)时,迁移一小部分数据
  • 把大任务拆成多个小任务,分散到多次请求中

5.3 Rehash 流程

ini 复制代码
阶段 1: 准备阶段
+------------------+     +------------------+
|  ht_table[0]     |     |  ht_table[1]     |
|  [桶0][桶1][...] |     |  [NULL][NULL]... |  ← 新分配更大的表
+------------------+     +------------------+

阶段 2: 渐进迁移
+------------------+     +------------------+
|  ht_table[0]     |     |  ht_table[1]     |
|  [桶0→][桶1→]   | --> |  [新桶0][新桶1]  |
|  [桶2→][...]     |     |  [新桶2]...      |
+------------------+     +------------------+
   ↑ 每次操作迁移一个桶

阶段 3: 完成
+------------------+     +------------------+
|  ht_table[0]     |     |  ht_table[1]     |
|  [NULL][NULL]... |     |  [完整数据]      |
+------------------+     +------------------+
  交换 ht_table[0] 和 ht_table[1]

5.4 代码示例

c 复制代码
// 每次操作时执行 rehash
int dictRehash(dict *d, int n) {
    // 如果不在 rehash,直接返回
    if (!dictIsRehashing(d)) return 0;
    
    // 迁移 n 个桶
    while(n--) {
        dictEntry *de, *nextde;
        
        // 检查当前桶是否为空
        if ((de = d->ht_table[0][d->rehashidx]) == NULL) {
            d->rehashidx++;
            continue;
        }
        
        // 迁移当前桶的所有节点
        while(de) {
            nextde = de->next;
            // 计算新索引并插入 ht[1]
            uint64_t h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            de->next = d->ht_table[1][h];
            d->ht_table[1][h] = de;
            d->ht_used[1]++;
            d->ht_used[0]--;
            de = nextde;
        }
        
        d->ht_table[0][d->rehashidx] = NULL;
        d->rehashidx++;
    }
    return 1;
}

5.5 触发时机

操作 行为
dictAdd / dictReplace 插入前检查,触发一次 rehash
dictFind / dictDelete 查找/删除前检查,触发一次 rehash
dictRehashMicroseconds 定时任务,在空闲时执行

5.6 负载因子阈值

c 复制代码
// 扩容阈值:负载因子 >= 1 时扩容
if (used >= size && can_resize) {
    dictExpand(d, used * 2);
}

// 缩容阈值:负载因子 < 0.1 时缩容
if (size > DICT_HT_INITIAL_SIZE && used * 100 / size < HASHTABLE_MIN_FILL) {
    dictShrink(d);
}

六、冲突解决:链表法

6.1 冲突处理

当两个 key 哈希到同一个桶时,使用链表连接:

yaml 复制代码
桶索引 0:  [key1=100] -> [key2=200] -> NULL
桶索引 1:  [key3=300] -> NULL
桶索引 2:  NULL

6.2 查找过程

c 复制代码
dictEntry *dictFind(dict *d, const void *key) {
    uint64_t h = dictHashKey(d, key);
    // 如果正在 rehash,先迁移当前桶
    if (dictIsRehashing(d)) _dictRehashStep(d);
    
    // 遍历链表
    dictEntry *he = d->ht_table[0][h & mask0];
    while(he) {
        if (dictCompareKeys(d, key, he->key))
            return he;
        he = he->next;
    }
    return NULL;
}

6.3 平均查找复杂度

情况 平均查找次数 最坏情况
理想情况 1 次 1 次
一般情况 1-2 次 O(n)(所有 key 都冲突)
扩容后 < 1.5 次 O(1)

七、指针标记优化(Set 优化)

7.1 背景

当 dict 用作 Set (无 value)时,每个 dictEntry 只需要存储 key。

但标准 dictEntry 结构体包含 keyvalnext,有内存浪费

7.2 优化方案

方案 A:标准模式(Hash)

css 复制代码
桶 -> [dictEntry] -> [dictEntry] -> NULL
         ↓               ↓
      [key+val]       [key+val]

方案 B:优化模式(Set)

css 复制代码
桶 -> [key_ptr] -> [dictEntry] -> NULL
                     ↓
                  [key+next]

7.3 指针标记(Pointer Tagging)

利用指针地址的最低 3 位(由于 8 字节对齐,这 3 位始终为 0)来存储元数据:

复制代码
指针值:     0x7FFF_0000_0000_0XXX
                                 ↑
                              低 3 位(标记位)

标记含义

标记值 含义
000 普通 dictEntry 指针
010 偶地址 key 指针
XX1 奇地址 key 指针

7.4 代码实现

c 复制代码
// 检查指针类型
#define ENTRY_PTR_NORMAL     0  // dictEntry 指针
#define ENTRY_PTR_IS_EVEN_KEY 2  // 偶地址 key
#define ENTRY_PTR_IS_ODD_KEY  1  // 奇地址 key(任何奇数)

// 判断是否为 dictEntry
static inline int entryIsNormal(const dictEntry *de) {
    return ((uintptr_t)de & 7) == ENTRY_PTR_NORMAL;
}

// 判断是否为直接 key 指针
static inline int entryIsKey(const dictEntry *de) {
    return !entryIsNormal(de);
}

// 获取实际的 dictEntry 或 key
dictEntry *decodeEntry(dictEntry *de) {
    if (entryIsNormal(de)) {
        return de;  // 直接返回
    } else {
        // 从 key 指针创建 dictEntry(临时)
        void *key = (void *)((uintptr_t)de & ~7);
        // ... 构建临时 entry
    }
}

7.5 优化效果

场景 每个元素内存
标准 dictEntry 24 字节(key + val + next)
优化后(单个 key) 8 字节(仅 key 指针)
优化后(链表) 16 字节(dictEntry 无 val)

八、迭代器设计

8.1 安全 vs 非安全迭代器

c 复制代码
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    unsigned long long fingerprint;
} dictIterator;
类型 特点 使用场景
安全迭代器 可以修改 dict(暂停 rehash) HSCANHKEYS
非安全迭代器 只读,不暂停 rehash 遍历但不修改

8.2 使用示例

c 复制代码
// 安全迭代器
dictIterator *iter = dictGetSafeIterator(d);
dictEntry *entry;
while ((entry = dictNext(iter)) != NULL) {
    void *key = dictGetKey(entry);
    void *val = dictGetVal(entry);
    // 可以安全地删除或修改
}
dictReleaseIterator(iter);

九、核心 API 速查

9.1 创建与销毁

c 复制代码
dict *dictCreate(dictType *type);              // 创建字典
void dictRelease(dict *d);                      // 销毁字典
void dictEmpty(dict *d, void(callback)(dict*)); // 清空字典

9.2 增删改查

c 复制代码
// 添加
int dictAdd(dict *d, void *key, void *val);     // 添加键值对
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing);
int dictReplace(dict *d, void *key, void *val); // 替换或添加

// 删除
int dictDelete(dict *d, const void *key);       // 删除

// 查找
dictEntry *dictFind(dict *d, const void *key);  // 查找键
void *dictFetchValue(dict *d, const void *key);  // 查找值

9.3 扩展与收缩

c 复制代码
int dictExpand(dict *d, unsigned long size);      // 手动扩展
int dictExpandIfNeeded(dict *d);                 // 按需扩展
int dictShrinkIfNeeded(dict *d);                 // 按需收缩
int dictRehash(dict *d, int n);                  // 执行 n 步 rehash

9.4 随机获取

c 复制代码
dictEntry *dictGetRandomKey(dict *d);            // 随机获取一个键
dictEntry *dictGetFairRandomKey(dict *d);        // 公平随机(分布均匀)
unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count);

9.5 宏工具

c 复制代码
dictSize(d)          // 获取元素数量
dictIsEmpty(d)       // 判断是否为空
dictIsRehashing(d)   // 是否在 rehash
dictPauseRehashing(d)   // 暂停 rehash
dictResumeRehashing(d)  // 恢复 rehash

十、性能分析

10.1 时间复杂度

操作 平均复杂度 最坏情况
插入(dictAdd O(1) O(n)(所有 key 冲突)
删除(dictDelete O(1) O(n)
查找(dictFind O(1) O(n)
Rehash(单步) O(1) O(n)(一个桶的链表很长)

10.2 空间复杂度

  • 负载因子(used / size)控制在 0.5 ~ 1.0 之间
  • 最小表大小 :4(DICT_HT_INITIAL_SIZE = 1<<2 = 4
  • 扩展策略:翻倍
  • 收缩策略:负载因子 < 12.5% 时缩容

十一、设计亮点总结

11.1 渐进式 Rehash

思想:"把大任务拆成小任务,分散到多次请求中"

  • 避免一次性迁移导致阻塞
  • 保证服务的持续可用性
  • 利用请求的间隙时间完成迁移

11.2 指针标记优化

思想:"利用内存对齐的空闲位,存储额外信息"

  • 减少 Set 类型的内存占用
  • 不需要额外的元数据结构
  • 对性能几乎无影响

11.3 安全迭代器

思想:"读写分离,保证遍历安全"

  • 遍历时暂停 rehash,避免数据丢失
  • 提供指纹校验,检测非法修改
  • 支持安全删除

十二、与 Java HashMap 对比

特性 Redis Dict Java HashMap
冲突解决 链表法 链表法 / 红黑树
扩容策略 渐进式 Rehash 一次性迁移(阻塞)
负载因子 0.5 ~ 1.0 0.75
哈希算法 SipHash 自定义(基于对象 hashCode)
线程安全 单线程(外部保证) 非线程安全(ConcurrentHashMap 是)
Set 优化 指针标记 无(HashSet 独立实现)
缩容 支持 不支持

附录:关键宏定义

c 复制代码
#define DICT_HT_INITIAL_EXP   2    // 初始大小指数
#define DICT_HT_INITIAL_SIZE  (1 << DICT_HT_INITIAL_EXP)  // 初始大小 = 4
#define HASHTABLE_MIN_FILL    8    // 最小填充率 = 12.5%
#define DICT_OK 0                   // 操作成功
#define DICT_ERR 1                  // 操作失败

参考源码 :Redis 8.8.2 (src/dict.h, src/dict.c)

相关推荐
就叫_这个吧4 小时前
RabbitMQ+elasticsearch+Redis,实现新增内容并异步到es中,是否消费成功检测
redis·elasticsearch·rabbitmq
李可以量化4 小时前
Redis Client 从了解到精通(二)下:String 类型进阶操作全解
redis·git·python·量化交易·qmt
BUG研究员_5 小时前
LangChain 向量数据库实战:Redis 与 Pinecone 的知识点总结
数据库·redis·langchain
李可以量化6 小时前
Redis Client 从了解到精通(二)上:redis-py 高级用法与核心命令实战
前端·数据库·redis·python·缓存·ptrade
仍然.7 小时前
Redis---String
数据库·redis·缓存
JavaPub-rodert7 小时前
Redis 和 MySQL 如何保证数据一致性?从缓存更新到延迟双删完整讲解
redis·mysql·缓存
半个落月8 小时前
Next.js 16 笔记应用实战:Redis 数据链路与组件两版拆分详解
前端·redis·next.js
家有娇妻张兔兔9 小时前
Redis 报错 “MISCONF Redis is configured to save RDB snapshots“?一文讲透根因与根除方案
数据库·redis·故障排查·misconf
一嘴一个橘子9 小时前
SpringDataRedis 操作 redis
java·redis