【Redis原理】数据结构(上)

发布于:2024-10-17 ⋅ 阅读:(8) ⋅ 点赞:(0)

动态字符串(SDS)

概念

Redis是使用C语言实现的,C语言字符串底层是字符数组,结束使用/0来表示,但Redis并没有直接使用C语言的字符串,因为C语言字符串存在多个问题:

  • 获取字符串长度需要运算
  • 非二进制安全
  • 不可修改(保存在常量池中,是不能修改的)
    所以Redis创建了一种新的字符串结构,称之为简单动态字符串
    Redis是使用C语言实现的,其中SDS是一个结构体

Redis源码:
在这里插入图片描述
几个重点:

  • uint8_t:无符号8比特位整型,所以buf字符数组中最多保存2^8-1=254个字符(此处是sdshdr8类型的SDS)
  • Unsigned char flags:指代SDS五种类型SDS_TYPE_5 ,SDS_TYPE_8,SDS_TYPE_16,SDS_TYPE_32,SDS_TYPE_64

所以,一个SDS字符串的结构如下:
在这里插入图片描述

SDS特点

SDS之所以叫做动态字符串,是因为它具备动态扩容的能力
在这里插入图片描述

SDS的优势

  1. 获取字符串长度的时间复杂度为O(1)
  2. 支持动态扩容
  3. 减少内存分配次数(因为做了内存的预分配)
  4. 二进制安全(不以\0为结束标志,而是len属性来维护一个字符串)

IntSet

概念

IntSetRedisset集合的一种实现方式,基于整数数组来实现,并具备长度可变,有序等特征
在这里插入图片描述

  • uint32_t length:contents[]数组中的元素个数;
  • int8_t contents[]:存放整数数据
  • encoding包含三种模式,表示存储整数的大小;
    在这里插入图片描述

IntSet的特点

升序

为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图:
在这里插入图片描述

统一的编码格式

为了快速定位数据,统一编码格式后,可以根据索引下标第一个数据的内存地址,算出偏移量.
这里的理解方式如下:
在这里插入图片描述
我们获得了第一个数据的内存地址之后,由于编码方式是统一的,那么这里的每一个整数数据都是占有2个字节,那么第三个数据的偏移量为2*2个字节,这样我们就可以直接得出第三个数据的地址,而不需要进行遍历(这也就是我们的索引要从0开始的原因)

计算公式:startptr+(sizeof(encoding)*index)

刚才的例子中,所有数据可以使用16Byte来表示,那么如果这时加入一个数据,无法使用16Byte来表示了,例如:5000,该怎么办?这就涉及我们的IntSet自动升级了

IntSet自动升级

在这里插入图片描述
升级的流程:

  • 升级编码为INTSET_ENC_INT32,每个整数占4字节,并按照新的编码方式以及元素个数扩容数组
  • 倒序依次将数组中的元素拷贝到扩容后的正确位置
  • 将待添加的元素放到数组末尾
  • 最后将intsetencoding属性改为INTSET_ENC_INT32 ,将length属性改为4

Dict

概念

我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查.而键与值的映射关系正是通过Dict来实现的.
Dict是由三部分组成:哈希表(DictHashTable),哈希节点(DicEntry),字典(Dict)
当我们向Dict添加键值对的时候,Redis首先根据Key计算出hash值(h),然后利用h&sizemask(由于size=sizemask-1h%size的效果是一样的)来计算元素应该存储到数组中哪个索引的位置
下图是这三部分结构体的设计:

在这里插入图片描述

这三者的关系是:
在这里插入图片描述

Dict特点

Dict的伸缩

Dict的扩容

Dict的扩容:Dict中的HashTable就是数组结合单向链表来实现的,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低.
Dict每次新增键值对时都会检查负载因子(LoadFactor=used/size),满足以下两种情况时会触发哈希表扩容:

  • 哈希表的LoadFactor>=1,并且服务器没有执行bgsave或者bgrewriteaof后台进程(后台进程的对CPU的消耗是很大的);
  • 哈希表的LoadFactor>5;
    扩容的源码:
    在这里插入图片描述
    注意:这里的扩容大小为used+1,但并不是代表就是扩容used+1,而是扩容到第一个大于used+12^n
Dict收缩

Dict收缩:每次删除元素时,也会对负载因子做检查,当LoadFactor<0.1时,会做哈希收缩.
Dict执行哈希收缩的原码:
在这里插入图片描述

Dictrehash

不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的sizesizemask发生变化,而key的查询与sizemask有关.因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash.过程是这样的:

  1. 计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩:
  • 如果是扩容,则新的size为第一个大于等于dict.ht[0].used+12^n
  • 如果是收缩,则新的size为第一个的大于等于dict.ht[0].uesd2^n(不得小于4)
  1. 按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]
  2. 设置dict.rehashidx=0,标示开始rehash
  3. dict.ht[0]中的每一个dictEntryrehashdict.ht[1]
  4. dict.ht[1]赋值给dict.ht[0],给dict.ht[0]初始化为空哈希表,释放原来的dict.ht[1]内存
    如图所示:
    在这里插入图片描述
渐进式哈希

因为新增或者删除操作一般来说都是在**Redis主线程中进行的,所以如果在主线程中触发了rehash操作,在数据量很大的情况下,可能会导致出现主线程阻塞的情况.
所以在Redis当中,rehash
分多次,渐进式**完成的,因此称之为渐进式rehash.流程如下:

  1. 计算新hash表的size,值取决于当前要做的是扩容还是收缩:
  • 如果是扩容,则新的size为第一个大于等于dict.ht[0].used+12^n
  • 如果是收缩,则新的size为第一个的大于等于dict.ht[0].uesd2^n(不得小于4)
  1. 按照新的size申请内存空间,创建dictht,并赋值给dict.ht[1]
  2. 设置dict.rehashidx=0,标示开始rehash(这里标记成0的原因是进行数据迁移的时候,下标就是从0开始的)
  3. dict.ht[0]中的每一个dictEntryrehashdict.ht[1]
    每次执行到新增,查询 ,修改,删除操作时,都检查一下dict.rehashidx是否大于-1,如果是,则将dict.ht[0].table[rehashidx]entry链表rehashdict.ht[1],并且将rehashidx++.直至dict.ht[0]的所有数据都rehash到了dict.ht[1]

    所以,渐进式哈希,相当于一次每次只是迁移了一个哈希节点上的链表
  4. dict.ht[1]赋值给dict.ht[0],给dict.ht[0]初始化为空哈希表,释放原来的dict.ht[0]内存
  5. rehashidx赋值为-1,代表rehash结束
  6. rehash过程中.新增操作,则直接写入ht[1],查询,修改和删除则会在dict.ht[0]dict.ht[1]依次查找并且执行.这样可以确保ht[0]的数据只减不增,随着rehash最终为空.

总结

Dict的结构

  1. 类似javaHashTable,底层是数组加链表来解决哈希冲突
    2. Dict包含两个哈希表,ht[0]平常用.ht[1]用来rehash

Dict的伸缩

  1. LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
  2. LoadFactor<0.1时,Dict收缩
  3. 扩容大小为第一个大于等于user+12^n
  4. 收缩大小为第一个大于等于user2^n
    5. Dict采用渐进式hash,每次访问Dict时执行一次rehash
  5. rehashht[0]只减不增,新增操作只在ht[1]执行,其他操作在两个哈希表

网站公告

今日签到

点亮在社区的每一天
去签到