1.Redis的根底类型dictEntry和redisObject

2.程序员应用redis时的底层思维

3.String底层数据结构

4.Hash数据结构介绍

5.List数据结构介绍

6.Set数据结构介绍

7.ZSet数据结构介绍

8.总结

1.Redis的根底类型dictEntry和redisObject

咱们能够先去redis的github上下载源码:https://github.com/redis/redis

就像咱们的JAVA对象,顶层全是Object一样,咱们的redis的顶层都是dictEntry,让咱们来看这样一段源码(dict.h中):

typedef struct dictEntry {    void *key;  //示意字符串 就是redis KV构造中的KEY    union {        void *val;   //val指向的是redisObject中        uint64_t u64;        int64_t s64;        double d;    } v;    struct dictEntry *next;     /* Next entry in the same hash bucket. */    void *metadata[];           /* An arbitrary number of bytes (starting at a                                 * pointer-aligned address) of size as returned                                 * by dictType's dictEntryMetadataBytes(). */} dictEntry;

咱们以最简略的set k1 v1 为例,因为Redis是以KV为构造的数据库,每个键值对都会有一个dictEntry, 这外面指向了key和value的指针,next指向下一个dictEntry。
key是字符串,然而Redis没有间接应用C的char数组,而是存在了redis的自定义字符串中(等等上面会解释),value因为会有不同的类型,redis将这几种根本的类型形象成了redisObject中,实际上五种罕用的数据类型,都是通过redisObject来存储的。
咱们看看redisObject的源码(server.h):

typedef struct redisObject {    unsigned type:4;       //以后对象的类型    unsigned encoding:4;   //以后对象的底层编码类型    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or  LRU或者LFU的拜访工夫数据                            * LFU data (least significant 8 bits frequency                            * and most significant 16 bits access time). */    int refcount;           //对象援用计数的次数    void *ptr;              //真正指向底层数据结构的指针} robj;

接下来咱们便能够取得这张图片,咱们对redis的存储构造就高深莫测了:

为了便于操作,Redis采纳redisObject构造来形象了不同的数据类型,这样所有的数据类型就能够用雷同的模式在函数之间传递,而不是应用特定的类型构造。同时,为了辨认不同的数据类型,
redisObject中定义了typeencoding字段对不同的数据类型加以辨别。简略地说,redisObject就是String,hash,list,set,zset的父类,能够在函数间传递的时候暗藏具体的根本类型信息,所以作者形象了redisObject。

2.程序员应用redis时的底层思维
咱们刚开始学习redis的时候,只会调用调用顶层的api,所以咱们看到的redis是这个样子的:

然而咱们学习了redis的的底层数据结构后,咱们将会看到这样子的redis:

3.String底层数据结构
Redis的String类型,其实底层是由三种数据结构组成的
1)int: 整数且小于二十位整数以下的数字数据才会应用这个类型

2)embstr (embedded string,示意嵌入式的String):代表embstr格局的SDS(Simple Dynamic String 简略动静字符串),保留长度小于44字节的字符串。
3)raw :保留长度大于44的字符串

咱们先对下面这个案例做一个测试:

咱们发现保留的数据内容,会随着保留内容的变动而发生变化。

这就是redis中,String类型没有间接复用C语言的字符串,而是新建了属于本人的构造————SDS(简略动静字符串)。在Redis数据库里,蕴含字符串的键值对都是由SDS实现的,Redis中所有的值对象蕴含的字符串对象底层也是由SDS实现

咱们点开sds.h,发现sds由多种类型形成:

struct __attribute__ ((__packed__)) sdshdr5 { //被废除    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */    char buf[];};struct __attribute__ ((__packed__)) sdshdr8 {    uint8_t len; /* used */                                       //已用长度    uint8_t alloc; /* excluding the header and null terminator */ //字符串最大字节长度    unsigned char flags; /* 3 lsb of type, 5 unused bits */       //用来展现的sds类型    char buf[];                                                   //真正无效的字符串数据,长度由alloc管制};struct __attribute__ ((__packed__)) sdshdr16 {    uint16_t len; /* used */    uint16_t alloc; /* excluding the header and null terminator */    unsigned char flags; /* 3 lsb of type, 5 unused bits */    char buf[];};struct __attribute__ ((__packed__)) sdshdr32 {    uint32_t len; /* used */    uint32_t alloc; /* excluding the header and null terminator */    unsigned char flags; /* 3 lsb of type, 5 unused bits */    char buf[];};struct __attribute__ ((__packed__)) sdshdr64 {    uint64_t len; /* used */    uint64_t alloc; /* excluding the header and null terminator */    unsigned char flags; /* 3 lsb of type, 5 unused bits */    char buf[];};

它用
sdshdr5、(2^5=32byte)
sdshdr8、(2 ^ 8=256byte)
sdshdr16、(2 ^ 16=65536byte=64KB)
sdshdr32、 (2 ^ 32byte=4GB)
sdshdr64,2的64次方byte=17179869184G

来存储不同长度的字符串,len示意长度,这样获取字符串长度就能够在O(1)的状况下,拿到字符串,而不是像C语言一样去遍历。
alloc能够计算字符串未被调配的空间,有了这个值就能够引入预调配空间的算法了,而不必去思考内存调配的问题。
buf 示意字符串数组,真存数据的。

为什么Redis要从新设计一个字符串SDS,而不间接应用char[]数组呢?

咱们能够从redis和sds的比照中能够发现:

C语言SDS
字符串长度解决要从数组的头部开始遍历,直到遇到'\0',工夫复杂度O(n)间接读取长度,工夫复杂度O(1)
内存重新分配内存超出调配的空间后,会导致下标越界或者溢出预调配: SDS调配后,如果长度小于1M,那么会额定调配一个与len雷同长度的未应用的空间。 惰性开释:SDS短时并不会发出多余的空间,而是将多余的空间记录下来,如果有变更操作,间接应用多余的空间,缩小调配频率。
二进制平安数据内容可能蕴含'\0',C语言遇上'\0'会提前结束,不会读取前面的内容有了len作为长度,就不会有遍历这种问题了

论断:
只有整数才会应用int,如果是浮点数,就是用字符串保留。
embstr 与 raw 类型底层的数据结构其实都是 SDS (简略动静字符串,Redis 外部定义 sdshdr5, sdshdr8等等)。

存储构造:
1)int
当数据类型为整数的时候,RedisObject中的prt指针间接赋值为整数数据,不会额定指向整数了,节俭内存开销。redis在一万以内只会存一份,就像JAVA的Integer -128~127只存一份。

2) embstr
当保留的字符串数据小于等于44字节的时候,embstr类型将会调用内存调配函数,只调配一块间断的空间,空间中一次蕴含redisObject和sdshdr两个构造,让元数据,指针和sds是一块间断的区域,防止内存碎片。

3) raw
字符串大于44字节时,SDS的数据质变多变大了,SDS和RedisObject布局会离开,会给SDS调配多的空间并用指针指向SDS构造,raw 类型将会调用两次内存调配函数,调配两块内存空间,一块用于蕴含 redisObject构造,而另一块用于蕴含 sdshdr 构造。

调配流程图:

4.Hash数据结构介绍
在查看Hash的数据结构之前,咱们先来看这样的一个配置:

咱们可能看不太懂,我来给解释一下:

hash-max-ziplist-entries:应用压缩列表保留哈希汇合中的最大元素个数。

hash-max-ziplist-value:应用压缩列表保留时哈希汇合中单个元素的最大长度。

可能咱们这么说还是不太懂,上案例:

Hash数据类型也和String有相似之处,达到了肯定的阈值之后就会对数据结构进行降级。

数据结构:
1)hashtable 就是和java当中应用的hashtable一样,是一个数组+链表的构造

2)ziplist 压缩链表

咱们先来看一下 压缩链表的源码:


ziplist是一种比拟紧凑的编码格局,设计思路是用工夫换取空间,因而ziplist实用于字段个数少,且字段值也较小的场景。压缩列表内存利用率高的起因与其连续性内存个性是分不开的。

当一个hash对象,只蕴含大量的键,且每个键值对的值都是小数据,那么ziplist就适宜做为底层实现。

ziplist的构造:

它是一个通过非凡编码的双向链表,它尽管是双向链表,但它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点的长度和以后节点的长度,通过就义局部读写性能,来换取高空间利用率。

zlbytes 4字节,记录整个压缩列表占用的内存字节数。
zltail 4字节,记录压缩列表表尾节点的地位。
zllen 2字节,记录压缩列表节点个数。
zlentry 列表节点,长度不定,由内容决定。
zlend 1字节,0xFF 标记压缩的完结。

节点的构造源码:

typedef struct zlentry {    unsigned int prevrawlensize; //上一个节点的长度     unsigned int prevrawlen;     //存储上一个链表节点长度所须要的的字节数                      unsigned int lensize;        //以后节点所须要的的字节数                                                                                                                                  unsigned int len;            //以后节点占用的长度                                                                                                                                                                unsigned int headersize;     //以后节点的头大小       unsigned char encoding;      //编码方式                                                                      unsigned char *p;            //指向以后节点起始地位                                                      

因为保留了这个构造,能够让ziplist从后往前遍历。

为什么有链表了,redis还要整出一个压缩链表?
1)一般的双向链表会有两个前后指针,在存储数据很小的状况下,咱们存储的理论数据大小可能还没有指针占用的内存大。而ziplist是一个非凡的双向链表,并没有保护前后指针这两个字段,而是存储上一个entry的长度和以后entry的长度,通过长度推算下一个元素在什么中央。就义读取的性能,获取高效的空间利用率,因为(简短KV键值对)存储指针比存储entry长度更费内存,这是典型的工夫换空间。

2)链表是不间断的,遍历比较慢,而ziplist却能够解决这个问题,ziplist将一些必要的偏移量信息都记录在了每一个节点里,使之能跳到上一个节点或者尾节点节点。

3)头节点里有头结点同时还有一个参数len,和SDS类型相似,这就是用来记录链表长度的。因而获取链表长度时不再遍历整个链表,间接拿到len值就能够了,获取长度的工夫复杂度是O(1)。

遍历过程:
通过指向表尾节点的地位指针zltail,减去节点的previous_entry_length,失去前一个节点的起始地址的指针。如此循环,从表尾节点遍历到表头节点

5.List数据结构介绍

咱们先来看一下list的默认配置:

ziplist压缩配置 :list-compress-depth 0
示意一个quicklist两端不被压缩的节点个数,这里的quicklist(下文会解释)是指quickList双向链表的节点,而不是指ziplist外面的数据项个数。

参数取值含意:
0:是个非凡值,示意都不压缩,这是redis的默认值。
1: 示意quicklist两端各有1个节点不压缩,两头的节点压缩。
2: 示意quicklist两端各有2个节点不压缩,两头的节点压缩。
3: 示意quicklist两端各有3个节点不压缩,两头的节点压缩。
依此类推…

(2) ziplist中entry配置:list-max-ziplist-size -2

当取正值的时候,示意依照数据项个数来限定每个quicklist节点上的ziplist长度。比方当这个配置为5的时候,ziplist里最多有5个数据项。

当取负值的时候,示意依照占用字节数来限定quicklist节点上的ziplist长度。这时,它只能取-1~-5这几个值。

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

quicklist构造介绍:
list用quicklist来存储,quicklist存储了一个双向链表,每一个节点都是一个ziplist。

](/img/bVcX7sS)

源码:

typedef struct quicklist {    quicklistNode *head;        //指向双向列表的表头    quicklistNode *tail;        //指向双向链表的表尾    unsigned long count;        //所有ziplist中共存储了多少个元素 /* total count of all entries in all listpacks */    unsigned long len;          //双向链表的长度,node的数量 /* number of quicklistNodes */    signed int fill : QL_FILL_BITS;       /* fill factor for individual nodes */    unsigned int compress : QL_COMP_BITS;  //压缩深度 /* depth of end nodes not to compress;0=off */    unsigned int bookmark_count: QL_BM_BITS;    quicklistBookmark bookmarks[];} quicklist;
typedef struct quicklistNode {    struct quicklistNode *prev; //前指针    struct quicklistNode *next; //后指针    unsigned char *entry; 指向理论的ziplist    size_t sz;             /* entry size in bytes */    unsigned int count : 16;     /* count of items in listpack */    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */    unsigned int container : 2;  /* PLAIN==1 or PACKED==2 */    unsigned int recompress : 1; /* was this node previous compressed? */    unsigned int attempted_compress : 1; /* node can't compress; too small */    unsigned int extra : 10; /* more bits to steal for future usage */} quicklistNode;

6.Set数据结构介绍

咱们来看一下set的配置:

set-max-intset-entries
set数据类型汇合中,如果没有超出了这个数量,且数据元素都是整数的话,类型为intset,否则为hashtable

intset类型:

typedef struct intset {    uint32_t encoding;    uint32_t length;    int8_t contents[];} intset;

看到源码后,咱们能够得出,论断,intset的数据结构实质是一个数组。而且**存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的。
**

咱们略微剖析一下set的单个元素的增加流程。
如果set曾经是hashtable的编码,那么走hashtable的增加流程。
如果原来是intset:
1)能转化为int对象,就用intset保留。
2)如果长度超过设置,就用hashtable保留
3)其它状况对立用hashtable保留。

7.ZSet数据结构介绍

咱们看一下ZSet的配置:


当有序汇合中蕴含的元素数量超过服务器属性 zset_max_ziplist_entries 的值(默认值为 128 ),
或者有序汇合中新增加元素的 member 的长度大于服务器属性zset_max_ziplist_value 的值(默认值为 64 )时,redis会应用跳跃表(下文会解释)作为有序汇合的底层实现

否则会应用ziplist作为有序汇合的底层实现

看一下源码:


当元素个数大于设置的个数或者元素的列表原本就是skiplist编码的时候,用skiplist存储,否则就用ziplist存储。

skiplist(跳表)是什么:
跳表是一种以空间换取工夫的构造。

因为链表是无奈进行二分查找的,因而借鉴了数据库索引的思维,提取出链表中要害节点(索引),当初要害节点上进行查找,再进入链表进行查找。

提取了很多要害节点,就造成了跳表。

因为跳表是以一种跳跃式的数据存在,当咱们查问‘14’这个数据的时候,能够跳过很多无用的数据,缩小遍历的次数。

跳表是一个典型的空间换工夫的解决方案,而且只有在数据量较大的状况下能力体现进去劣势。还是要读多写少的状况才适宜应用,所以它的适用范围还是比拟无限的,新增或者删除的时候要把所有数据都更新一遍。

8.总结