03 Redis-字典

本文内容均来自《Redis设计与实现》一书 

Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作也是构建在对字典的操作之上的。

字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,Redis会使用字典作为哈希键的底层实现。 

1.定义

结构 

// 哈希表
typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值(size - 1)
    unsigned long sizemask;

    // 该哈希表已有的节点数量
    unsigned long used;
} dictht;

// 哈希表节点
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;    

// 字典
typedef struct dict {
    
    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash索引(不进行rehash时为-1)
    int trehashidx;
} 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 void *key2);

    // 销毁键的函数
    void (*keyDistructor) (void *privdata, void *key);    

    // 销毁值的函数
    void (*valDistructor) (void *privdata, void *obj); 
} dictType;

dictht(哈希表)

table:是一个数组,数组中的每个元素指向一个dictEntry结构的指针,每个dictEntry结构保存着一个键值对。

size:记录哈希表的大小,即table数组的大小。

used:记录哈希表目前已有节点的数量。

sizemask:总是等于size-1,和哈希值一起决定一个键放到table数组的哪个索引位置。

dictEntry(哈希表节点)

key:保存键值对中的键。

v:保存键值对中的值(一个指针,一个uint64_t整数,或者一个int64_t整数)。

next:指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突问题。

dict(字典)

type:指向一个dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。

privdata:保存需要传给那些类型特定函数的可选参数。

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

rehashidx:与rehash有关,记录rehash目前的进度,如果目前没有进行rehash,值为-1。

字典示例 

2.哈希算法 

当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放在哈希表数组的指定索引上面。

Redis使用MurmurHash2算法来计算键的哈希值。其优点在于:即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度非常快。

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

  • 使用字典设置的哈希函数,计算key的哈希值

        hash = dict  -> type -> hashFunction(key);

  • 使用哈希表的sizemask属性和哈希值,计算出索引值(h[x] 可以是h[0]或h[1])

        index = hash & dict -> ht[x].sizemask;

3.键冲突

当有两个或者以上的数量的键被分配到哈希表数组的同一个索引上面时,我们称这些键发生了冲突。

解决方法

链地址法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的节点可以用这种方式连接起来,就解决了键冲突的问题。

示例

假设新增的键值对<k2, v2>的键k2和键值对<k1, v1>的键k1产生了冲突。

因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新的节点添加到链表的表头位置(复杂度为O(1)),排在其他已有节点的前面。 

4.rehash 

随着操作的不断执行,哈希表中保存的键值对会逐渐地增多或减少,为了让哈希表地负载因子(load factory)维持在一个合理地范围之内,当哈希表保存地键值对数量太多或者太少时,程序需要对哈希表地大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行rehash操作来完成。

Redis对字典的哈希表执行rehash的步骤 

  • 为字典的h[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(即ht[0].used的属性值):如果是执行扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2^n;如果执行的收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n。
  • 将保存的ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放在ht[1]哈希表的指定位置。
  • 当ht[0]包含的所有键值对都迁移到ht[1]之后(此时ht[0]为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

哈希表的扩展与收缩

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作

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

哈希表的负载因子的计算公式

  • load_factory = ht[0].used / ht[0].size

根据BGSAVE命令或者BGREWRITEAOF命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,是因为在执行BGSAVE或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进城的子进程,而大多数操作系统都采用写时复制技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要内存写入操作,最大限度地节约内存。

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

5.渐进式rehash

 扩展和收缩哈希表可以通过rehash来完成,但是rehash并不是一次性、集中式地完成的,而是分多次、渐进式地完成。原因在于,如果哈希表中保存地键值对数量庞大,那么一次性将这些键值对全部rehash到ht[1]中地话,庞大地计算量可能会导致服务器在一段时间内停止服务。所以为了避免rehash对服务器性能造成影响,服务器并不是一次性将ht[0]中地所有简直对全部rehash到ht[1]中,而是分多次、渐进式地rehash到ht[1]中。

哈希表渐进式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操作已完成。

渐进式rehash的好处

采取分而治之的方式,将rehash键值对所需的计算工作平均分摊到对字典的每个增删改查操作上,从而避免集中式rehash带来庞大的计算量。

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

因为在渐进式rehash执行过程中,字典会同时使用ht[0]和ht[1]两个哈希表。所以在渐进式rehash进行期间,字典的增删改查操作都会在两个哈希表上进行。另外,渐进式rehash执行期间,新添加到字典的键值对一律会保存在ht[1]中,而ht[0]不进行任何添加操作,这一措施保证了ht[0]包含的键值对数量只减不增,并随着rehash操作的执行而最终变成空表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值