Redis设计与实现要点(2)--链表与字典

1.链表

Redis链表的实现较为简单,主要结构为链表节点和链表头节点,链表节点(adlist.h/listNode)结构主要由以下几部分组成:

  1. 前置节点prev:链表节点类型的指针,指向前一个节点
  2. 后置节点next:同前置节点类型相同,指向后一个节点
  3. value:代表这个节点的值

链表头节点主要是为了更好地操作这个链表,主要有以下部分:

  1. 头节点head,链表节点类型指针,指向这个链表表头
  2. 尾节点tail,同头节点类型一样,指向表尾
  3. len:链表长度 unsigned long类型
  4. dup函数:节点值复制函数
  5. free函数:节点释放函数
  6. match函数:节点值对比函数

2.字典

2.1 字典结构

字典结构主要由三个部分构成,字典结构,hash表结构,hash表节点结构;
字典主要结构(dict.h/dict)如下:

  1. dictType *type:类型特定函数,dictType结构体主要存储了一些为hash表服务的函数(如计算hash值,复制键,复制值,销毁键,销毁值,对比键的函数)
  2. void *privatedata:私有数据,保存需要传递给类型特定函数的可选参数。这个属性和第一个属性主要是为了为多态服务。
  3. hash表数组:包含两个hash表,平常使用ht[0],rehash时使用ht[1]
  4. int rehashIdx:指示rehash的进度,若目前没有进行rehash则置为-1

hash表结构(dict.h/dict.ht)主要如下:

  1. dictEntry **table:hash表数组,里面包含多个hash表节点结构
  2. unsigned long size:hash表大小
  3. unsigned long sizemask:索引掩码,永远等于size-1,详情见hash算法一节
  4. unsigned long used :hash表已有节点数量

hash表(dictEntry)节点结构:

  1. void *key; 键
  2. union{
    void *val;
    uint64_t;
    int64_t s64;
    } v; 值,可以为一个指针,也可以为 uint64_t,int64_t的值
  3. struct dictEntry *next; 指向下一个节点
2.2 hash算法

Redis的hash算法和java的HashMap类似,主要分为两步

  1. 根据hash函数计算出key的hash值
  2. 用hash值和hash表的sizemask属性想与(&),计算出索引值
    这里解释一下为什么采用与操作可以计算索引值:因为sizemask的值为size-1,而size的大小我们通过源码可以看出初始化一个hash表他的大小为4,而扩容时又保证了他的size永远为2的n次幂,因此sizemask的二进制值永远为01…11(如初始化时为011),所以可以用与操作来计算索引值。
    源码中的dict.h定义了hash表的初始大小
2.3 解决键冲突

Redis的hash表解决键冲突的方法和java的HashMap一样采用拉链法(链地址法),即相同索引的键值对会放在一个链表上,不同的时java的HashMap会把新的对象放到链表末端(对应的访问时也会访问链表末尾),而Redis为了实现访问速度更快,因此把新节点放到了链表的头部(Redis链表有指向头结点的指针)

2.4 rehash(Hash表的扩展与收缩)

首先是hash表扩展和收缩的时间:主要与负载因子(used/size)有关。
当服务器没有执行BGSAVE命令和BGREWRITEAOF命令,负载因子大于1时进行扩展
当服务器执行BGSAVE命令和BGREWRITEAOF命令,负载因子大于5时扩展
BGSAVE命令和BGREWRITEAOF命令需要创建子进程,由于有很多系统都采用写时复制的方法与Redis交互,因此存在子进程时会尽量避免扩展操作,以减少对内存的读写。
另一方面,当负载因子小于0.1时,Redis进行收缩操作。

Hash表的扩展和收缩主要通过rehash来执行,rehash的步骤:

  1. 为字典的ht[1]分配空间:如果执行扩展操作,那么分配第一个大于等于ht[0].used * 2 的2的n次幂
    如果执行的是收缩操作,则分配第一个大于等于ht[0].used的2的n次幂
  2. 将ht[0]上的键值对rehash到ht[1]上。(采用渐进式rehash的方法)
  3. 当ht[0]上面的所有值均处理完毕后,将ht[1]赋给ht[0],在为ht[1]分配一个新的Hash表
2.5渐进式rehash

渐进式rehash主要是为了避免一次将ht[0]上面的数据rehash到ht[1]上导致的服务器停止服务(Redis为单线程)。所以采取渐进式的方式,逐渐的将ht[0]上的数据rehash到ht[1]上。主要步骤如下:

  1. 为ht[1]分配空间,此时字典同时持有ht[0]和ht[1]两个hash表。
  2. 再字典中维持一个索引计数器变量rehashidx,并将其设为0,代表rehash开始
  3. 再reahsh期间,每次对字典添加,删除,查找或者更新操作时,均会顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],并对rehashidx增一。(遇到rehashidx上值为null会跳过)
  4. 当ht[0]上的所有键值对都完成rehash之后,将rehashidx的值置为-1。代表rehash结束。
    在rehash期间,所有的增加操作均会增加到ht[1]上,查找时会先在ht[0]上查找,查不到的话会查找ht[1]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值