还有两个多月就找工作了,决定把之前看的一些东西整理一下,做个记录,也整理一下最近的思路。
Redis 作为一个基于key=>value的内存数据库,使用ANSI C语言实现,以其高性能和支持丰富的数据结构闻名于世,而其数据结构也是其高性能的基础,今天分享一下我对此的理解,并以redis3.2的正式版源码分析。
在Redis内部,有非常多的数据结构:sds(简单动态字符串),list,intset(整数集合),hash(字典),zskiplist(跳跃表),ziplist(压缩表)等。
1. sds
typedef char *sds;
sds是一种简单动态字符串,而sdshdr封装了C原生字符串,并在其基础上,增加了一些功能,使之后对它的调用简单易懂可扩展。
/*下面是sds数据结构的具体实现*/
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
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 */
char buf[];
};
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[];
};
这是sdshdr的具体实现结构体,它作为sds的持有者,对sds进行存储和处理,len表示sds的长度,alloc表示分配了的长度,这样方便扩展;flags标志来判断使用哪个类型;buf[]则作为sds的真正储存数组,关系大致如下:
Redis采用动态字符串的形式,用len记录长度,这样可以在O(1)的复杂度内获取字符串长度;根据不同的类型和字符串的长短,分别用不同类型的sdshdr,可以节约不少空间;将alloc和len分离,可以在一定的范围内节省分配内存所用的时间;在Redis中,运用了大量的指针移动技巧来获取void*对象,也提高了程序的运行效率。例如:
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
通过s-sizeof(sdshdr)来获取sdshdr的地址;再如:
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
运用s[-1]来获取flags,判断类型。
其实,运用指针的移动而不是数组的下标,还保证了字符串的二进制安全,在遇到‘\0‘的情况下不会中断。
sds字符串在Redis里的应用:
1.保存数据库中字符串的值
2.用作缓冲区:
AOF模块的缓冲区,
客户端状态中的输入缓冲区。
2.list
看看list的实现,非常有助于复习一遍数据结构中链表的实现原理,建议大家仔细阅读源码。
typedef struct listNode { /*节点*/
struct listNode *prev;
struct listNode *next;
void *value; /*value用函数指针类型,决定了value可以是sds,list,set,dict等类型*/
} listNode;
以上是listNode的数据结构。
typedef struct list { /*链表结构*/
listNode *head; /*头节点*/
listNode *tail; /*尾节点*/
/*类似java类里的的方法,方便调用*/
void *(*dup)(void *ptr); /*复制节点*/ //说实话,我不是很懂这个函数指针的意思,如有清楚地可以给我留言,谢谢。
void (*free)(void *ptr); /*释放节点*/
int (*match)(void *ptr, void *key); /*匹配节点,返回key值得index,但是我不清楚他在那里实现的*/
unsigned long len; /*记录链表的长度*/
} list;
Redis中,list的实现是一个双端链表,这样可以方便的获取其前后的节点值,方便之后对节点的查找;Redis通过list来对listNode进行持有,分别记录list的头尾节点和list长度,可在O(1)的时间复杂度上进行查找;
另外,list还实现了迭代器对链表进行遍历,可正向可反向,非常方便,代码如下;
typedef struct listIter {
listNode *next;
int direction; //标注迭代器的运行方向
} listIter;
list在Redis中运用相当广泛,除了实现列表外,发布和订阅、慢查询、监视器等功能也使用了链表来获取,另外,Redis服务器还使用链表来持有 多个客户端的状态信息,以及用链表来构建客户端输出缓冲区。
注:Redis中void* 来修饰value值,所以value可以是任意类型的数据,void*在这里实现了一种类似多态的思想。
3.dict(字典)
字典结构是整个Redis的核心数据结构,基本上是其内部结构的缩影。
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
dictEntry是最核心的字典结构的节点结构,它保存了key和value的内容;另外,next指针是为了解决hash冲突,字典结构的hash冲突解决方法是拉链法,对于hashcode重复的节点以链表的形式存储。
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask; /*hash表的掩码,总是size-1,用于计算hash表的索引值*/
unsigned long used;
} dictht;
dictht是节点dictEntry的持有者,将dictEntry结构串起来,table就是hash表,其实dictEntry *table[]这样的书写方式更容易理解些,size就是table数组的长度,used标志已有节点的数目。
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
dict是最外层的字典结构的接口形式,type标志类型,privdata标志其私有数据,dict持有两个dictht结构,一个用来存储数据,一个用来在rehash时使用,rehashidx标志是否正在rehash(因为Redis中rehash是一个渐近的过程,正在rehash的时候rehashidx记录rehash的阶段,否则为-1)。
注:rehash是一个为了让负载因子(load_factor=used/size)控制在一个合理的范围内而重新分配内存和扩展结构的过程。
iterators是一个迭代器,用于记录当前迭代的数目。
上面的图来自网络,非常清晰的记录了dict内部结构之间的关系。
注:因为dictEntry节点组成的链表没有子项链表尾部的指针,所以新加的节点一般都加在链表的头部,排在已有节点的前面,因为这样的时间复杂度为O(1)。
字典结构在Redis中广泛应用,包括数据库和hash键值对。
4.intset
typedef struct intset { /*整数集合的数据结构*/
uint32_t encoding; //编码方式
uint32_t length;
int8_t contents[];
} intset;
当一个集合元素只有整数并且数量元素不多的时候,可以选择用整数集合来作为其底层实现。整数集合的数据结构如上所示。
重点说一下这个contents数组,它存储集合中的内容,并且以从小到大的顺序排列,并保证其没有重复的元素。
虽然定义中其类型为int8_t,但具体编码方式还是取决于encoding。
当最大的数在以上取值范围之内是便会升级到这个更大范围的数据类型,但是如果移除了这个最大取值,不会降级。
分范围定义其类型有两个好处:提高其灵活性,节约内存。但是也增加了升级的开销。
在Redis 中,整数集合的应用范围不是很广,只在实现集合时用到。
5. zskiplist(跳跃表)
对于不了解跳跃表的可以去这个地方看看,了解一下:http://blog.nosqlfan.com/html/3041.html。
跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现,简单的多的多。
typedef struct zskiplistNode {
robj *obj; //存储对象的指针
double score; //分数
struct zskiplistNode *backward; //后退指针,每次只能退一步
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针,每次可以跳跃好几步
unsigned int span; //这个就是决定前进指针能跳跃几步的跨度标志
} level[];
} zskiplistNode;
zskiplistNode是跳跃表的节点结构,obj指针指向存储具体对象的地址,score标志分数。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zskiplist持有节点,并记录头结点和尾节点以及长度,level记录层数最大的节点的层数,也就是zskiplistNode中最大的level.size。
上图来自于一本非常经典的书籍《Redis的设计与实现》。非常清晰的勾勒了跳跃表的数据结构,单看这个图,就知道其查找效率高于链表。这是一种用空间来换时间的链表实现。
注:其层次的分配是随机的,下面是其随机生成的算法,非常简单。
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
跳表在Redis中仅仅作为zset(有序集合)的底层实现出现,所以其数据结构定义在server.h中,其实现函数在t_zset.c中。
6.ziplist(压缩表)
ziplist是一个编码后的列表,是由一系列特殊编码的连续内存块组成的顺序型数据结构,特殊的设计使得内存操作非常有效率,此列表可以同时存放字符串和整数类型,列表可以在头尾各边支持推加和弹出操作在O(1)常量时间,但是,因为每次操作涉及到内存的重新分配释放,所以加大了操作的复杂性
。
typedef struct zlentry {
//prevrawlen为上一个数据结点的长度,prevrawlensize为记录该长度数值所需要的字节数
unsigned int prevrawlensize, prevrawlen;
//len为当前数据结点的长度,lensize表示表示当前长度表示所需的字节数
unsigned int lensize, len;
//数据结点的头部信息长度的字节数
unsigned int headersize;
//编码的方式
unsigned char encoding;
//数据结点的数据(已包含头部等信息),以字符串形式保存
unsigned char *p;
} zlentry;
zlentry是实际存储数据的节点。一个ziplist可以有多个zlentry节点,具体形式如下:
压缩表之所以成为压缩表,是因为它起到了一定的压缩功能,对于其他的数据结构为了快速定位,使用了大量的指针结构,这样对于长度较大的数据优势明显,但是对于长度非常小的数据,比如说一个表里的每一个数据长度都很短,但是数据量并不小,这样的话,就会出现大量的指针结构,造成内存浪费,而压缩表则分配了一块连续内存来存储,就避免了大量的指针结构,节省了内存。另外,ziplist也使用了动态分配内存的方法,也一定程度上避免了内存的浪费。下图(此图来自书本)是内存的每块代表的含义:
压缩表在Redis中的应用只存在于hash和list结构的实现中,为了在存储时节省内存。
在Redis 中,这几个数据结构算比较核心的了,大部分的功能需求都可以通过这几个中的一个或多个实现。
推荐一本非常不错的讲解Redis源码的书籍:黄健宏先生的《Redis 的设计与实现》,也是翻译本,在中国算比较核心的讲解Redis实现的书籍了(好像也没有其他的)。