咱们都知道Redis是用C语言编写的内存数据库。可是因为C几乎没有提供任何数据结构的封装,因此Redis为了实现更快,更安全的操做,本身在内部封装了一系列的数据结构。 其中包括了简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表,下面来一一介绍(画的图有点丑。。)。redis
在redis中,只有字符串字面量才会用C字符串来表示(好比打印日志),其它都使用SDS来表示(好比键值对的键都是用SDS表示的字符串)。算法
struct sdshdr {
// 记录buf数组已使用的字节数,也就是SDS字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
复制代码
SDS为了能够重用C字符串函数库里的函数,因此遵循了用空字符结尾,但这个空字符不计入len属性中。数据库
当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表做为列表键的底层实现。同时,在发布与订阅、慢查询、监视器等功能也用到了链表。数组
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
void (*match)(void *ptr, void *key);
} list;
复制代码
链表结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len。而dup、free和match则是用于实现多台所需的类型特定函数,从而实现能够保存各类不一样类型的值。安全
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
}listNode;
复制代码
多个listNode能够经过prev和next指针组成双端链表。可是无环,由于表头节点的prev指针和表尾节点的next指针都指向NULL,因此对链表的访问以NULL为终点。 服务器
Redis的数据库就是使用字典做为底层来实现的。能够把数据库中全部的对象都看做是键值对,而这个键值对就是保存在表明数据库的字典里的。另外,哈希键的底层也是经过字典实现的。数据结构
typedef struct dict {
// 类型特定函数(我以为这个应该是至关于Java中的泛型)
dictType *type;
// 私有数据
void *privdata;
// 哈希表数组,字典存储使用ht[0],ht[1]在rehash迁移字典数据时使用
dictht ht[2];
// rehash索引,当rehash不在进行时,值为-1
int trehashidx;
} dict;
复制代码
type属性和privdata属性是针对不一样类型的键值对,为建立多态字典而设置的。函数
typedef struct dictht {
// 哈希表节点数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值,老是等于size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
复制代码
sizemask属性和哈希值一块儿决定一个键应该被放到table数组的哪一个索引上面。性能
typedef struct dictEntry {
// 键
void *key;
// 值,用union结构存储数据,用于压缩空间
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,造成链表(拉链法解决哈希冲突)
struct dictEntry *next;
} dictEntry;
复制代码
当要将一个新的键值对添加到字典里面时,程序须要先根据键值对的键计算出哈希值,再根据哈希表的sizemask和哈希值计算出索引值,而后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。 Redis使用MurmurHash2算法来计算键的哈希值,这种算法的优势在于,即便输入的键是有规律的,算法仍能给出一个很好的随机分布性,而且算法的计算速度也很是快。优化
扩展和收缩哈希表的工做能够经过执行rehash(从新散列)操做来完成,Redis对字典的哈希表执行rehash的步骤以下:
为了不rehash对服务器性能形成影响,服务器并非一次性将ht[0]里面的全部键值对所有rehash到ht[1],而是分屡次、渐进式的将ht[0]里面的键值对慢慢的rehash到ht[1]。这里就用到了rehashidx属性,当程序处理rehash期间时,rehashidx值被设置为0,当rehash操做完成时,又将它设置为-1.
渐进式rehash的好处在于它采起分而治之的方式,将rehash键值对所需的计算工做均摊到对字典的每一个增删改查操做上,从而避免了集中式rehash带来的庞大计算量。
另外,在rehash期间,字典的删除、查找、更新操做会在两个哈希表上进行,若是在ht[0]没有找到的话,就回去ht[1]找。而添加操做则所有在ht[1]进行,即全部新添加的键值对都会存到ht[1]里面。
跳跃表是一种有序数据结构,它经过在每一个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还能够经过顺序行操做来批量处理节点。在Redis中用跳跃表来做为有序集合的底层实现之一。
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
复制代码
level属性用于在O(1)复杂度内获取跳跃表中层数最高的那个节点的层数,注意,表头节点的层高并不能算在里面。
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode * forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
复制代码
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,且元素数量很少时,将会使用整数集合做为集合的底层实现。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
复制代码
encoding的类型能够是int16_t,int32_t或者int64_t。其中虽然contents被声明为int8_t,但实际上contents数组中不会保存int8_t类型的值,真正的类型仍是取决于encoding属性的值。注意,若是contents数组中包含了不一样整数类型的值,那么encoding将被设置为占用空间最大的那个类型。同时,其余值也将被升级编码为该类型。
当咱们要将一个新元素添加到整数集合里时,而且新元素的类型比整数集合现有元素的类型都要长时,咱们将须要先将整数集合进行升级,才能将新元素添加进去。
升级整数集合并添加新元素分三步进行:
压缩列表是列表建和哈希键的底层实现之一。当列表键或哈希键中的元素较少时,将会使用压缩列表来做为他们的底层实现。
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序性数据结构。一个压缩列表能够包含任意多个节点,一个节点能够保存一个SDS或一个整数值。
每一个压缩列表节点能够保存一个字节数组或者一个整数值。压缩列表节点由三部分组成。
记录了压缩列表中前一个节点的长度。previous_entry_length属性自身的长度能够是1字节或5字节。
负责保存节点的值,节点值能够是字节数组或整数,具体由encoding决定。
若是当前压缩列表的节点长度都小于254字节,那么用于记录前一个字节长度的属性previous_entry_length只须要用一个字节保存,可是如今要新加一个字节长度大于254字节的节点到压缩列表中来,那么将会形成连锁更新,由于新加节点的后一个节点保存了这个节点的长度,须要将previous_entry_length扩展为5字节的,而后继续相似的扩展直到最后一个节点。
Redis的设计与实现 黄建宏 著