深入学习Redis(一)基础数据结构

236 阅读6分钟

本章节主要分析redis的几种常用的底层数据结构

一、字符串

1.1 数据结构

    在redis内部,除去字符串字面量(无须进行修改的字符串)外,redis会使用一种SDS的数据结构来表示字符串,无论是key还是value。

                                   如上图所示, SDS包含三个属性值

  • free记录buf数组中未使用的字节的数量
  • len记录buf数组中已使用的字节的数量
  • buf是一个字节数组,用于保存字符串                                           


1.2 SDS和C语言字符串比较的优势

  • SDS获取字符串长度的时间复杂度为O(1),直接获取len,而C语言为O(N)
  • 使用SDS api可以杜绝缓冲区溢出
  • 可以减少修改字符串带来的内存重分配次数。通过空间预分配和惰性空间释放,将修改字符串需要重新分配内存的次数由N次减少为最多N次
  • SDS不仅可以保存文本数据,还可以保存任意格式的二进制数据
  • 兼容部分C字符串函数

二、hash字典

2.1 数据结构

上图是redis中整个字典的数据结构

  • 字典:字典包括type、privdata、ht、rehashidx等字段,其中rehashidx是和渐进式rehash相关的属性,记录了rehash的进度,ht是一个长度为2的数组,正常使用的是ht[0],在进行扩容缩容rehash时,会使用到ht[1]。ht的元素就是下面的哈希表
  • 哈希表:和java中hashmap类似,table是用来存放键值对的数组,size是hash表大小,sizemask是用来进行取模运算用的,used表示目前存在键值对的索引数量
  • 键值对:属性包括key, value,next


2.2 redis字典的特性

redis中的字典,整体上和java 中的hashmap类似,但是其中也存在一些特殊的地方

  • 字典的hash函数由type中的hashFunction实现,不同类型的字典hash计算方法不一样
  • 为了防止rehash阻塞主线程,redis采用了一种渐进式rehash的算法,后续独立讲解


三、list列表

3.1 数据结构


  • head指向链表头,tail指向链表尾
  • len保存链表长度,保证取链表长度的时间复杂度为O(1)
  • dup函数用于复制链表节点所保存的值;free函数用于释放链表节点所保存的值;match函数则用于对比链表节点所保存的值和另一个输入值是否相等。
  • listNode中value可以是是一个void*指针,可以保存任意类型的值


3.2 链表特性

  • 双向列表:删除,插入的时间复杂度为O(1),查找的时间复杂度为O(N)
  • 无环:头节点prev和尾节点的next都是null
  • 可快速定位到头和尾


四、intset

4.1 数据结构


intset是一种用于保存整数值的数据结构,可以保存16、32、64三种整数类型,并且保证元素唯一性

  • encoding表示当前整数的编码方式
  • length表示当前元素的个数
  • contents存储集合中的元素

4.2 intset特性

  • 当添加的元素类型比当前集合中元素类型更高时,需要先对整个集合元素进行升级,再添加元素
  • intset中的元素是按顺序进行存放的,所有可以进行二分查找,查找效率较高


五、压缩列表

5.1数据结构

压缩列表是为了节约内存而开发的,由一系列特殊编码的连续内存块组成


  • zlbytes记录整个列表占用的内存字节数
  • zltail记录压缩列表的尾节点的偏移量
  • zllen记录整个压缩列表中节点的个数
  • entryX用以保存列表元素
  • zlend特殊标志位,标志列表结束

每个entry由下图组成


  • preivious_entry_length表示前面的entry的字节长度,在进行逆向遍历时,可以根据这个字段计算出前面entry的起始地址
  • encoding记录了content属性保存的数据类型及长度
  • content存放的具体内容


六、跳跃列表

6.1 数据结构



跳跃表是在链表的基础上采用分层的结构,和正常的链表不一样的是,跳跃列表的next指针是一个数组,一个节点可以指向多个next,并且对应不同测层次。节点代码如下

typedef struct zskiplistNode {

    // 后退指针
    struct zskiplistNode *backward;

    // 分值
    double score;

    // 成员对象
    robj *obj;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;


6.2 跳跃列表分析

跳跃列表的最底层是一个双向链表,在插入新的节点时,会根据概率,构造更高层次的单向链表,层次越高,概率越小,下面是一种简单的计算跳跃列表层次的代码

randomLevel()
level := 1
// random()返回一个[0...1)的随机数
while random() < p and level < MaxLevel do
   level := level + 1
return level

可以看出一个节点加入第二层链表的概率是p(p<1),加入第三层链表的概率是p*p。

  • 跳跃列表的查询效率近似于二分查找,可以达到平均O(logN),最坏O(N)的时间复杂度
  • 相比于平衡树,跳跃列表构造更加简单明了,并且插入操作更方便


七、redis中五种基本数据对象的实现方式

7.1 字符串对象

字符串对象的编码可以是intrawembstr

  • 如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 使用int
  • 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节,使用 raw
  • 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节,使用embstr


7.2 列表对象

列表对象的编码可以是 ziplist(压缩列表) 或者 linkedlist(链表)

  • 列表对象保存的所有字符串元素的长度都小于 64 字节,且列表对象保存的元素数量小于 512 个,此时使用ziplist
  • 其他的时候使用linkedlist


7.3 哈希对象

哈希对象的编码可以是 ziplist 或者 hashtable

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节,且哈希对象保存的键值对数量小于 512 个,此时使用ziplist
  • 其余时候使用hashtable


7.4 集合对象

集合对象的编码可以是 intset 或者 hashtable

  • 集合对象保存的所有元素都是整数值,且集合对象保存的元素数量不超过 512 个,此时使用intset
  • 其余使用hashtable


7.5 有序集合

有序集合的编码可以是 ziplist 或者 skiplist

  • 有序集合保存的元素数量小于 128 个,且有序集合保存的所有元素成员的长度都小于 64 字节,此时使用ziplist
  • 其余使用skiplist