青春因磨砺而出彩,人生因奋斗而升华
redis字典简介
关联数组或者映射,是一种用于保存键值对的抽象数据结构;在字典中,一个key和一个value进行关联,这些关联就成为键值对;字典中每一个键都是唯一的,程序可以在字典中根据键查找与之关联的值,或通过键来更新和删除值等;(redis自己构建了字典);
用途
redis字典的结构
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表 两张哈希表 后续字典扩展做rehash用
dictht ht[2];
// rehash索引;当rehash没有进行时,值为-1;否则表示rehash进行到的索引位置;
long rehashidx;
// 当前运行迭代器数
unsigned long iterators;
}dict
字段介绍:
-
type: 指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
-
privdata:保存了需要传给哪些类型特定函数的可选参数
-
ht: 包含两个数组,数组中每个项都是一个dictht哈希表,字典只使用ht[0]哈希表,h[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
-
rehashidx: 记录rehash目前进度,如果没有rehash值为-1;
子结构
typedef struct dictType {
// 计算hash值的函数
uint64_t (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType
typedef struct dictht {
dictEntry **table; // 哈希数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long used; // 该哈希表已有节点的数量
}dictht;
字段介绍:
-
table: 一个数组,数组中每一个元素都指向dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
-
size: 记录了哈希表的大小,也即table数组的大小
-
used: 记录了哈希表目前已有节点(键值对)的数量。
-
sizemask:属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面;
节点结构
typedef struct dictEntry {
void *key; // 键
union {
void *val; // 自定义类型
uint64_t u64; // 无符号整形
int64_t s64; // 有符号整形
double d; // 浮点型
} v; // 值
struct dictEntry *next; //指向下个哈希节点,形成链表
} dictEntry;
每个dictEntry都保存着一个键值对
字段介绍
-
key: 保存键值对中的键
-
v: 保存键值对中的值,其值可以是一个指针、uint64_t整数,或一个int64_t整数;
-
next: 指向下一个哈希节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突问题;
hash
hash算法
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出hash值,然后再将hash值与哈希表的sizemask属性做位与,得到索引值哈希数组的index;然后将数据放入哈希数组指定索引上面;
hash冲突
如冲突图
字典整体存储架构图
哈希表的扩展与收缩
扩展条件
-
负载因子 = 哈希表已保存节点数量 / 哈希表大小;
-
空hash表(初始化扩容4)
-
服务器目前没有执行bgsave或bgrewriteaof命令,并且哈希负载因子 >= 1时,扩容为原数组大小的2倍
-
如果服务器正在执行bgsave或bgrewriteaof,并且哈希表的负载因子 >= 5时,扩容为原数组大小的2倍
缩容条件
当hash表中元素逐渐删除的越来越稀疏时,redis会对hash进行缩容来减少hash表空间占用。
- 条件:元素个数 < 数组长度的10%,hash表就会缩容。所容不考虑redis是否在做bgsave
渐进式rehash
为什么渐进式rehash?
步骤:
-
判断负载因子,如果达到条件,则进行rehash操作
-
为ht[1]分配空间,让字典同时持有ht[0] ht[1]两个哈希表
-
在字典中维持一个索引计数器变量rehashidx, 并将他们的值设置为0,表示rehash正式开始进行;
-
在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作外,顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash完成之后,将rehashidx+1
-
ht[0]所有键值都被rehash到ht[1],将rehashidx设置为-1,表示操作完成
渐进式rehash执行期间的hash操作
因为在进行渐进式rehash的过程,字典会同时使用ht[0] ht[1]两个hash表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行;
且新添加到字典的键值对一律保存到ht[1]里面,ht[0]不进行任何添加操作;保证ht[0]包含的键值对数量只减不增;
渐进式rehash带来的问题
渐进式hash避免了redis阻塞,但由于rehash时,需要分配一个新的hash表,在rehash期间,同时有两个hash作用,会使得redis内存使用量瞬间突增,在redis满容状态下rehash可能会导致大量key驱逐;
思考
从结构来看每个字典中都包含两个hashtable。那么为什么一个字典需要两个hashtable?