Redis基本数据结构和原理

简介:

Redis是一个key-value存储系统,在各种系统中的使用率越来越高,大部分原因是因为其高性能的特性。Redis拥有非常多的优势,读写性能高–100000次/s以上的读速度,80000次/s以上的写速度;Redis的所有操作都是单线程原子性的,支持丰富的特性,如支持订阅-发布模式,通知,设置key过期特性。

String(SDS)

上边设置key=msg,value=hello world的键值对,它们的底层存储是:键(key)是字符串类型,其底层实现是一个保存着“msg”的SDS。值(value)是字符串类型,其底层实现是一个保存着“hello world”的SDS。

SDS的结构

/*  
 * 保存字符串对象的结构  
 */  
struct sdshdr {  
      
    // buf 中已占用空间的长度  
    int len;  
  
    // buf 中剩余可用空间的长度  
    int free;  
  
    // 数据空间  
    char buf[];  
};

使用SDS的好处:
1.防止在改变某个字符串时缓冲区溢出。
2.减少扩展或收缩字符串带来的内存重分配次数
空间预分配,惰性空间分配

链表(list的实现方式之一)

链表是list的实现方式之一。当list包含了数量较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis会使用链表作为实现List的底层实现。此链表是双向链表:

typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;  
}

list结构

typedef struct list{
    //表头节点
    listNode  * head;
    //表尾节点
    listNode  * tail;
    //链表长度
    unsigned long len;
    //节点值复制函数
    void *(*dup) (void *ptr);
    //节点值释放函数
    void (*free) (void *ptr);
    //节点值对比函数
    int (*match)(void *ptr, void *key);
}

下面是list的数据结构图

《Redis基本数据结构和原理》 image.png

字典

在字典中,一个键(key)可以和一个值(value)进行关联,字典中的每个键都是独一无二的。
Redis本身的K-V存储就是利用字典这种数据结构的,value类型的哈希表也是通过这个实现的。
下面是哈希表的定义:

typedef struct dictht {
   //哈希表数组
   dictEntry **table;
   //哈希表大小
   unsigned long size;
 
   //哈希表大小掩码,用于计算索引值
   unsigned long sizemask;
   //该哈希表已有节点的数量
   unsigned long used;
}

下面是dictEntry的数据结构

typeof struct dictEntry{
   //键
   void *key;
   //值
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}

我们会发现这样会存在hash冲突,解决的办法是采用链地址法来解决hash冲突。
Redis在dictht的基础上,又抽象了一层字典dict.

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privedata;
    // 哈希表
    dictht  ht[2];
    // rehash 索引
    in trehashidx;
}

扩充Rehash:随着对哈希表的不断操作,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成。其实现方式和hashmap略有不同,因为dict有两个hash表dictht,所以它是通过这两个dictht互相进行转移的。

渐进式Rehash:在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。详细步骤如下:
1.为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2.维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash的开始。
3.在rehash进行期间,每次对字典执行CURD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash到ht[1]表中,并且将rehashidx加一
4.当ht[0]中所有数据转移到ht[1]中时,将rehashidx设置成-1,表示rehash结束。

跳跃表

Redis 只在两个地方用到了跳跃表,一个是实现有序集合键(sorted Sets),另外一个是在集群节点中用作内部数据结构。
其实跳表主要是来替代平衡二叉树的,比起平衡树来说,跳表的实现要简单直观的多。跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速查找访问节点的目的。跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美 ——查找、删除、添加等操作都可以在O(logn)期望时间下完成。
Redis 的跳跃表 主要由两部分组成:zskiplist(链表)和zskiplistNode (节点)

typedef struct zskiplistNode{  
   //层  
     struct zskiplistLevel{  
     //前进指针  
        struct zskiplistNode *forward;  
    //跨度  
        unsigned int span;  
    } level[];  
  //后退指针  
    struct zskiplistNode *backward;  
  //分值  
    double score;  
  //成员对象  
    robj *obj;  
}

typedef struct zskiplist {  
     //表头节点和表尾节点  
     structz skiplistNode *header,*tail;  
     //表中节点数量  
     unsigned long length;  
     //表中层数最大的节点的层数  
     int level;  
  
}zskiplist;

1.层:level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。level数组的每个元素都包含:前进指针:用于指向表尾方向的前进指针,跨度:用于记录两个节点之间的距离.
2.后退指针:用于从表尾向表头方向访问节点
3.分值和成员:跳跃表中的所有节点都按分值从小到大排序(按照这个进行排序的,也就是平衡二叉树(搜索树的)的节点大小)。成员对象指向一个字符串,这个字符串对象保存着一个SDS值(实际存储的值)

此文是对《redis设计与实现》总结

    原文作者:shoulda
    原文地址: https://www.jianshu.com/p/b2ffbff4b47e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞