Redis设计与实现之字典
什么是字典
字典, 又称 符号表(symbol table)
、 关联数组(associative array)
或者 映射(map)
, 是一种用于保存 键值对(key-value pair)
的抽象数据结构。
键值对
在字典中, 一个键(key)可以和一个值(value)进行关联, 这些关联的键和值就被称为键值对。
字典的实现
底层实现
Redis 的字典使用 哈希表
作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。
哈希表节点定义
代码位置 : dict.h
typedef struct dictEntry {
void *key; //字典key
union { //数据值
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; //指向下个哈希表节点,形成链表,hash键冲突时才被用到
} dictEntry;
- 哈希表节点使用dictEntry结构表示, 每个dictEntry结构都保存着一个键值对
- key属性保存着键值对中的键, 而v属性则保存着键值对中的值, 其中键值对的值可以是
一个指针
, 或者是一个uint64_t整数
, 又或者是一个int64_t整数
- next属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一起, 以此来
解决键冲突(collision)的问题
。
哈希表定义
代码位置 : dict.h
typedef struct dictht {
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小
unsigned long sizemask; //哈希表大小掩码,用于计算索引值 总是等于 size - 1
unsigned long used; //该哈希表已有节点的数量
} dictht;
- table 属性是
一个数组
, 数组中的每个元素都是一个指向dictEntry
结构的指针, 每个 dictEntry 结构保存着一个键值对。 - size 属性记录了
哈希表的大小
- used 属性记录了
哈希表目前已有节点(键值对)的数量
。 - sizemask 属性的值
总是等于 size - 1
, 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。
字典定义
代码位置 : dict.h
typedef struct dict {
dictType *type; //类型特定函数
void *privdata; //私有数据
dictht ht[2]; //哈希表
long rehashidx; //rehash 索引 当 rehash 不在进行时,值为 -1
unsigned long iterators; //目前正在运行的安全迭代器的数量
} dict;
- type属性和privdata属性是针对不同类型的键值对, 为创建多态字典而设置的
- ht属性是一个包含两个项的数组, 数组中的每个项都是一个dictht哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用
- 除了 ht[1] 之外, 另一个和 rehash 有关的属性就是rehashidx: 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。
eg. 空字典
eg. 普通状态下的字典(没有进行 rehash)
哈希算法
当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出哈希值和索引值, 然后再根据索引值, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
Redis 计算哈希值和索引值的方法如下:
// 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
// 使用哈希表的 sizemask 属性和哈希值,计算出索引值
// 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;
eg. 将一个键值对 k0 和 v0 添加到一个新建立的字典里面
- 程序先使用语句:hash = dict->type->hashFunction(k0), 计算键 k0 的哈希值
- 假设计算得出的哈希值为 8 , 那么程序会继续使用语句:index = hash & dict->ht[0].sizemask = 8 & 3 = 0;
- 计算出键 k0 的索引值 0 , 这表示包含键值对 k0 和 v0 的节点应该被放置到哈希表数组的索引 0 位置上
【知识点】 当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis 使用 MurmurHash2 算法来计算键的哈希值。
哈希冲突的解决
当有 两个或以上数量 的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。
字典哈希冲突解决方案 参见 : 哈希冲突解决方案
rehash原理及实现
rehash原理实现 参见 : rehash原理
字典的主要API
函数 | 作用 | 时间复杂度 |
---|---|---|
dictCreate | 创建一个新的字典。 | O(1) |
dictAdd | 将指定的键值对添加到字典里 | O(1) |
dictAddRaw | 基础的向hash表中新增一个键值对的方法 | O(1) |
dictGetIterator | 获取迭代器, 不安全, safe = 0 | O(1) |
dictGetSafeIterator | 获取迭代器, 安全, safe = 1 | O(1) |
dictReleaseIterator | 释放迭代器 | O(1) |
dictNext | 依据迭代器获取下一个键值对 | O(1) |
dictGetRandomKey | 随机获取一个键值对 | O(1) |
dictGetSomeKeys | 随机返回指定数量的key | O(1) |
dictEmpty | 清空字典数据但不释放空间 | O(1) |
dictReplace | 将给定的键值对添加到字典里面, 如果键已经存在于字典,那么用新值取代原有的值。 | O(1) |
dictFind | 查找指定的key | O(1) |
dictFetchValue | 返回指定的key的值 | O(1) |
dictDelete | 从字典中删除给定键所对应的键值对。 | O(1) |
dictRelease | 释放给定字典,以及字典中包含的所有键值对 | O(N), N为字典中键值对数量 |
dictEnableResize | 启用redis空间再次分配 | O(1) |
dictDisableResize | 禁用redis空间再次分配(比如: rehash期间ht[0]禁用resize) | O(1) |
dictResize | 重计算字典大小,将字典容量设置为可以容纳当前字典数据的最小值 | O(1) |
dictRehash | 对字典进行rehash | O(N),N为字典中键值对数量 |
dictRehashMilliseconds | 执行rehash操作,指定运行时间,此时间内rehash可能未最终完成 | O(N),N为字典中键值对数量 |
参考资料
参考内容
书籍
: 《Redis 设计与实现》
实体书
: 可在京东搜索购买
注意事项
《Redis 设计与实现》 本书讲解基于redis 3.x 版本, 本人整理基于redis 5.0.6,期间跨了两个大版本, 部分功能具体实现已发生变化, 如发现整理内容与 《Redis 设计与实现》 讲解不一致, 可加 QQ : 2215508028 沟通,或在相关章节评论留言,会自动同步至issue
整理内容时参考的redis源码
版本
: 5.0.6
官方git
: redis官方GIT (commit :bb78454b0fef0dc5903328d037ac2520108e0044 (5.0.6版本,redis官方的commit))