字典
简介
因为C语言没有内置字典结构,所以Redis自己构建了字典的实现。字典在Redis应用十分广泛,对数据库的增、删、改操作都是在字典的操作之上的,比如:SET msg “hello world”。
字典还是哈希键的底层实现之一,当一个哈希键包含的键值比较多或者键值对中对元素都是比较长对字符串时,Redis会使用字典作为哈希键底层实现。
组成
字典在Redis中由 dict.h/dict 定义
字典的结构体:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
解释:
type保存了针对字典不同用途的函数
privdata针对上面的函数的参数列表
哈希表 ht[0] 是日常插入删除应用的哈希表
哈希表 ht[1] 是对ht[0]进行rehash时才使用的
字典中的哈希表
字典使用哈希表作为底层实现
一个哈希表包含多个哈希节点
每个哈希表节点保存一个键值对
哈希表在 dict.h/dictht 中定义,结构体如下:
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
一个空哈希表例子:
哈希表中的哈希表节点
结构体定义
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
解释:
v值可以是指针、无符号64位整数、有符号64位整数 next是一个指向下一个哈希表节点的指针在拉链法中解决冲突
一个哈希表及哈希表节点例子:
哈希算法
算法流程:
在对字典添加新的键值对时:
- 根据键的值算出哈希值 :hash = dict->type->hashFunction(key);
- 根据哈希值算出索引值:index = hash & dict->ht[x].sizemask;
- 将键值对插入到哈希表中对应索引的哈希节点处
解决冲突:
使用链地址法解决冲突
文字描述:当要添加新键值对(哈希表节点)时,计算出该键值对的索引值后,发现哈希表对应的索引处已经有一个哈希表节点,那么就把新的哈希表节点对next指针指向该哈希表节点,并且让哈希表索引指向新的哈希表节点
图片描述:
Rehash
扩展或者收缩哈希表
扩展
扩展条件:
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
原因:在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。
扩展过程:
- 为ht[1]分配空间,大小为第一个大于ht[0]已有节点的数量2的2的n次幂
比如:ht[0]已经有的节点数量为7,则为ht[1]分配空间16,因为16为2的4次方而且大于72 - 将保存在ht[0]中的所有键值对重新计算哈希值和索引值转移到ht[1]中
- 迁移完毕,释放ht[0],将ht[0]的指针指向ht[1],并且为ht[1]新建一个空哈希表
收缩:
收缩条件:
当哈希表的负载因子小于 0.1 时
收缩过程:
- 为ht[1]分配空间,大小为第一个大于ht[0]已有节点的数量的2的n次幂
比如:ht[0]已经有的节点数量为7,则为ht[1]分配空间8,因为8为2的3次方而且大于7 - 将保存在ht[0]中的所有键值对重新计算哈希值和索引值转移到ht[1]中
- 迁移完毕,释放ht[0],将ht[0]的指针指向ht[1],并且为ht[1]新建一个空哈希表
渐进式rehash
在Redis中的rehash不是一步完成的,如果哈希表很大,那么rehash过程很耗时,一步完成会阻塞数据库的增删改查。
过程:
- 在进行rehash时,将rehash值从-1置为0
- 每次转移一个哈希表节点,rehash+1
- 直到rehash完毕,将rehash值置为-1
渐进式rehash过程中新增的节点都会添加到ht[1]中
主要API
本章思维导图