redis数据结构 (3)——字典

dict是一个用于维护key和value映射关系的数据结构 . Redis的一个database中所有key到value的映射,就是使用一个dict来维护的,key 是对象的名称,value 是各种不同的对象,所有的对象都挂在一棵字典上。除了容纳所有对象的主干字典外,还有容纳所有带过期时间的对象的过期主干字典,它的 key 是对象的名称,value 是对象的过期时间戳。

typedef struct redisDb {
    dict *dict;
    dict *expires;
    ...

} redisDb;

字典的 value 呈现出了多态性,它可以是一个单纯的整数或者浮点数,也可以是一个对象,会有一个统一的对象头,也就是前面的 redisObject 结构体(见该系列第一篇),会根据 type 字段和 encoding 字段来决定 ptr 字段指向的具体数据结构。我们来看一下字典的结构体代码定义

# 字典结构
typedef struct dict {
    dictType *type; // 字典的接口实现,为字典带来多态性
    void *privdata; // 存储字典的附加信息
    dictht ht[2]; // 注意这里不是指向指针的数组,为什么?
    long rehashidx; // 渐进式rehash时记录当前rehash的位置
    unsigned long iterators;
} dict;

type 和 privdata 是针对不同类型的键值对,为创建多态字典而设置的

type指向一个 dictType 结构的指针, 每个dictType 结构保存了一簇用于操作特作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数.而pridata 则保存了需要传给那些特定类型函数看可选参数.

ht 属性,包含两个数组,数组的每一项都是一个 dictht 哈希表,一般情况下字典只使用ht[0] 哈希表,ht[1]哈希表只会在对哈希表进行 rehash 时使用.

rehashidex 记录了 rehash 当前的进度,如果没有进行 rehash, 值就为-1.

# dict 哈希表 结构
typedef struct dictht {
    dictEntry **table; // 指向第一维数组
    unsigned long size; // 数组的长度
    unsigned long sizemask; // 用于快速hash定位 sizemask = size - 1
    unsigned long used; // 数组中的元素个数
} dictht;

# 定义了字典功能的接口
typedef struct dictType {
    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;

# key-value 哈希表节点
typedef struct dictEntry {
    void *key;   // 键
    union {
        void *val; //指向 sds|set|dict|zset|quicklist  的指针
        uint64_t u64; // 用于过期字典,val存储过期时间戳
        int64_t s64; 
        double d; // 用于zset,存储score值
    } v; //值
    struct dictEntry *next; //指向下一个节点的指针 形成单向链表
} dictEntry;

字典的哈希表结构和java的HashMap 结构几乎是一样的,都是通过某个哈希函数从key计算得到在哈希表中的位置,采用链表法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。和java hashMap 不同的地方就在于 rehashing这个过程,redis采用了一种称为渐进式重哈希的方法,在需要扩展内存时避免一次性对所有key进行重哈希,而是将重哈希操作分散到对于dict的各个增删改查的操作中去。这种方法能做到每次只对一小部分key进行重哈希,而每次重哈希之间不影响dict的操作。dict之所以这样设计,是为了避免重哈希期间单个请求的响应时间剧烈增加

当添加一个 key-value到字典里面的时候,程序需要先根据key来计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希数组的指定索引上面 , 如果当前dictEntry数组位置上已经有数据了,那么就采用拉链法避免哈希碰撞。redis这里使用的是单链表,所以为了查询效率每次把新数据插入到链表头位置(新插入的数据被访问的概率大)

Redis计算哈希值和索引值的方法如下:

# 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);

# 使用哈希表的 sizemask 属性和哈希值,计算出索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

为了能更清楚地展示dict的数据结构定义,我们用一张结构图来表示它(没有rehash的情况下)。如下。

上图看出,redis键过期时间是单独的一个字典来描述的,和主干字典 有以下不同

(1)过期字典是一个指针,指向键空间的某个键对象。

(2)过期字典的值是一个longlong类型的整数,这个整数保存了键所指向的数据库键的过期时间–一个毫秒级的 UNIX 时间戳。

 

思考:redis为什么不像java 1.8 hashMap 那样当链表达到一定的长度转换为一棵红黑色来 提高查询效率呢?

Rehash

哈希表的键值会不听的增多减少,为了让负载因子,维持在一个合理的范围,我们需要适当的进行扩展和收缩

rehash过程如下:

  • 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used属性的值)如图:
    1. 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
    2. 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 2

  • 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。如图

  • 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

思考:为什么每次扩容都是2的倍数

渐进式rehash

redis的设计目的之一就是追求 '快速响应时间',如果像java hashMap 那样扩容一个2倍数组,当redis中包含大量键值对的时候,势必会造成停顿所以为了防止这种情况的发生,redis采用了渐进式rehash。当字典需要扩容时,它会申请一个新的 hashtable 放在字典的 ht[1] 中,在迁移完成之前新旧两个 hashtable 将会共存,也就是 ht[1] 和 ht[0] 两个字段值同时存在。

在后续该字典的每个指令中,Redis都会将ht[0]的一部分键值对迁移到  ht[1]中。目前渐进式迁移每次迁移 10 个槽位,也就是最多 10 个链表。

 

上文提到dict中rehashidx字段用来维护当前rehash的进度,很多材料说该字段在每次rehash完成后+1是不对的,该字段的准确语义是记录当前rehash的进度(ht[0]中迁移到的位置)

 

渐进式rehash的好处在于其采取分而治之的方式,将rehash键值对所需要的计算工作均摊到字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。

渐进式rehash执行期间的哈希表操作

因为在渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作都是在两个表上进行的。

例如,查找操作会先在ht[0]上进行,如果没找到再在ht[1]上进行。

添加操作的键值对会一律保存到ht[1]中,这一措施保证ht[0]包含的键值对只会减少不会增加。

 

哈希表扩容与收缩的条件

1.服务器目前没有在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1

2.服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5

3.当哈希表负载因子小于0.1时,程序自动开始对哈希表执行收缩操作

原创文章 15 获赞 22 访问量 6942

猜你喜欢

转载自blog.csdn.net/qq_31387317/article/details/93105637
今日推荐