相信你们都使用过redis,对redis的数据结构也有所了解。那么今天就从redis的数据结构的底层实现来研究下它为啥如此高性能。redis
redis没有直接用C语言传统的字符串表示(以空字符串结尾的字符数组),而是本身建立了一种名为简单动态字符串的抽象类型(simple dynamic string,SDS)。算法
不过在redis里面也会用到c字符串,可是它只做为字符串字面量用在一些无须对字符串值进行修改的地方,好比日志的打印。数据库
当redis须要的是一个能够修改的字符串值时,那么就会用SDS来表示字符串值,好比redis的数据库里,包含字符串的键值对在底层都是由SDS实现的。数组
好比 客户端执行安全
redis> set str "hello" OK
那么redis将在数据库建立一个新的键值对,其中 键值对的key是一个字符串对象,对象的底层实现是一个保存着 "str"的SDS。bash
键值对的value也是一个字符串对象,对象的底层实现也是一个保存着 "hello"的SDS。数据结构
又好比,客户端执行命令:app
redis>rpush fruits "banana" "pair" "apple"
那么redis将在数据库建立一个新的键值对,其中函数
键值对的键是一个字符串对象,对象的底层实现是一个保存着 "fruits"的SDS。性能
键值对的value是一个列表对象,列表对象包含了都由sds实现的三个字符串对象。
除了用来保存数据库中的字符串值之外,sds还被用来用做缓冲区等模块。
如上图所示:每一个sds.h/sdshdr结构表示一个SDS值:
struct sdshdr{ //记录buf数组中已使用字节的数量 //等于SDS所保存字符串的长度 int len; //记录buf数组中未使用的字节数量 int free; //字节数组,用于保存字符串 char buf[]; }
free = 0,表示这个sds没有可分配的使用空间。
len = 5,表示保存了一个5字节长的字符串。
buf属性是一个char类型的数组,数组前五个字节包存了'R','e','d','i','s'五个字符,而最后一个字节保存了空字符'\0'。
SDS遵循C字符串的空字符结尾的惯例的好处是,SDS能够直接重用一部分C字符串函数库里面的函数。另外SDS里面的空字符的1字节空间不计入len属性中。
1.3 SDS与字符串的区别
(1).常熟复杂度O(1)获取字符串长度
C字符串不记录自身长度信息,因此为了获取C字符串的长度,程序必须遍历整个字符串,因此这个操做的复杂度是O(N),
而SDS经过len属性记录了自身的字符串长度,因此复杂度是O(1)。
(2).杜绝缓冲区溢出
当SDS API须要对SDS进行修改时,API会先检查SDS的空间是否知足要求,若是不知足,API将会自动将SDS的空间扩展至执行修改所需的大小,
而后才去执行实际的修改操做。
而传统的c字符串因为不知道自身的长度,当修改字符串时可能会致使缓冲区溢出问题。
(3).减小修改字符串时带来的内存重分配次数
先看看C字符串的数组进行一次内存重分配操做: -若是程序执行增常字符串操做,好比append操做,程序须要经过内存重分配来扩展底层数组的空间大小---若是忘了,会致使缓冲区溢出。 -若是程序猿执行缩短字符串操做,好比trim,若是忘记铜鼓内存重分配释放再也不使用的空间,就会产生内存泄漏。 而SDS经过 空间预分配和惰性空间释放不会有以上问题。 空间预分配就是 当对SDS进行修改之后,SDS的长度(len属性值)将小于1MB,那么程序分配和len属性值一样大小的未使用空间;若是SDS的长度(len属性值)将>=1MB,那么程序分配1MB大小的未使用空间 惰性空间释放就是,SDS空间释放后,只要修改free属性的值就好
(4).二进制安全
其实就是文本信息含有多个空字符时,sds对其中的数据不作任何限制,数据写入时怎么样,读出来仍是怎么样。 而C字符串不行,以下图所示,C字符串这只能读取到Redis
(5).兼容部分C字符串函数
redis列表键的底层实现之一就是链表。当一个键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,redis就会使用链表做为列表键的底层实现
除了链表键之外,发布与订阅,慢查询,监视器等功能也用到了链表。
每一个链表节点使用一个adlist.h/listNode结构来表示
typedef struct listNode{ //前置节点 struct listNode *prev; //后置节点 struct listNode *next; //节点的值 void *value; }listNode;
多个listNode经过prev和next指针组成一个双端链表,以下图所示;
虽然仅仅使用多个listNode来组成链表,可是用adlist.h/list 来操做链表要方便不少。
typedef struct list{ //表头节点 listNode *head; //表尾节点 listNode *tail; //链表所包含的节点数量 unsigned long len; //节点值复制函数 void *(*dup)(void *ptr); //节点值释放函数 void *(*free)(void *ptr); //节点值对比函数 void *(*match)(void *ptr); }list;
list结构为链表提供了表头指针head,表尾指针tail,以及链表长度计数器len。另外还有dup,free,match这些函数。
redis的链表特性能够总结以下: 1.双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。 2.无环:表头节点的prev指针和表尾节点的next指针都指向null,对链表的访问以null为终点。 3.带表头指针和表尾指针:经过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度是O(1)。 4.带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。 5.多态:链表节点使用void*指针来保存节点值,而且能够经过list结构的dup,free,match三个属性为节点值设置类型特定函数,因此链表能够用于保存各类不一样类型的值。
字典,又称为符号表(symbol table),关联数组(associative array)或者映射(map),是一个用于保存键值对的抽象数据结构。
redis数据库就是使用字典来做为底层实现的,对数据库的增删改查操做也是构建在对字典的操做之上的。字段仍是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素比较长的时候,redis就会使用字典做为哈希键的底层实现。
redis的字典使用哈希表做为底层实现,一个哈希表里面能够有多个哈希表节点,而每一个哈希节点就保存了字典中的一个键值对。
typedef struct dictht{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 //老是等于size-1 unsigned long sizemask; //该哈希表已有节点数量 unsigned long used; }dictht;
table是一个数组,数组中每一个元素都是一个指向dict.h/dictEntry结构的指针,每一个dictEntry结构都保存着一个键值对。
哈希表节点使用dictEntry结构表示,每一个dictEntry结构都保存着一个键值对:
typedef struct dictEntry{ //键 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; }v; struct dictEntry *next; }dictEntry;
key保存着键值对的键;
而v属性则保存着键值对中的值,其中键值对的值能够是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数;
next 属性是指向另外一个哈希表节点的指针,这个指针能够将多个哈希值相同的键值对链接在一块儿,以此解决键冲突的问题。
typedef struct dict{ //类型特定函数 dictType *type; //私有数据 void *privdata; //哈希表 dictht ht[2]; //rehash索引 //当rehash不在进行时,值为-1 int rehashidx; }dict;
type和privdata属性是针对不一样类型的键值对,为建立多态字典而设置的;
ht属性是包含两个项的数组,每一个项都是一个dictht哈希表,通常状况下,字典桌子使用ht[0],ht[1]哈希表侄仔进行rehash时使用。还有一个属性rehashidx,记录的是rehash目前的进度。
附图看看就能理解了:
至于哈希算法和rehash操做将在下次附上。