Redis---底层数据结构

《Redis设计与实现》读书笔记

   redis存在五种数据结构,单个的数据结构的底层实现,并不是固定的,而是由真正底层的结构去实现的,本节内容就是展示底层的数据结构实现:

  一.动态字符串

    概念:redis没有直接使用C语言传统的字符串表示,而是自己构建了一种简单动态字符串的抽象类型(SDS),并且将SDS用作redis的默认字符串。而C语言自带的字符串只被redis用来当常量使用(就是不需要修改字符串值的情况下使用,如果字符串的值需要修改,就使用SDS)。SDS甚至要用来实现缓冲区buffer,AOF模块中的,客户端状态中的输入缓冲区,都是SDS实现的 !

  底层实现:为什么redis要使用SDS,它有什么优点是C字符串无法达到的?

        1.SDS的定义:

Strcut sdshdr{
    int len;//记录buf已经使用的字节数量 等于字符串的长度
    int free;//记录buf 数组未使用字节的数量
    char buf[];  //数组,用来保存字符串。
}

        原来我们以为buf很高档,只不过增加了两个属性。同样也使用'\0'来结尾,并且不计入字符串长度中。(string是c++提出的api)

        2.长度值获取优势:传统字符串长度获取需要遍历(记得我们使用方法strlen()来获得)

        3.防止溢出:调用strca()进行拼接的时候,不知道分配空间的大小,容易溢出。

        4.减少修改字符串时带来的内存重分配次数

        5.二进制安全:如果字符串中间存在'\0'那么就会被认为结束了。但是sds有len属性,它是通过这个属性判断字符串是否结束的。

        6.注意扩容机制:当长度<1MB时,扩容会加倍现有空间,而超过1MB的时候,只会多扩容1MB空间,最大长度是512MB。

 

.链表 Linkedlist

    概念:链表提供了高效的节点重排能力,以及顺序性的节点访问方式。列表键底层实现之一就是链表。由于c语言不带list,所以redis构建了自己的链表实现。

    底层实现

//节点
typedef struct listNode{
    struct listNode *prev;
    struct listNode *next;
    void *value;
}listNode
// 链表
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;
        list结构为链表提供了表头指针,表尾指针。 list整体结构:
总结:两个指针,一个Len,三个函数:复制,释放,节点值对比。
 

 三. hashtable

    概念:别名符号表(symbol table) ,Redis的数据库就是使用字典来作为底层实现的(比如我们的String类型,是Key-value类型的,其实他的存储结构就是字典)。另外哈希键也是通过字典实现的。

    底层实现

1.哈希表,dict hash table

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

    下面三个属性很好理解,重点是第一个,table是一个数组,数组的元素是指针(所以是指针的指针:指向的是存地址的地址),指针指向dictEntry结构的指针。dictEntry结构是什么?

typedef struct dictEntry{
    void *key; //键
    union{
        void *val;
        uint64-tu64;
        int64_ts64}v;
    struce dictEntry *next;
}dictEntry;

    key属性保存键值对中的键,而v保存值(可以是三个里面的一种类型),也就意味着:dictEntry是一个键值对。next指向下一个dictEntry。(相当于链表,接近哈希冲突的)而table是一个数组,那么这里就找到了哈希表的原型了,数组+链表节点

    【注意】 01,2,3他们也是dictEntry,这里要注意,那么里面同样是k1-v1结构,所以使用k来进行hash分配。(而后面的数据库db里面的存放同样使用字典来存放,使用箭头指出去的是value,这里就搞混淆了,我们的节点一共三个指针,所以都可以使用箭头画出来,只是图片展示的维度不同,所以不要以为value是存放在外面,然后entry又不管了之类的,它只是展示的角度不一样)

        2.字典 dict

typedef struct dict{
    dictType *type;// 类型指定函数  不同的类型,使用不同的哈希函数。
    void *privdata;// 私有数据
    ditcht ht[2];// 哈希表--在这里。
    int trehashidx;//rehash 索引
}dict;

    ht包含两个哈希表,字典只使用ht[0],ht[1]是用来对h[0]进行rehash的时候使用。rehashidx记录了rehash的进度。

    哈希冲突:由于没有指向链表尾部的指针,所以发送哈希冲突是把新的节点,放在头部。提高效率。

    3.rehash重新散列:随着操作的执行,节点的数量会变多或者变少,程序相应要对哈希表的大小进行扩展或者收缩,就需要通过rehash来进行。执行步骤:

            1.为字典ht[1]分配空间,大小由ht[0]包含的键值对数量,来决定。

            2.将ht[0]的节点,重新计算打入ht[1]中,

            3.转移完毕,释放ht[0],将ht[1]变成ht[0]。

     什么时候进行收缩or扩展?

            1.服务器没有执行bgsave,或者bgrewriteaof命令,哈希表的负载因子>=1。

            2.正在执行上面的保存动作,并且哈希表的负载因子大于等于5

            3.当负载因子<0.1,自动进行收缩。

    负载因子计算公式:已保存节点/哈希表大小。

     4.渐进式rehash: 当hash字典很大的时候,rehash是一个耗时的操作,所以不能等待全部一次性完成,这里推出了渐进式rehash策略:从ht[0]复制到ht[1]的时候,是分多次进行的,详细步骤:

            1.为ht[1]分配空间。

            2.将rehashidx设置为0(表示开始rehash,普通时候是-1)。

            3.rehash进行期间,每次对字典进行的操作,都会顺带把rehashindx指向的索引位置转移到ht[1]中,然后rehashindex+1.最后恢复为-1.(查询的时候,会同时查询两个hash表),没有操作的时候,就是定时任务进行转移。添加操作的话,就直接添加到ht[1]中,ht[0]就不会新增加了。

 

四.跳跃表 skiplist

    概念:是一种有序的数据结构,通过每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在大部分情况下,跳跃表的效率可以和平衡树进行媲美,而且跳跃表的实现更加简单。有序集合底层实现就有跳跃表。redis就两个地方用到了跳跃表,第一个就是刚刚说的set,第二个是集群节点中用作内部数据结构。

    底层实现:有Node和list两个结构体共同实现的,node代表List中的节点。list用来保存节点信息,比如头节点,尾节点,节点个数。

如图中所示:左边一个是list优点是Node.

header:指向跳跃表的表头节点。tail:指向表尾节点。level:记录跳跃表中,层数最大的(除去表头节点层数)

length:跳跃表的长度(不算表头节点的节点数)

Node:bw就是后退指针。第二个分值,节点按照各自的分值进行排序。(从小到大)

        level:各层有两个属性:前进指针和跨度(图上线上的值,代表距离)

说明:表头节点构造是一样的,只不过下面三个值不会使用,所以省略了。

typedef struct zskiplistNode{
    //层
  struct zskiplistLevel{
        struct zskiplistNode * forward; //前进指针
        unsigned int span;//跨度
    }level[];
    struct zskiplistNode *backward;//后退指针
    double score;//分值
    robj *obj;//成员对象
}zskiplistNode;
  • 层:level数组包含多个元素,每个元素包含一个指向其他节点的指针。层的数量越多,访问其他节点速度越快。每次创建一个新的跳跃表的时候,程序根据幂次定律随机生成一个介于1和32之间的数作为level的大小。也就是高度。
  • 前进指针:每个节点都有一个指向表尾放向的指针。最基本的就是一个一个指向连接的。
  • 跨度:跨国了几个节点,值就是几。
  • 后退指针:只能一个一个后退。
  • 分值和成员:对象是是一个指针,指向一个字符串对象,字符串对象保存着一个SDS值。在同一个跳跃表中,各个节点保存的成员对象必须唯一。多个节点的分值可以相同。
typedef struct zskiplist{
    struct skiplistNode *header ,*tail;
    unsigned long length;//表中节点的数量
    int level;//表中层最大的节点的层数
}zskiplist;
   header和tail指针分别指向跳跃表的表头和表尾节点。

    

五.整数集合 intset

    概念:集合键的底层实现之一,

    底层实现:intset是redis用于保存整数值的集合抽象数据结构,它可以保存16,32,64整数值,并且保证集合中不会出现重复元素。

typedef struct intset{
    uint32_t encoding;//编码方式
    uint32_t length; //包含元素的个数。
    int8_t contents[];//保存元素的数组
}intset;
contents就是Intset的底层实现,里面的数据按从小到大排列,并且不包含重复项。它保存的类型不是int8,而是取决于encoding属性的值。

升级:当一个新的元素添加到这个集合中,这个新元素的类型长度比已存在的要长,那么整数集合就需要升级,

升级步骤

    1.根据新元素的类型,扩展整数集合的底层数组大小,并未新元素分配空间。

    2.将底层所以元素都转换成新元素的类型,然后转移到正确的位置上(注意有序)(因为是扩展,所以在移动数据的时候,肯定是从后面的数据开始移动,而且移动还是往后面移动)

    3.将新元素添加到数组里面。

升级策略的好处:1.提升整数集合的灵活性:不用担心存入的数据类型出错,我们存入什么,集合都会自动适应

                             2.尽可能的节省空间:有64位数据的时候才升级,就不用过早准备空间,用64位存放16位的,造成浪费。

降级:不支持降级。

 

六.压缩列表 zipList

    概念:是列表键和哈希键的底层是是实现之一,当一个列表键只有少量的数据,就会使用ziplist来存放,
    底层实现:包含任意多个节点(entry),每个节点存放一个字节数组或者整数值。
结构还是较为简单:
  • zibytes:表示整个压缩列表占用的内存字节数。
  • zltail:表示压缩列表的尾节点举例压缩列表起始地址有多少个字节。(可以快速找到尾节点,不用遍历)
  • zllen:记录了压缩列表节点数量。2字节,数量大于了两字节,就需要遍历才能得出节点数量。
  • zlend:1字节,用来标记压缩列表的末端。
 

节点构成:

我们知道它可以存放字节数组or整数(多个范围的)

第一个属性我们通过名字就可以知道是干什么的,记录前一个节点的长度(那么你就要知道,每一个节点的长度是不固定的!),作用:可以从表尾向前遍历,那么你就知道了(这个不存在索引直接定位!)

encoding:记录保存数据的类型以及长度。也就是上面说的字节组or整数。(字节组可以表示字符串)

    问题来了:连锁更新

    在表头插入一个大于普通节点长度的节点,因为第一属性大小是动态固定的,比如原表头第一属性是1字节,现在插入的长度>255字节,所以一个字节装不下,怎么办?

    (这里又产生思考,你这个不是连续的吗,怎么插入数据啊?扩充空间移动?)

    原表头节点的第一属性装不下,那么就需要扩充空间,如果这个时候,它的长度刚好是253.扩充了两个字节,完了,第二节点的第一属性也装不下了,也要扩充,。。。那么最坏的情况就是所以的节点都要扩充(。。连锁反应,名不虚传)

    不只是添加节点会产生连锁反应,删除的时候也会,比如第三个节点存放第二个节点是用了1字节,第二个节点用了四个字节来存放第一字节,现在把第二字节剔除了,那么第三字节理所当然的升级成为第二字节,记录第一字节,这个时候就需要扩充了。。也有可能产生连锁。

    解决办法:还是重新分配,n次扩展就需要n次空间重分配。发生概率小。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值