一、字典结构
Redis 的字典使用哈希表作为底层实现,一个哈希表中可以有多个哈希节点,每个哈希节点就保存了字典中的一个键值对;
typedef struct dict{
//类型特定函数
void *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash 索引 当rehash不在进行时 值为-1
int trehashidx;
}dict;
- type 属性和 privdata 属性: 作用是针对不同类型的键值对,创建不同类型的字典;
- ht 属性: 是两个哈希表组成的数组,正常情况下 ht[0] 保存着业务数据,ht[1] 在字典扩容时作为一张中间表使用;
- rehashidx 属性: 记录 rehash 进度的状态值,在没有进行 rehash 时,它的值为 -1。
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//哈希已有节点的数量
unsigned long used;
}dictht;
- table 属性: 由一个个指向哈希节点指针组成的一个数组结构;
- size 属性: 记录了哈希表的大小;
- sizemask 属性: 它的值等于 size -1;
- used 属性: 记录了目前哈希表中键值对的数量;
typedef struct dictEntry
{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
二 、业务数据是怎么保存到字典中的?
1、使用哈希算法计算出存放哈希列表的索引值
- 首先使用字典中的哈希函数计算出 key 的哈希值
-
- hash = dict->type->hashFunction(key);
- 使用哈希表中的 sizemask 属性与哈希值进行二进制的“与”运算后得到索引值
-
- index = hash & ht[x].sizemask;
示例说明
假设一个键值对 k2 和 v2 添加到字典(字典没有进行扩容操作),假设使用哈希函数计算出的 hash = 8,计算出的 index = 0 ,因此 k2 和 v2 键值对存放在哈希表 ht[0] 中索引为 0 的节点上;
// 计算出 8 和 3 的二进制值
8: 1000
3: 0011
// 二进制与运算
1000
& 0011
----
0000
// 二进制转十进制数
0*2^0 + 0*2^1 + 0*2^2 + 0*2^3 = 0
2、解决键冲突
在 Redis 字典中,如果有两个及以上数量的键被分配到哈希表数组中的同一个节点上时,称为键冲突,Redis 的哈希表使用单向链地址法来解决哈希冲突, 为了保存性能考虑,新节点总是添加到链表的表头位置。
三、字典中哈希表的扩展与收缩
// 负载因子 = 哈希表大小 / 哈希表已有节点数
load_factor = ht[0].used/ht[0].size;
1、扩容:
- 扩容条件
-
- 服务器没有执行 BGSAVE 或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1;
- 服务器正在执行 BGSAVE 或 BGREWRITEAOF 命令,并且哈希表负载因子大于等于 5。
- 扩容大小
-
- 第一个大于等于 used * 2 的 2 的 n 次幂 (4 * 2 = 8 , 而 8 = 2 的 3 次幂);
- 扩容过程
-
- 渐进式 rehash : rehash 动作并不是一次性、集中性地完成,而是分多次、渐进式地将 rehash 键值对所需要的计算工作均摊到每个增删改查操作中,避免集中式 rehash 带来的庞大计算量,过程如下图:
2、 缩容
- 缩容条件
-
- 负载因子小于 0.1 时,程序自动开始对哈希表进行收缩操作;
- 缩容大小
-
- 第一个大于等于 used 的 2 的 n 次幂 (used = 4 , 而 4 = 2 的 2 次幂);
- 缩容过程
-
- 渐进式 rehash : 缩容操作的 ****rehash 动作也不是一次性、集中性地完成,而是分多次、渐进式地将 rehash 键值对所需要的计算工作均摊到每个增删改查操作中。