1_3_Redis设计与实现读书记-字典

207 阅读7分钟

字典

字典: 又称符号表,是一种用于保存键值对(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]表为空释放。

6. 字典 API

7. 回顾