Redis-字典

Redis字典使用哈希表作为底层数据结构,通过哈希算法解决键冲突。当负载因子达到一定阈值时,Redis会进行渐进式Rehash操作以扩展或收缩哈希表,确保性能。在Rehash过程中,字典同时维护两个哈希表,新添加的键值对仅存入ht[1],查找操作则在两个表中进行。
摘要由CSDN通过智能技术生成

Redis字典

字典中每个键对应一个值,并且字典中的键是独一无二的。C语言没有内置字典这个数据结构,Redis实现了该数据结构。字典在Redis中的使用是相当普遍的,例如对于Redis键值对数据库,底层就是使用字典实现的。

例如

redis> set k1 "redis"

其中键”k1”、值”redis”,就是保存在代表Redis数据库的字典里面的。

Redis字典底层是使用哈希表实现的,熟悉Java哈希表的同学对该数据结构应该比较清楚,Redis哈希表和Java哈希表的实现有很多相同之处。

一个哈希表包括若干哈希表结点,每个结点就是一个键值对。

哈希表

Redis的哈希表定义如下

typedef struct dictht {
  //哈希表数组
  dictEntry **table;
  //哈希表大小(table数组的大小)
  unsigned long size;
  //哈希表大小掩码,用于计算索引值,其值总是等于size-1
  unsigned long sizemask;
  //哈希表已有结点的数量
  unsigned long used;
} dictht;
  • 其中table是一个数组,数组中保存的是dictEntry,每个dictEntry是一个键值对
  • size记录了哈希表的大小,也就是table数组的大小
  • sizemask是掩码,用于计算索引值,其值总是等于size-1。原理如下

key%size == key & (size-1)

  • used记录了哈希表已有结点的数量

这里写图片描述

图1

上图展示了一个空的哈希表

哈希表结点

Redis定义了哈希表结点,如下所示

typedef struct dictEntry {
  //键
  void *key;
  //值
  union {
    void *val;
    uint64_t u64;
    int64_t s64;
  } v;
  struct dictEntry *next;
} dictEntry;

其中

  • key保存着键值对的键
  • v保存着键值对的值
  • next保存下一个哈希表节点的指针。通过next指针可以将多个哈希表节点链接起来,解决哈希冲突。

这里写图片描述

图2

如上图所示,关键字k1和k0的索引值都是1,通过next指针将这两个哈希节点链接在一起

字典

Redis中定义的字典如下

typedef struct dict {
  //类型特定函数
  dictType *type;
  //私有数据
  void *privdata;
  //哈希表
  dict ht[2];
  //rehash索引,默认值-1
  int rehashidx;
} dict;
  • type是一个指向dictType的指针
  • dictType保存了若干函数的指针
  • privdata保存了传给这些函数的参数

Redis定义的dictType如下

typedef strut 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);
}
  • ht是一个哈希表数组,该数组大小为2,其中ht[0]是在普通状态下使用的,ht[1]是在对哈希表rehash时使用的。
  • rehashidx指示当前rehash的进度,当rehash=-1时表示当前不在rehash。例如当rehashidx=2时,表示正在进行ht[0]表索引2的rehash。

普通状态下的一个字典如下所示

这里写图片描述

图3

哈希算法

将一个新的键值对加入到字典时,程序将会根据键计算出哈希值,再根据这个哈希值和掩码去计算索引,最终将该键值对加入该索引对应的哈希链中。

例如,对于键值对(key,value),首先根据字典哈希算法计算出哈希值

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

当字典处于普通情况下,也就是字典不在rehash时,该键值对插入字典的ht[0]哈希表中,计算索引

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

当字典处于rehash状态时,该键值对插入字典的ht[1]哈希表中,计算索引

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

解决哈希冲突

当两个不同的键通过哈希算法映射到同一个索引时,即产生了哈希冲突。如上图2,键k1和键k0冲突了,Redis将具有相同的哈希值的键值对放在一个链表中,并将最新的键值对加入到链表的头以提高效率。

Rehash

随着程序的运行,字典的大小可能扩展或者收缩,为了提高效率,Redis动态地rehash哈希表。

rehash步骤如下

  1. 为字典的ht[1]哈希表分配空间,这个哈希表需要分配的大小取决于执行的操作(扩展or收缩)以及ht[0].used的大小,具体地
    • 如果执行的是扩展操作,那么ht[1]的大小取大于等于ht[0].usedx2的第一个2^n^ (其中n是一个正整数)。例如,当ht[0].used=5的时候,那么ht[1]的大小取大于等于5x2=10的第一个2^n^,也就是16。
    • 如果执行的是收缩操作,那么ht[1]的大小取大于等于ht[0].used的第一个2^n^。例如,当ht[0].used=5的时候,那么ht[1]的大小取大于等于5的第一个2^n^,也就是8。
  2. 将保存在ht[0]中的所有键值对rehash到ht[1]中,也就是重新计算哈希索引,并根据新计算出的哈希值将键值对放到ht[1]的正确位置上。
  3. ht[0]中的所有值都转移到ht[1]后,释放h[0],将ht[0]设置为指向ht[1],新建一个空的哈希表,并将ht[1]指向该空的哈希表,为以后rehash做准备。

哈希表的扩展和收缩

首先介绍下负载因子的概念

负载因子 = 哈希表已经保存的哈希结点的数量 / 哈希表的大小

即:load_factor = ht[0].used / ht[0].size

  • 当满足如下任一条件时,Redis将会执行扩展操作

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

为何扩展时会根据是否正在BGSAVE或BGREWRITEAOF来动态决定负载因子所需要的大小呢?简单解释一下,以后还有待深入研究:

Redis在执行BGSAVE或BGREWRITEAOF命令时,会fork一个子进程来执行这个操作。操作系统会通过写时复制技术(Copy On Write) 技术来提高效率。写时复制指的是,当进程fork一个子进程时,子进程其实和父进程是共用一个相同的物理空间的,只有父或子进程真正执行写操作时(系统调用),系统才会为子进程分配相应的物理内存空间。

这样如果Redis将负载因子设置为1,Redis执行BGSAVE或BGREWRITEAOF,系统就会过早的为子进程分配内存空间,内存写入操作较为频繁。

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

渐进式Rehash

rehash过程会将ht[0]的所有键值对转移到ht[1]中,但是这个操作不是一次性完成的。如果ht[0]里的键值对只有几个或者几十个,那么rehash操作会很快完成,但是若ht[0]的键值对有几亿甚至几十亿个的时候,这个rehash操作可不是立马就能执行完成的,如果一次性执行完rehash操作,redis将很难再响应正常的应用请求,吞吐量下降很快。因此rehash是渐进式的、分多次执行的。

渐进式rehash步骤:

  1. 为字典的ht[1]哈希表分配空间,做好rehash的准备
  2. 将字典的rehashidx设置为0,表示开始rehash
  3. 在rehash期间,每次对字典执行增、删、改、查的时候,还会顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1]中。当本次rehash完成后,rehashidx递增1
  4. 随着程序的进行,在某个时刻,ht[0]上的所有键值对都会被rehash到ht[1],此时将rehashidx设置为-1,表示rehash结束

Redis将Rehash过程分摊到了每一次的增、删、改、查操作中,避免了一次性执行rehash带来的巨大开销。

渐进式Rehash过程中,字典实际上是拥有两个哈希表(ht[0]和ht[1])的,字典的删除、查找、更新操作会在两个表中进行。对于查找操作,首先会去ht[0]去查找,如果没有查到会去ht[1]去查找。

渐进式Rehash过程中,对字典的添加操作只会在ht[1]中操作而不会在ht[0]中操作,这保证了在渐进式Rehash过程中ht[0]的哈希结点数只会减少,而不会增加,并最终会变为空表。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值