Redis做为一个开源的用C编写的非关系型数据库,基于优秀的CRUD效率,经常使用于软件系统的缓存,其自己提供了如下五种数据格式:html
接下来咱们就要针对这五种数据结构,来分析其底层的结构java
这里选用的版本是redis-5.0.4
,因此可能有不少地方和现在网络上的其余博文不太一致,不一样的地方我会在文中指出c++
由于redis使用c语言开发,因此天然没有java和c++的那些字符串类库,在redis中,其本身定义了一种字符串格式,叫作SDS(Simple Dynamic String),即简单动态字符串redis
这个结构定义在sds.h中:数据库
typedef char *sds;
复制代码
可是这个sds类型仅做为参数和返回值使用,并非真正用于操做的类型,真正核心的部分是下面的这些类:数组
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
复制代码
除掉第一个结构体(已经弃用),sds具体类型的结构能够分为如下部分:缓存
这里和老版本作一下对比,由于我手头只有4.x和5.x的版本,它们sds的实现是一致的,可是据其余人说sds以前的版本实现方式不一样,有时间我会去下载下来看一下,其将字符串分为如下部分:安全
redis同时写重写了大量的与sds类型相关的方法,那redis为何要这么下功夫呢,有如下4个优势:网络
咱们查看源文件能够看到有两个list,一个是ziplist,字面意是压缩列表,另外一个是quicklist,字面意是快速列表,在redis中直接使用的是quicklist,可是咱们先来看ziplist数据结构
ziplist并非一个类名,其结构是下面这样的: <zlbytes><zltail><entries><entry>...<entry><zlend>
其中各部分表明的含义以下:
这些数据均为小端存储,因此可能有些人查看数据的二进制流与其含义对应不上,实际上是由于读数据的方式错了
ziplist内部采起数据压缩的方式进行存储,压缩方式就不是重点了,咱们仅从宏观来看,ziplist相似一个封装的数组,经过zltail能够方便地进行追加和删除尾部数据、使用entries能够方便地计算长度
可是其依然有数组的缺点,就是当插入和删除数据时会频繁地引发数据移动,因此就引出了quicklist数据类型
其核心数据结构以下:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* ziplist全部节点的个数 */
unsigned long len; /* quicklistNode节点的个数 */
int fill : 16; /* 单个节点的填充因子 */
unsigned int compress : 16; /* 压缩端结点的深度 */
} quicklist;
复制代码
咱们能够明显地看出,quicklist是一个双向链表的结构,可是内部又涉及了ziplist,咱们能够这么说,在宏观上,quicklist是一个双向链表,在微观上,每个quicklist的节点都是一个ziplist
在redis.conf中,可使用下面两个参数来进行优化:
这种存储方式的优势和链表的优势一致,就是插入和删除的效率很高,而链表查询的效率又由ziplist来进行弥补,因此quicklist就成为了list数据结构的首选
hash这种结构在redis的使用时最为常见,在redis中,hash这种结构有两种表示:zipmap和dict
zipmap其格式形以下面这样: <zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"
各部分的含义以下:
这其中相邻的两个字符串就分别是键和值,好比在上面的例子中,就表示"foo" => "bar", "hello" => "world"
这样的对应关系
这种方式的缺点也很明显,就是查找的时间复杂度为O(n),因此只能看成一个轻量级的hashmap来使用
这种方式就适于存储大规模的数据,其格式以下:
typedef struct dict {
dictType *type; /* 指向自定义类型的指针,能够存储各种型数据 */
void *privdata; /* 私有数据的指针 */
dictht ht[2]; /* 两个hash表,通常只有h[0]有效,h1[1]只在rehash的时候才有值 */
long rehashidx; /* -1:没有在rehash的过程当中,大于等于0:表示执行rehash到第几步 */
unsigned long iterators; /* 正在遍历的迭代器个数 */
} dict;
复制代码
若是咱们不想更深刻的话了解到这种程度就能够了,其中真正存储数据的是dictEntry结构,以下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
复制代码
很明显是一个链表,咱们知道这是采用链式结构存储就足够了
这种方式会消耗较多的内存,因此通常数据较少时会采用轻量级的zipmap
在redis中,咱们能够查看intset.h
文件,这是一个存储整数的集合,其结构以下:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
复制代码
其中各字段含义以下:
具体的操做咱们就不详细展开了,了解集合这种数据结构的应该都很清楚,咱们这里说一下,intset有一个数据升级的概念,比方说咱们有一个16位整数的set,这时候插入了一个32位整数,因此就致使整个集合都升级为32位整数,可是反过来却不行,这也就是柔性数组的由来
若是集合过大,会采用dict的方式来进行存储
zset,有不少地方也叫作sorted set,是一个键值对的结构,其键被称为member,也就是集合元素(zset依然是set,因此member不能相同),其对应的值被称为score,是一个浮点数,能够理解为优先级,用于排列zset的顺序
其也有两种存储方式,一种是ziplist/zipmap的格式,这种方式咱们就不过多介绍了,只须要了解这种格式将数据按照score的顺序排列便可
另外一种存储格式是采用了skiplist,意为跳跃表,能够当作平衡树映射的数组,其查找的时间复杂度和平衡树基本没有差异,可是实现更为简单,形以下面这样的结构(图来源跳跃表的原理):