Redis内部数据结构的实现

还有两个多月就找工作了,决定把之前看的一些东西整理一下,做个记录,也整理一下最近的思路。

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实现的书籍了(好像也没有其他的)。

时间: 2024-10-08 17:55:29

Redis内部数据结构的实现的相关文章

[转]Redis内部数据结构详解-sds

本文是<Redis内部数据结构详解>系列的第二篇,讲述Redis中使用最多的一个基础数据结构:sds. 不管在哪门编程语言当中,字符串都几乎是使用最多的数据结构.sds正是在Redis中被广泛使用的字符串结构,它的全称是Simple Dynamic String.与其它语言环境中出现的字符串相比,它具有如下显著的特点: 可动态扩展内存.sds表示的字符串其内容可以修改,也可以追加.在很多语言中字符串会分为mutable和immutable两种,显然sds属于mutable类型的. 二进制安全(

【转】Redis内部数据结构详解——ziplist

本文是<Redis内部数据结构详解>系列的第四篇.在本文中,我们首先介绍一个新的Redis内部数据结构--ziplist,然后在文章后半部分我们会讨论一下在robj, dict和ziplist的基础上,Redis对外暴露的hash结构是怎样构建起来的. 我们在讨论中还会涉及到两个Redis配置(在redis.conf中的ADVANCED CONFIG部分): hash-max-ziplist-entries 512 hash-max-ziplist-value 64 本文的后半部分会对这两个配

【转】Redis内部数据结构详解 -- skiplist

本文是<Redis内部数据结构详解>系列的第六篇.在本文中,我们围绕一个Redis的内部数据结构--skiplist展开讨论. Redis里面使用skiplist是为了实现sorted set这种对外的数据结构.sorted set提供的操作非常丰富,可以满足非常多的应用场景.这也意味着,sorted set相对来说实现比较复杂.同时,skiplist这种数据结构对于很多人来说都比较陌生,因为大部分学校里的算法课都没有对这种数据结构进行过详细的介绍.因此,为了介绍得足够清楚,本文会比这个系列的

redisbook笔记——redis内部数据结构

在Redis的内部,数据结构类型值由高效的数据结构和算法进行支持,并且在Redis自身的构建当中,也大量用到了这些数据结构. 这一部分将对Redis内存所使用的数据结构和算法进行介绍. 动态字符串 Sds(Simple Dynamic String,简单动态字符串) Sds在Redis中的主要作用有以下两个: 1. 实现字符串对象(StringObject): 2. 在Redis程序内部用作char* 类型的替代品: 对比C 字符串,sds有以下特性: –可以高效地执行长度计算(strlen):

redis内部数据结构深入浅出

最大感受,无论从设计还是源码,Redis都尽量做到简单,其中运用到的原理也通俗易懂.特别是源码,简洁易读,真正做到clean and clear, 这篇文章以unstable分支的源码为基准,先从大体上整理Redis的对象类型以及底层编码. 当我们在本文中提到Redis的“数据结构”,可能是在两个不同的层面来讨论它. 第一个层面,是从使用者的角度,string,list,hash,set,sorted set 第二个层面,是从内部实现的角度,属于更底层的实现,   ht(dict),raw,em

你真的懂了redis的数据结构吗?redis内部数据结构和外部数据结构揭秘

Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有序集合SortedSet. 很多人面试时都遇到过这种场景吧? 其实除了上面的几种常见数据结构,还需要加上数据结构HyperLogLog.Geo. 可是很多人不知道redis 不仅有上面的几种数据结构,还内藏了内部的数据结构.即redis可以分为外部数据结构和内部数据结构. 1. 如何查看redis的数据结构? ####1.1 如何查看redis的外部数据结构?可以使用type命令,返回key的类型,如str

redis 内部数据结构 intset

这是<redis 七种内部数据结构>:https://www.cnblogs.com/christmad/p/11364372.html 的第七篇 原文地址:https://www.cnblogs.com/christmad/p/11365853.html

redis 内部数据结构 skiplist

这是<redis 七种内部数据结构>:https://www.cnblogs.com/christmad/p/11364372.html 的第六篇 原文地址:https://www.cnblogs.com/christmad/p/11365774.html

redis 内部数据结构 sds

这是<redis 七种内部数据结构>:https://www.cnblogs.com/christmad/p/11364372.html 的第二篇 原文地址:https://www.cnblogs.com/christmad/p/11365746.html