Redis-基本数据结构

简单动态字符串(SDS)

  1. SDS在Redis中的应用
    * 键值对中,键和值得底层实现都是保存着字符串的SDS
    * AOF模块的AOF缓冲区、客户端状态的输入缓冲区
  2. SDS的结构
    在这里插入图片描述
    在这里插入图片描述
    这其中,len记录字符串中保存的长度,free记录剩余多少未使用的字节,buf表示字符串数组
  3. SDS的优点
    1. 因为自身保存着字符串长度len,所以SDS在计算长度时,时间复杂度为O(1),这也保证了获取字符串长度不会成为Redis的瓶颈
    2. 缓冲区不会溢出。当需要对SDS字符串进行修改时,API会检查SDS的空间是否满足修改需求,如果不满足,SDS会进行自动空间扩展至执行修改所需要的大小,再进行修改操作。
    3. 减少修改字符串时带来的内存重分配次数。SDS通过未使用空间解除了字符串 长度和底层数组长度的关联,即在buf数组中,数组长度不一定全部填充了字符,而可能包含未使用的字节,由free记录。
    4. 二进制安全:所有的SDS API都会以处理二进制的方式来处理buf中的数据,但程序写入是如何,读取也是如何。Redis中可以通过保存文本数据,也可以保存二进制数据
  4. 两种对未使用空间的优化策略
    1. 空间预分配
      用于字符串增长操作。当对SDS进行修改并且需要进行空间扩展时,程序会分配必要的空间和额外的未使用空间。如果SDS长度小于1MB,程序就会为free属性分配和len属性相同的值,未使用空间可以进行下一次分配(len占用13个字节,则未使用空间free也是13个字节,还有一个额外的字节保存空字符)。如果长度≥1MB,则会分配1MB的未使用空间(len=30MB,free=1MB,another=1byte)。
      总结:在扩展空间前,API会先检查未使用空间是否足够,如果足够就直接使用,如果不足就进行空间预分配操作,这使得连续增长N次字符串所需的内存重分配次数从必定N次变成最多N次!
    2. 惰性空间释放
      用于字符串缩短操作。当API需要缩短字符串时,程序并不会收缩多出来的字节,而是将这些记录在free属性中,作为未使用空间进行分配。以后进行增长操作时可以直接使用

链表

  1. 链表提供了高效的节点重排能力顺序性的节点访问方式,并且通过增删节点来调整链表长度
    在这里插入图片描述
  2. 链表的应用场景:list键(lpush integer 1 2 3)、发布与订阅、慢查询、监视器
  3. 链表的特性
    1. 双端链表。链表带有prev和next指针,可以获取节点前驱节点和后继节点,复杂度O(1)
    2. 无环。链表头prev指针和结尾的next指针都是指向NULL,所以不管从头到尾还是从尾到头,都会以NULL为终点。
    3. 带表头指针和表尾指针。可以list的head和tail指针来获取头节点和尾节点,时间复杂度为O(1)
    4. 多态。链表可以保存各种不同类型的值。
    5. 带链表长度计数器。通过len属性来对list持有的节点进行统计,程序获取节点数量的时间复杂度为O(1)。

字典(dict)

字典是用来保存键值对的抽象数据结构,每个键都是唯一的。还是哈希键的底层实现之一,当哈希键包含的键值对比较多或者元素都比较长,redis会使用字典。

  1. 字典的实现
    每个哈希表节点就保存了字典中的一个键值对。

    1. 哈希表的结构
      在这里插入图片描述

    每个dictEntry保存一个键值对,size记录哈希表的大小,used记录哈希表目前的节点数。
    next属性是指向另一个节点的指针,通过将多个哈希值相同的键值对连接在一起来解决键冲突问题(链地址法)。

    1. 字典的结构
      在这里插入图片描述
      上面的图片中,type和privdata是针对不同类型的键值对,为创建多态字典而设置的。
      ht属性包含了一个两个项的数组,其每个项都是一个哈希表,一般情况字典只使用ht[0]哈希表,而ht[1]哈希表用来进行rehash
      rehashidx属性记录了rehash的当前的进度,如果正在rehash则为0,没有进行rehash则是-1.
  2. 哈希算法和冲突解决

    1. 哈希算法当添加一个新的键值对时,首先根据键值对的键计算出哈希值,再通过哈希值计算出索引值,根据索引值,将包含新键值对的哈希表节点放到哈希数组的指定索引上面。使用的MurmurHash2算法能给出一个很好的随机分布,速度快。
    2. 冲突解决:当两个及以上的键被分配到同一索引上,redis通过链地址法进行冲突解决,将冲突的哈希表节点通过next指针进行一个连接,连接成一个单向链表
      在这里插入图片描述
  3. rehash
    1. rehash的步骤:rehash是渐进的、分多次进行的
    1. 为字典的ht[1]分配空间,让字典同时拥有两个哈希表,如果执行的是扩展操作,那么ht[1]的大小是第一个大于等于ht[0].used*2的2^n^(假设ht[0].used*2=14,那么ht[1]的大小是16)。如果执行的是收缩操作,那么ht[1]的大小是第一个大于等于ht[0].used的2^n^
    2. 在字典中维持一个索引计数器变量rehashidx,并设置为0,表示开始rehash。
    3. 将保存在ht[0]的所有键值对rehash到ht[1]上,即重新计算键的哈希值和索引值,再放到ht[1]上面。如果此时对字典进行增删改查操作会在两个表上进行,例如查找,会在ht[0]中先查找,找不到再去ht[1],添加操作则直接保存在ht[1]中,不对ht[0]进行操作,保证只减不增
    4. 当ht[0]包含的键值对都迁移到ht[1]上,释放ht[0],并将ht[[1]设置为ht[0],创建一个空哈希表ht[1]。
    在这里插入图片描述

    1. 哈希表的扩展与收缩操作
      1. 满足以下任意一个条件,就执行扩展操作
        • 服务器没执行BGSAVE或BGREWRITEAOF,且哈希表的负载因子≥1
        • 服务器执行BGSAVE或BGREWRITEAOF,且哈希表的负载因子≥5
      2. 因为在执行BGSAVE期间,redis创建当前服务器进程的子进程,这个操作采用写时复制来进行优化,所以在子进程存在期间,需要提高负载因子来避免在子进程期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度节约内存
      3. 当负载因子<0.1的时候就要进行收缩操作

跳跃表(skiplist)

跳跃表是一种有序的数据结构,通过在每个节点中维持**多个**指向其他节点的指针,到达快速访问的目的。由zskiplistzskiplistNode两个结构组成,其中zskiplist用于保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode表示跳跃表节点。
在这里插入图片描述

  1. 跳跃表的结构
    1. header:表头节点
    2. tail:表尾节点
    3. level:层数最大的节点的层数,不包含表头,上图为5
    4. length:跳跃表的长度,包含的节点数量
    5. 前进指针:用于访问位于表尾方向的其他节点。
    6. 跨度:当前节点到下一节点的距离
    7. 后退指针:指向位于当前节点的前一个节点,从表尾遍历到表头时使用。每个节点只有一个后退指针,每次只能后退至前一个节点
    8. 分值:存放是zset中的score,在跳跃表中从小到大排列,
    9. 成员对象:节点中的o1 o2 o3就是保存的成员对象。指向一个字符串对象,保存着一个SDS值。分值相同的成员变量在字典序中按大小进行排序,小得在前,大的在后。

整数集合

整数集合是集合键的底层实现之一,如果集合中只包含整数值且数量不多,可以用整数集合来实现集合键。整数集合的保存类型为int16_t,int32_t,int64_t。只支持升级不支持降级。例如下图:
在这里插入图片描述

  1. 整数集合的升级
    如果添加一个新元素到整数集合时,新元素的类型比整数集合现有的元素类型都要长,就要先进行升级。
    1. 升级的步骤:
      1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
      2. 将底层数组现有的所有元素转化成新元素相同的类型,进行空间重分配,将类型转换后的元素放置到正确的位置。
      3. 添加新元素到底层数组。
    2. 升级之后,如果新元素小于现有的元素,就将新元素放置在数组最开头。如果大于所有现有的元素,就放置在数组的末尾。
    3. 升级的好处提升整数集合的灵活性,节约内存。整数集合可以自动升级底层数组来适应新元素,不必担心出现类型错误。同时也可以让集合保存三种不同类型的值,且升级只有在有需要的时候进行。

压缩列表(ziplist)

压缩列表是列表键和哈希键的底层实现之一。当只有少量列表项且列表项要么是小整数要么是短字符串时,用压缩列表实现。

  1. 压缩列表节点的结构
    在这里插入图片描述
    previous_entry_length记录该节点的前一个节点的长度。
    encoding记录content属性所保存数据的类型以及长度。
    content负责保存节点的值,可以是一个字节数组或者整数。

对象

redis中,每个键值对中键和值都是一个对象。Redis中共有字符串(String)、列表(list)、哈希(hash)、集合(set)、有序集合(zset,sorted set)。
Redis对象通过带有引用计数器的内存回收机制进行内存回收,当一个对象不再被使用,就会被释放。对象会记录自己的最后一次访问的时间用来计算空转时间。
在这里插入图片描述

  1. 字符串对象
    从上图可知,字符串对象的编码是int、raw、embstr。如果保存的是整数值,可以设置编码为int。如果是长度小于等于32字节的字符串可以使用embstr。

    1. int、embstr和raw的转换关系:当int中的对象保存的不再是整数值,而是字符串值,那么字符串对象的编码变成raw。同样对于embstr字符串执行任何修改命令,字符串的编码也会先变成raw再进行修改。
  2. 列表对象
    列表对象的编码是ziplist或者LinkedList。其中ziplist编码的列表对象使用压缩列表来实现,LinkedList编码的对象使用的双端链表作为底层实现,每个节点保存一个字符串对象,字符串对象中保存一个列表元素。

    1. 编码转换: 当对象同时满足这两个条件时才能使用ziplist进行编码:列表对象保存的字符串元素都小于64字节列表对象保存的元素个数小于512个。如果不能满足则使用LinkedList。
  3. 哈希对象
    哈希对象的编码可以是ziplist或者hashtable。当使用ziplist时,每当有新的键值对添加到哈希对象,程序先将保存了键的压缩列表节点推入到压缩列表的尾部,然后将保存值的压缩列表节点推入到压缩列表尾部。

    1. 编码转换:如果hash对象使用ziplist编码时比如满足:所有的键值对的键和值的字符串长度都小于64字节保存的键值对数目小于512个。如果不满足任意一个就转换成hashtable
  4. 集合对象
    编码可以是intset(整数集合)或者hashtable。
    在这里插入图片描述

    1. 编码转换:当同时满足:所有元素都是整数值元素数量不超过512个
  5. 有序集合
    编码可以是ziplist或者skiplist。在ziplist中,第一个节点保存元素的成员,第二个节点保存元素的分值。且分值从小到大排序
    编码转换:同时满足保存的元素数量小于128个所有元素成员的长度都小于64字节时才适用ziplist

  6. 内存回收
    redis通过引用计数器来实现内存回收机制,创建一个对象时,引用计数的值会被初始化为1,当对象被新程序使用时,其引用计数值会增加1,不再被引用时会减1,当对象的引用计数器为0,对象的内存就会被释放。

  7. 内存共享:引用计数器不仅有内存回收机制,其引用计数属性还能进行对象共享。
    Redis内存共享的步骤:1. 数据库键的值指针指向一个现有的对象。2.被共享的值对象的引用计数加1。
    redis在初始化服务器时会创建从0-9999的所有整数值的对象,服务器会共享这些对象。且服务器不能共享字符串对象。
    为什么不能共享包含字符串的对象?:字符串复杂,验证的时间复杂度变高,导致消耗过多的CPU时间。

  8. 时间空转:对象会记录自己最后一次被访问的时间,由此来计算空转时间,如果时间超过了上限,这部分键就会被释放回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值