Redis(3) —— 底层数据结构 —— 字典

字段是以 key-value 形式来存储数据的,c 语言中没有字典的数据结构,所以 Redis 构建了自己的字典实现

1. Redis 字典的用处
  • Redis 数据库就是以字典来存储数据
  • Redis 的散列表(哈希键) 使用字典来实现的


2. 字典的实现

哈希表的实现

Redis 字典所使用的哈希表由 dict.h/dictht 结构定义

typedf struct dictht{
    //哈希表数组
    dictEntry **table;
    
    //哈希表大小
    unsigned long size;
    
    //哈希表大小掩码,用于计算索引值
    //总是等于 size-1
    unsigned long sizemask;
    
    //该哈希表已有节点的数量
    unsigned long used;
}


哈希表节点的实现

哈希表节点使用 dictEntry 结构表示,每个 dictEntry 结构都保存着一个键值对

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


字典的实现

Redis 中的字典由 dict.h/dict 结构表示

typedef struct dict{
    //类型特定函数
    dictType *type;
    
    //私有数据
    void *privatedata;
    
    //哈希表
    dictht ht[2];
    
    //rehash 索引
    //当 rehash 不在进行时,值为 -1
    int trehashidx;
}dict;
  • type 属性和 private 属性是针对不同类型的键值对,为创建多态字典而设置的。
  • type 属性指向一个 dictType 结构的指针,每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数
  • private 属性保存了需要传给那些类型特定函数的可选参数
  • ht[2] 保存了两张哈希表, 0 号哈希表一般正常使用,1 号哈希表只有在 rehash 的时候才会用到
  • trehashidx 记录当前 rehash 的进度,如果没有开始 rehash,则 trehashidx 为 -1


3. 哈希算法

将一个键值对添加到字典里面时,需要经过两个步骤计算出键值对应该放置的位置

  • 使用 dict 中的 type 里面的 HashFunction 计算出 key 的哈希值
  • 用计算出的哈希值与 dict 里面的 ht(散列表) 中的 sizemask 做 &(与)操作,得出键值对应该放的位置(index)


4. 解决键冲突

Redis 用链地址法来解决键冲突。

程序总是将新节点添加到链表的表头位置(头插法)



5. rehash

随着操作的不断进行,哈希表保存的键值对会逐渐增多或减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表大小进行相应 的扩展或者收缩

扩展和搜索通过 rehash 来完成

rehash 步骤如下:

  1. 为字典 ht[1] 分配空间,分配空间的大小取决于要执行的操作以及当前键值对的数量(ht[0].used 属性的值)
    • 如果是扩展操作,那么 ht[1] 分配的空间为 2n, 使 2n >= ht[0].used * 2, 且 n 要最小
    • 如果是收缩操作,那么 ht[1] 分配的空间为 2n, 使 2n >= ht[0].used, 且 n 要最小
  2. 将保存在 ht[0] 上面的键值对 rehash 到 ht[1] 上,rehash 是指重新计算键值对在 ht[1] 上的哈希值和索引值,然后将键值对放到 ht[1] 上
  3. rehash 完成之后,释放 ht[0] 的空间,将 ht[1] 设置为 ht[0], 并将 ht[1] 的位置设置为一个空的哈希表,为下次 rehash 做准备


6. 哈希表扩展与收缩的条件

哈希表的负载因子 = ht[0].used / ht[0].size

换种方式就是说:哈希表的负载因子等于哈希表平均每个桶下边挂载的节点数量

哈希表的扩展条件:

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

哈希表的收缩条件::

  1. 当哈希表的负载因子小于等于 0.1


7. 渐进式 rehash

rehash 不是一次性执行完的,而是在每次对字典进行 增删改查 的时候就会进行一次 rehash

这样做是为了将 rehash 操作均摊到每一次的增上改查上,避免集中式 rehash 带来的庞大计算量

rehash 的步骤:

  1. 为 ht[1] 分配空间
  2. 将字典中的 rehashidx 设置为 0, 表示 rehash 开始
  3. 在每次对字典进行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表的 rehashidx 索引上的所有键值对 rehash 到 ht[1] 上,之后将 rehashidx 加一
  4. 当 rehash 执行完的时候,也就是 ht[0] 上的键值对全部转移到 ht[1] 上的时候,程序将 rehashidx 设置为 -1, 表示 rehash 工作已经完成

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

  1. 字典的删除、查找、更新等操作会在两个哈希表上进行
  2. 字典的添加操作一律在 ht[1] 哈希表上操作




参考资料

[1].《Redis 设计与实现》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值