Redis理解之数据结构

       我们知道redis里面的每个键值对都是有对象组成的。其中,键总是字符串对象,而值可以是字符串对象、列表对象、哈希对象、集合对象和有序对象 这五个对象的任意一种。下面将会分别介绍以上涉及到的五种对象的底层数据结构。

     1)简单动态字符串(SDS)

      区别于C语言的字符串,redis构建了简单动态字符串SDS作为默认的字符串表示,来看一下SDS的定义:


    上左图就是SDS的结构定义,右图中举例的

     free为0 ,代表这个SDS没有分配任何未使用的空间; 

     len为5,表示这个SDS保存了一个五字节字长的字符串;

     buf是一个char类型的数组,最后保存空字符 '\0' 字节;

     SDS与C字符串的区别:

     ①获取字符串长度的时间复杂度

     C字符串不记录自身长度信息,获取字符串长度的时间复杂度是O(N),SDS直接访问len属性即可获取长度信息,复杂度为0(1)。

     ②杜绝缓冲区溢出

     C字符串容易造成缓冲区溢出,具体可以看下面这个例子(例子来自《redis设计与实现》 ):



  

         SDS完全杜绝了出现缓冲区溢出的可能性:当API需要最SDS做修改时,API会先对SDS的空间做检查,如果不满足的话,SDS会自动对其做扩展,然后才会执行相应的修改,举个例子:

     

     ③减少修改字符串是带来的内存重分配次数

     对于C字符串来说,每次修改都意味着一次内存重分配操作;SDS通过未使用空间实现了空间预分配和惰性空间释放两种优化策略。

   空间预分配:当SDS的API对其做修改时,并且需要扩展空间时,程序不仅会分配所需要的空间,还会分配额外的未使用空间。


     惰性空间释放:优化了SDS的字符串缩短操作,当SDS的API对其做缩短操作时,程序不会立即释放掉多余的空间,而是使用free属性,将这些缩短的空间大小记录下来,并等待进来使用,举个例子:

   

      ④二进制安全

  C字符串除了结尾不能出现空字符,所以导致C字符串不能保存图片、视频、音乐等二进制数据,SDS程序不会对任何数据做过滤,可以用来保存图片、视频等数据信息。

   ⑤兼容部分C字符串函数

 因为SDS保留了C的风格,末尾一空字符结尾,就是为了兼容部分的C函数。

 总结,如下图:



    2)链表

  每一个链表节点使用listNode节点来表示,如下图



 链表的结构如下图

 



   每一个链表节点由一个listNode来表示,每一个节点都有一个指向前置和后置的指针,所以redis链表实现的是双端链表;

   每一个链表都使用一个list结构来表示,这个结构带有链表头、表尾和长度等信息;

   链表表头节点前置和表尾节点的后置节点都指向NULL,所以是无环的;

   通过链表设置不同类型的特定函数,所以redis链表可以用来保存不同类型的信息;


     3)字典

    redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每一个哈希表节点就保存一个键值对,接下来分别看一下哈希表、哈希表节点以及字典的实现。

   哈希表的结构如下:

  

  

table是一个数组,每个元素指向一个哈希表节点,下图表示一个大小为4的空哈希表(没有任何键值对)


哈希表节点:每一个哈希表节点保存着一个键值对,如下图:


  字典的数据结构如下:


ht保护两个项哈希表的数组,一般情况下字典只适用h[0],h[1]哈希表只会在对h[0]进行rehash时使用;


另外要提一下哈希算法,当要有一个新的键值对添加到字典的时候,先会计算出哈希值和索引值,然后根据索引值将键值对放到哈希表数组指定索引上面。举个例子,如果要将一个新的键值对KO和VO,插到字典里面,如下图所示:


那么这里就会出现一个问题,键冲突,redis是如何解决的呢?redis的哈希表是通过链地址法来解决冲突的。如下图,被分配到同一个索引上的多个节点用单向链表链接起来:


    对与哈希表的扩展或者收缩,redis是采取rehash(重新散列)来完成的,具体的步骤如下:


 为了 避免rehash对性能造成影响,redis是采用渐进式rehash进行操作的,具体步骤如下解释:



     4)跳跃表

     跳跃表是由zskiplistNode和zskiplist定义的,zskiplistNode结构用来表示跳跃表节点,zskiplist用来表示保存跳跃表节点的相关信息,zskiplistNode的定义如下:



zskiplist的结构如下:



下图表示带有zskiplist结构的跳跃表

 

每一个跳跃表节点的层高都是1-32之间的随机数。

同一个跳跃表中,多个节点可以包含相同的分值,但每个的节点成员对象必须是唯一的。

跳跃表的节点按照分值大小来进行排序,当分值相同时,节点按照成员对象的大小进行排序。


    5)整数集合

  整数集合是redis用于保存整数值的集合抽象数据结构,并保证集合是有序的并且不会出现重复元素。结构定义如下:



其中编码方式有3种,int16_t、int32_t、int64_t。同样的,举个例子:


       下面介绍一下整数集合的升级操作,如果新加入的新元素的类型比现有集合的元素类型都要长时,整数集合需要升级操作,才能将新元素添加到整数集合里面。具体升级步骤如下:

      

      另外需要说明的一点是,整数集合不支持降级。一旦对数组进行升级,编码就会一直保持升级后的状态。


  6)压缩列表

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包括多个节点,每个节点可以保持一个字节数组或者一个整数值,具体组成和说明如下:

 

压缩列表节点的构成如下:


  • prev_entry_len:记录前驱节点的长度。
  • encoding:记录当前节点的value成员的数据类型以及长度。
  • value:根据encoding来保存字节数组或整数

   另外介绍要提高一种情况就是连锁更新:

    连锁更新的两种情况:

  • 如果前驱节点的长度小于254,那么prev_entry_len成员需要用1字节长度来保存这个长度值。
  • 如果前驱节点的长度大于等于254,那么prev_entry_len成员需要用5字节长度来保存这个长度值。

如果一个压缩列表中,有多个连续、长度介于250字节到253字节之间的节点,因此记录这些节点只需要1个字节的prev_entry_len,如果要插入一个长度大于等于254的新节点到压缩列表的头部,然而原来的节点的prev_entry_len成员长度仅仅为1个字节,无法保存新节点的长度,因此会对新节点之后的所有prev_entry_len成员大小为1字节的节点产生连锁更新。同样的,如果一个压缩列表中,是多个连续的长度大于等于254的节点,当往压缩列表的头部插入一个长度小于254的节点,也会产生连锁更新。另外删除节点也会产生连锁更新。

 在redis中,只处理第一种情况,不处理因为节点的变小而引发的连锁更新,防止出现反复的缩小-扩展。

         

   以上主要介绍了后面会用到的几种数据结构,了解了这个,对下篇谈到的redis的五种对象的理解就会更容易一点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值