IO模型
- 概述
- 实现
- 字典
- 哈希表
- 哈希表dictht结构
- 哈希表节点dictEntry结构
- 哈希算法
- 解决键冲突
- rehash
- 渐进式rehash
- API
- 参考文献
概述
字典又称映射(map), 是一种用于保存键值对的数据结构。
字典的键(key)唯一,一个键对应一个值(value),查找,删除,更新都需要通过键来操作。
redis的数据库就是使用字典作为底层的
新增键值对"msg"->"hello world", 执行redis命令:
redis> SET msg "hello world"
字典也是哈希键的底层实现之一
实现
字典
字典结构 dict.h/dict
typedef struct dict{
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引 当rehash不在进行时值为-1
int trehashidx;
}dict;
- type 指向dictType结构体的指针, 该结构体保存了对特性类型键值对操作的函数
- privadata 保存type中函数的特定参数.
- ht 包含两个dictht哈希表, 一般只是使用ht[0], ht[1]在rehash时才会使用
- rehashidx 记录了rehash目前的进度.
哈希表
哈希表dictht结构
typedef struct dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
//值为 size - 1 掩码 计算索引
unsigned long sizemask;
//
unsigned long used;
}dictht;
- table 中的元素都是dict.h/dictEntry结构, 是一个保存着键值对的哈希表节点.
- size 为哈希表的大小, 即table数组大小.
- sizemask 值等于size - 1, 用于计算索引.
- used 记录哈希表已有节点
哈希表节点dictEntry结构
dictht中table数组保存的键值对元素.
typedef struct dictEntry{
// 键
void *key;
// 值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
// 指向下个哈希表的节点, 形成链表.
struct dictEntry *next;
}dictEntry;
- key 保存着键值对中的键
- v 是一个共用体, 表示键值对中的值, 它可以是void * 指针, uint64_t无符号整数和 int64_t整数其中的一个.
- next 指向下一个节点的指针, 链地址法解决哈希冲突.
普通状态下的字典结构
哈希算法
- 使用字段设置的特定函数中的哈希函数计算哈希值
hash = dict->type->hashFunction(key);
- 通过sizemask属性和哈希值, 计算出索引值
index = hash & dict->ht[x].sizemask;
解决键冲突
- Redis 的哈希表采用链地址法解决哈希冲突
- 多个相同hash值的哈希表节点使用next指针构成一个单向链表.
rehash
- 让哈希表的负载因子维持在一个合理的范围, 应对哈希表底层的数组进行相应的扩增或缩小
rehash步骤:
- 为字典的ht[1]哈希表分配空间, 扩展则其大小为于 第一个大于等于ht[0].used * 2 的 2^n。 收缩则其大小为第一个小于等于 ht[0].used 的2^n。
- 将ht[0]中的所有键值对rehash到ht[1] 中(经过重新计算)。
- 释放ht[0], 将ht[1] 设为 ht[0]。
rehash条件:
- 服务器目前没有执行BGSAVE命令和BGREWRITEAOF命令, 且哈希表负载因子大于1
- 服务器正在执行BGSAVE命令和BGREWRITEAOF命令, 且哈希表负载因子大于5
- 哈希表负载因子小于0.1, 则会进行哈希表的收缩
- 负载因子 = 哈希表已保存节点数量 / 哈希表大小
BGSAVE和BGREWRITEAOF命令会创建服务进程的子进程,扩展操作会影响写时复制,造成内存浪费。
写时复制:对于父进程和子进程,其共用一份内存空间,只有进程空间的各段的内容要发生变化时,才会将父进程的内存复制一份给子进程。
渐进式rehash
rehash存在的问题:
- 存有大量数据时扩展较慢,影响正常地增删改查操作
解决:
- 使用渐进式rehash,将rehash的工作均摊到每一次的增删改查上。
渐进式rehash步骤:
- 为ht[1] 分配空间, 让字典同时持有ht[0] 和 ht[1]两个哈希表
- 维持一个索引计数器变量rehashidx = 0
- 每次对字典的增删改查操作, 都会顺带将 ht[0] 中 rehashdx 位置的所有键值对rehash到 ht[1] 上. rehashidx向后递增。
- 随着对字典操作的进行,所有的ht[0] 中的键值对都会被rehash到 ht[1],最后设置rehashidx = -1, 渐进式 rehash 完成。
渐进式rehash时对字典进行操作:
- 所有的增删改查都会根据情况,同时再ht[0] 和 ht[1] 上同时操作。
- 查找操作,会先在ht[0]中查找, 不存在则再在ht[1]中查找
- 新增操作, 都会在ht[1]上进行。
API
函数 | 作用 | 时间复杂度 |
dictCreate | 创建一个新字典 | O(1) |
dictAdd | 添加键值对 | O(1) |
dictReplace | 替换键值对, 不存在则添加 | O(1) |
dictFetchValue | 返回给定键的值 | O(1) |
dictGetRandomKey | 随机返回一个键值对 | O(1) |
dictDelete | 删除键值对 | O(1) |
dictRelease | 释放字典, 以及所有键值对 | O(N) |