剖析源代码使用的Redis源代码为4.0.10版本
可以直接从redis的官网redis.io获取
本次讲解Redis底层数据结构,对于比较基础的内容不会做过多的讲解。
字符串
字符串是Redis中的基础数据结构
所有的key都是字符串
Redis中因为众所周知的原因放弃了C语言原生字符串进行了封装
redis中的字符串被称为Simple dynamic string
举个应用场景的例子:
redis> SET msg "Hello World"
OK
在这个场景中Key是msg,其底层就是一个保存着msg的SDS,value的”Hello World"的底层也是一个SDS
在sds.h中可以看到SDS的结构
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[];
};
其中len用于记录字符串长度,alloc用于记录分配的空间。
对于常用API均在sds.h文件中有封装
相比较于C的字符串,SDS可以常数时间获取长度,同时防止了缓冲区溢出。在此不展开。有兴趣可以查看源码和相关书籍
链表
在adlist.c中定义了链表节点和链表结构
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
typedef struct list {
listNode *head;
listNode *tail;
//复制函数
void *(*dup)(void *ptr);
//释放函数
void (*free)(void *ptr);
//匹配函数
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
typedef struct listIter {
listNode *next;
int direction;
} listIter;
可以看到Redis中的链表为双向链表,同时会记录链表长度。
节点中的值为(void *)类型,目的是保存各种值。
三个函数指针是出于多态的目的设计的,用于保存不同的函数实现不同数据类型的各种功能。
listIter是一个用于遍历链表的迭代器,direction表示方向,用于正向或者反向遍历链表。
链表API请看adlist.c文件,都是链表的常规操作。
字典
直接上代码
在dict.h中定义了字典的结构
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx;
//rehashidx=-1表示不在rehash过程中
unsigned long iterators; /* number of iterators currently running */
} dict;
dichht中定义了一个table(哈希表)用于存放Entry,Entry都是一个KV对。Entry用next指针指向下一个Entry,dict中用拉链法解决Hash冲突。sizemask用于计算索引值。
放一张比较清楚的图,图片来自于《Redis的设计与实现》
一个dict中有2个dictht,这个主要作用在于rehash扩容。
Redis为了防止一次性扩容使得客户端响应时间增加,采用了一种渐进式rehash的方法,每次对数据进行操作时,将一部分的数据rehash到另一个dictht中。
rehash的时机:负载因子(used/size)大于1。
跳跃表
非常神奇的数据结构,数学上可以证明这个东西搜索的平均时间复杂度时O(lgN),和搜索树差不多。从上往下看其实就是一颗二叉搜索树。
在redis.h中定义了skiplist相关的结构(很奇怪为什么在这里定义。。)
typedef struct zskiplistNode {
sds ele;
double score;//权重
struct zskiplistNode *backward;//后退指针
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度,用于表示该位置到下一个节点,需要跳过几个最底层节点
} level[];//层级数组
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;//头尾节点
unsigned long length;//跳表长度
int level;//层数
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
可以看出有序集合使用的结构是一个dict和一个跳跃表。
跳跃表中,每一个节点都有一个层数,层数在创建节点随机决定。层数越高,节点数越少。
每一层形成一个链表,搜索时从最高层开始,搜到则返回结果搜到不存在的节点则向下走一层,直到搜到最底层仍没有则表中不存在该值。
附一个跳跃表搜索过程。
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))//ZSKIPLIST_P=0.25
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
//ZSKIPLIST_MAXLEVEL=32
}
该函数用于确定每个节点的层数,最高层数为32,每个节点每次有0.25的概率层数+1,一个节点层数的期望就是1+0.25^1+0.25^2+···+0.25^n。求和就是每个节点约1.33层。
插入一个节点时,先从最高层开始搜索,找到插入位置插入,之后下降一层,继续寻找插入位置插入,依次类推直到每一层都插入。代码细节比较多,这里不贴。有兴趣可以查看t_zset.c中相关部分代码。
skiplist的优势在于可以O(lgN)时间复杂度进行查找,有序集合中常用的统计范围中的数据就利用了这个优势。使用dict的目的在于O(1)复杂度进行查找成员。
下周更新剩下的数据结构,quicklist,ziplist,intset
参考资料:
1.《Redis设计与实现》 黄健宏
2. https://blog.csdn.net/acceptedxukai/article/details/17333673