Redis~哈希(Hash)类型的数据结构之字典

哈希类型

  • 字典类型 (Hash) 又被成为散列类型或者是哈希表类型,它是将⼀个键值 (key) 和⼀个特殊的“哈希表”关联起来。相当于java中的Map<Object, Map<Object, Object>>
    -
  • 哈希类型的底层数据结构可以是压缩列表(ZipList)或者字典(Dict)
  • 当哈希对象的所有键值对的键和值的字符串长度都小于64字节,并且保存的键值对数量小于512个时,使用压缩列表, 压缩列表在上一篇文章中讲到
    在这里插入图片描述
    在这里插入图片描述
  • 如果不满足上述条件中的任意一个,都会使用字典

字典介绍

  • 在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就称为键值对
  • 字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等

字典的底层实现

-Redis的字典底层采用了哈希表来进行实现。一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对

哈希表(struct dictht)

  • Redis字典所使用的哈希表由dict.h/dictht结构定义:
typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值。总是等于size-1
    unsigned long sizemask;
    //该哈希表已有节点的数量
    unsigned long used;
} dictht;

成员介绍:

  • table属性:是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个 dictEntry结构保存着一个键值对

  • size属性:记录了哈希表的大小,也即是table数组的大小

  • used属性:则记录了哈希表目前已有节点(键值对)的数量

  • sizemask属性:的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面

  • 下图展示了一个大小为4的空哈希表(没有包含任何键值对)
    在这里插入图片描述

哈希表节点

  • 哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry {
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    } v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

成员如下:

  • key属性保存着键值对中的键

  • v属性则保存着键值对中的值,值可以是 一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数

  • next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连 接在一次,以此来解决键冲突(collision)的问题

  • 下图就展示了如何通过next指针,将两个索引值相同的键k1和k0连接在一 起在这里插入图片描述

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

字典(struct dict)

typedef struct dict {
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash 索引。当rehash 不在进行时,值为-1
    in trehashidx; /* rehashing not in 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 void *key2);
    //销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    //销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;
 
  • 为保证字典具有多态及泛型,dictType中提供了如哈希函数以及K-V的各种操作函数,使得字典适用于多重情景

  • 下图展示了一个普通状态下(没有进行rehash)的字典
    在这里插入图片描述

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

#使用字典设置的哈希函数,计算键key 的哈希值, Redis使用MurmurHash2算 法来计算键的哈希值
hash = dict->type->hashFunction(key);
#使用哈希表的sizemask 属性和哈希值,计算出索引值
#根据情况不同,ht[x] 可以是ht[0] 或者ht[1]
index = hash & dict->ht[x].sizemask;

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

rehash(重新排列)

  • Redis作为一个插入频繁且对效率要求高的数据库,当插入的数据过多时,就会因为哈希表中的负载因子过高而导致查询或者插入的效率降低,此时就需要通过rehash来进行重新扩容并重新映射。
  • 但是如果只是用一个哈希表,映射时就会导致数据库暂时不可用,作为一个使用频繁的数据库,短期的停机几乎是不可容许的问题,所以Redis设计时采用了双哈希的结构,并采用了渐进式rehash的方法来解决这个问题。

rehash双哈希结构的实现

  • rehash双哈希结构实现步骤如下
  1. 为ht[1]的哈希表分配空间
  2. 将ht[0]中的键值对重新映射到ht[1]上
  3. 当ht[0]的数据迁移完成,此时ht[0]为一个空表,此时释放ht[0],并让ht[1]成为新的ht[0],再为ht[1]创建一个新的空白哈希表,为下一次的rehash做准备
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

渐进式rehash的实现

  • 由于数据库中可能存在大量的数据,而rehash的时候又过长,为了避免因为rehash造成的服务器停机,rehash的过程并不是一次完成的,而是一个多次的,渐进式的过程。
    -
    哈希表渐进式 rehash 的详细步骤:
  1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
  5. 渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值