字典
字典: 又称符号表,是一种用于保存键值对(key-value pair)的抽象数据结构。字典中的每个键都是唯一的,可以根据键对值进行操作。
Redis中自建了字典实现。使用字典来作为数据库的底层实现,对数据库的CRUD也是构建在对字典的操作上。还是哈希键的底层实现之一。
1. 字典的实现
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
1.1 哈希表
Redis字典使用的哈希表结构:
typedef struct dictht{
//哈希表数组
dictEntry ** table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
table 属性是一个数组,数组中的每一个元素都是一个指向dictEntry结构的指针,每个dictEntry保存一个键值对。
size 属性记录了哈希表的大小,也就是table数组的大小
used 记录了哈希表目前已有节点的数量
sizemask 总是等于size-1,属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。
1.2 哈希表节点
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry{
//键
void *key;
//值
union{
//指针
void *val;
//无符号整数
uint64_t u64;
//整数
int64_t s64;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
Key 属性保存着键值对中的键
V 属性保存着键值对中的键(可为指针、uint64_t整数、int64_t整数)
next 属性是指向另一个哈希表节点指针。可以将多个哈希值相同的键值对连接在一起,一次解决键重读问题。(个人感觉:有点类似于Java中的Map,通过链表的方式,处理Hash冲突)
1.3 字典
Redis中的字典结构:
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash 索引
// 当rehash 不在进行时,值为 -1
in trehashidx; /* rehashing not progress if rehashidx == -1*/
} dict;
typedef struct dictType{
//计算哈希值的函数
unsigned int (*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 *key2);
//销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
//销毁值的函数
void (*valDestructor)(void *privdata, void *obj)
} dictType;
type 属性和 privadata 属性是针对于不同类型的键值对,为创建多态字典而设置到的:
type 是一个指向 dictType 结构的指针,每个dictType结构保存了操作特定类型键值对的函数,Redis对不同用途的字典设置了不同类型的特定函数。
privdata 保存了需要传给那些类型特定函数的可选参数。
ht 是一个包含两个项的数组,每项都是一个dictht哈希表。一般情况,字典只使用h[0]哈希表,h[1]哈希表只会在对h[0]进行rehash时使用。
rehashidx 记录了当前rehash的进度,值为-1时表示当前没有在进行rehash。
2. 哈希算法
Redis中计算哈希值和索引值的方法:
使用hashFunction(key)方法计算key的hash值,再根据计算的hash值,与哈希表长度的掩码进行&运算
例子:将一个键值对k0和v0添加到字典,
3. 解决键冲突
冲突:当有两个或以上的数量的键分配到哈希表数组的同一索引上。
Redis使用链地址法解决键冲突。(链地址法:每个表节点都有一个next指针,多个节点使用next指针构成一个单向链表,同一索引的节点使用单向链表连接起来)
4. rehash
扩展和收缩哈希表时,通过执行rehash(重新散列)操作,对哈希表中的节点,重新计算新的索引位置。
Redis对字典的哈希表执行rehash步骤如下:
(1) 为字典的ht[1]分配足够的空间,大小取决于ht[0].used属性的值
1)扩展操作:ht[1]大小为第一个大于等于ht[0].used * 2 的 2的n次幂。
2)收缩操作:ht[1]大小为第一个大于等于ht[0].used 的2的n次幂。
(2) 将保存在ht[0]中的所有键值对rehash到ht[1]上面。(重新计算哈希值与索引值)
(3) ht[0]的值迁移到ht[1],释放ht[0],将ht[1]设置为ht[0],新创建一个ht[1]空白哈希表,为下一次rehash做准备。
哈希表的扩展与收缩
BGSAVE:在后台异步保存当前数据库的数据到磁盘。用于在后台异步保存当前数据库的数据到磁盘,命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。
BGREWRITEAOF:异步执行一个 AOF(AppendOnly File) 文件重写操作。重写会创建一个当前 AOF 文件的体积优化版本。即使 Bgrewriteaof 执行失败,也不会有任何数据丢失,因为旧的 AOF 文件在 Bgrewriteaof 成功之前不会被修改。
**注意:**从 Redis 2.4 开始, AOF 重写由 Redis 自行触发, BGREWRITEAOF 仅仅用于手动触发重写操作。
扩展条件
1)服务器没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
2)服务器正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
负载因子计算公式:
负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
**注:**BGSAVE命令或者BGREWRITEAOF命令执行时,Redis需要当前服务器创建子线程,因为写时复制技术的原因,服务器会提高执行扩展操作的负载因子,尽可能避免子进程存在期间,对哈希表进行扩展操作,避免不必要的内存写入操作,最大限度节约内存。
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
5. 渐进式 rehash
原因:针对于哈希表中的键值对过多,一次性将ht[0]中的所有键值对rehash到ht[1]上,可能会导致服务器短时间内停止服务。避免这种情况,分多次、渐进式地将ht[0]中的键值对rehash到ht[1]中。
哈希表渐进式rehash的步骤:
1)为ht[1]分配足够大小的空间,让dict同时持有两个哈希表。
2)在字典中维持一个索引计数器变量rehashidx,并置为0,表示rehash工作正式开始。
3)在rehash过程中,每次对字典的CRUD操作时,除了指定操作以外,还会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash完成后,将rehashidx属性的值增一。
4)当ht[0]的所有键值对被rehash到ht[1],则会将rehashidx属性的值设为-1,表示rehash操作完成。
特点:采用分而治之的方式,将rehash键值对所需的工作分摊到对字典的每个CRUD操作上,避免集中式rehash带来的庞大计算量。
当rehash过程中,字典会同时使用两个哈希表,查找时会先在ht[0]上执行,然后到ht[1]上查找。新增加的键值对将会添加到ht[1]中,保证了ht[0]上的键值对只减不增,直到ht[0]表为空释放。