Redis5——数据结构

422 阅读11分钟

Redis5——数据结构

Redis是一款高性能的开源key-value型数据库,想要深入了解Redis,就得对底层的数据结构有了解,本文简要介绍了Redis5中用到的8种数据结构,包括简单动态字符串(SDS)、字典(Hash)、双向链表、压缩列表(ziplist)、快速链表(quicklist)、整数集合、跳跃表(skiplist)、Stream。

简单动态字符串(SDS)

介绍

Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。SDS是Redis的基本数据结构之一,用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

数据结构

/* 这里记录5种SDS字符串的结构  */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 低3位存储类型,高5位存储长度 */
    char buf[]; /* 字符数组 */
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已使用长度,占用1字节 */
    uint8_t alloc; /* 总长度,占用1字节 */
    unsigned char flags; /* 低3位存储类型,高5位预留 */
    char buf[]; /* 字符数组 */
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* 已使用长度,占用2字节 */
    uint16_t alloc; /* 总长度,占用2字节 */
    unsigned char flags; /* 低3位存储类型,高5位预留 */
    char buf[]; /* 字符数组 */
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* 已使用长度,占用4字节 */
    uint32_t alloc; /* 总长度,占用4字节 */
    unsigned char flags; /* 低3位存储类型,高5位预留 */
    char buf[]; /* 字符数组 */
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 已使用长度,占用8字节 */
    uint64_t alloc; /* 总长度,占用8字节 */
    unsigned char flags; /* 低3位存储类型,高5位预留 */
    char buf[]; /* 字符数组 */
};
/* sds类型定义 */
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

字典(Hash)

介绍

字典又称散列表,是用来存储键值(key-value)对的一种数据结构。在很多高级语言中都有实现,如Java中的HashMap。但是C语言没有这种数据结构,Redis是K-V型数据库,整个数据库是用字典来存储的,对Redis数据库进行任何增、删、改、查操作,实际就是对字典中的数据进行增、删、改、查操作。

Redis中的字典与Java中HashMap有很多相似之处,如大小都是2的幂次,方便计算索引值和扩容;Hash碰撞后都是将碰撞的数据采用头插法的方式形成链表(Java8在链表过长时会转成红黑树)。

数据结构

/* hash表 */
typedef struct dictht {
    dictEntry **table; /* 节点数组,存储键值对 */
    unsigned long size; /* table数组的大小 */
    unsigned long sizemask; /* 大小掩码,值为size-1 */
    unsigned long used; /* 已有元素数量 */
} dictht;
/* hash表节点 */
typedef struct dictEntry {
    void *key; /* 键 */
    union {	/* 值,根据类型选择对应的格式 */
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; /* hash碰撞时,指向下个节点,形成单链表 */
} dictEntry;
/* 字典表 */
typedef struct dict {
    dictType *type; /* 字典函数类型 */
    void *privdata; /* 私有数据 */
    dictht ht[2]; /* hash表,一般用ht[0],在扩容时才会用到ht[1] */
    long rehashidx; /* 扩容标识,-1表示不在扩容,不为-1时表示扩容到具体的索引值 */
    unsigned long iterators; /* 当前运行的迭代器数 */
} dict;
/* 字典函数类型 */
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key); /* hash函数 */
    void *(*keyDup)(void *privdata, const void *key); /* 键复制函数 */
    void *(*valDup)(void *privdata, const void *obj); /* 值复制函数 */
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); /* 键对比函数 */
    void (*keyDestructor)(void *privdata, void *key); /* 键销毁函数 */
    void (*valDestructor)(void *privdata, void *obj); /* 值销毁函数 */
} dictType;

结构示意图

双向链表

介绍

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。因为Redis使用的C语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。

链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

数据结构

/* 链表节点 */
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;

结构示意图

压缩列表(ziplist)

介绍

压缩列表ziplist本质上就是一个字节数组,是Redis为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组合。

数据结构

/*
 * ZIPLIST OVERALL LAYOUT
 * ======================
 *
 * The general layout of the ziplist is as follows:
 *
 * <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
 *
 * NOTE: all fields are stored in little endian, if not specified otherwise.
 *
 * <uint32_t zlbytes> is an unsigned integer to hold the number of bytes that
 * the ziplist occupies, including the four bytes of the zlbytes field itself.
 * This value needs to be stored to be able to resize the entire structure
 * without the need to traverse it first.
 *
 * <uint32_t zltail> is the offset to the last entry in the list. This allows
 * a pop operation on the far side of the list without the need for full
 * traversal.
 *
 * <uint16_t zllen> is the number of entries. When there are more than
 * 2^16-2 entries, this value is set to 2^16-1 and we need to traverse the
 * entire list to know how many items it holds.
 *
 * <uint8_t zlend> is a special entry representing the end of the ziplist.
 * Is encoded as a single byte equal to 255. No other normal entry starts
 * with a byte set to the value of 255.
 */

大概意思是:

ZIPLIST 总体布局是

1)zlbytes:占4个字节,是一个无符号整数,用于保存ziplist占用的字节数。有个这个值,可以不需要遍历压缩列表就其大小

2)zltail:占4个字节,是列表中最后一个entry的偏移量。这允许不需要完全遍历压缩列表就在结尾处进行pop操作。

3)zllen:占2个字节,是压缩列表的元素个数。当entry超过2^16-2时,这个值设置为2^16-1,我们需要遍历列表才知道具体entry个数。

4)entry:压缩列表存储的元素,可以是字节数组或者整数,长度不限。

5)zlend:表示压缩列表的结尾,占1个字节,恒为255,即0xFF。

快速链表(quicklist)

介绍

快速链表由双向链表和ziplist结合而成,双向链表充当链表作用,ziplist充当连续占用空间数组的作用。快速链表本身就是一个双向链表,每个节点都是ziplist。为什么要这样设计呢?

  1. 双向链表在插入节点上复杂度很低,但它的内存开销很大,每个节点的地址不连续,容易产生内存碎片。
  2. ziplist是存储在一段连续的内存上,存储效率高,但是它不利于修改操作,插入和删除数都很麻烦,复杂度高,而且其需要频繁的申请释放内存,特别是ziplist中数据较多的情况下,搬移内存数据太费时!

Redis综合了双向链表和ziplist的优点,设计了quicklist这个数据结构,使它作为列表键的底层实现。quicklist可以看成是用双向链表将若干小型的ziplist连接到一起组成的一种数据结构。当ziplist节点个数过多,quicklist退化为双向链表,一个极端的情况就是每个ziplist节点只包含一个entry,即只有一个元素。当ziplist元素个数过少时,quicklist可退化为ziplist,一种极端的情况就是quicklist中只有一个ziplist节点。

数据结构

/* 双向链表 */
typedef struct quicklist {
    quicklistNode *head; /* 头节点 */
    quicklistNode *tail; /* 尾节点 */
    unsigned long count;        /* 元素个数 */
    unsigned long len;          /* 节点个数 */
    int fill : 16;              /* ziplist长度,正数为元素个数限制,负数为大小限制 */
    unsigned int compress : 16; /* 首尾不压缩的节点个数 */
} quicklist;
/* 节点 */
typedef struct quicklistNode {
    struct quicklistNode *prev; /* 前置节点 */
    struct quicklistNode *next; /* 后置节点 */
    unsigned char *zl; /* ziplist */
    unsigned int sz;             /* ziplist的大小 */
    unsigned int count : 16;     /* ziplist元素的个数 */
    unsigned int encoding : 2;   /* 编码方式:1:原生RAW 2: LZF压缩 */
    unsigned int container : 2;  /* 容器类型 1: NONE 2: ZIPLIST */
    unsigned int recompress : 1; /* 代表这个节点之前是否是压缩节点,若是,则在使用压缩节点前先进行解压缩,使用后需要重新压缩,为1,代表是压缩节点 */
    unsigned int attempted_compress : 1; /* 节点太小,不压缩 */
    unsigned int extra : 10; /* 预留 */
} quicklistNode;
/* 压缩节点 */
typedef struct quicklistLZF {
    unsigned int sz; /* 节点大小 */
    char compressed[]; 
} quicklistLZF;
/* 元素 ziplist中单个元素 */
typedef struct quicklistEntry {
    const quicklist *quicklist; /* 当前元素所在的链表 */
    quicklistNode *node; /* 当前元素所在的节点 */
    unsigned char *zi; /* 当前元素所在的ziplist */
    unsigned char *value; /* 字符串值 */
    long long longval; /* 整型值 */
    unsigned int sz; /* 值大小 */
    int offset; /* 该元素在ziplist中的偏移量*/
} quicklistEntry;

结构示意图

整数集合

介绍

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

数据结构

typedef struct intset {
    uint32_t encoding; /* 编码类型 */
    uint32_t length; /* 元素个数 */
    int8_t contents[]; /* 元素数组 */
} intset;

跳跃表(skiplist)

介绍

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。

数据结构

/* 跳跃表节点 */
typedef struct zskiplistNode {
    sds ele; /* 键 */
    double score; /* 分值 */
    struct zskiplistNode *backward; /* 后退指针,只在第一层,用于从后向前遍历节点,头节点和第一个节点为NULL */
    struct zskiplistLevel {
        struct zskiplistNode *forward; /* 前进指针,指向本层下一个节点,尾节点的forward指向NULL */
        unsigned long span; /* 跨度,用于记录节点之间的距离 */
    } level[]; /* 节点层数,在生成跳跃表节点时,随机生成一个1~64的值,值越大出现的概率越低 */
} zskiplistNode;
/* 跳跃表 */
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; /* 头节点和尾节点 */
    unsigned long length; /* 跳跃表长度,除头节点外的节点总数 */
    int level; /* 跳跃表的高度,即表中层数最⼤的节点的层数 */
} zskiplist;

头节点:跳跃表的一个特殊节点,它的level数组元素个数为64。头节点在有序集合中不存储任何member和score值,ele值为NULL, score值为0;也不计入跳跃表的总长度。头节点在初始化时,64个元素的forward都指向NULL, span值都为0

结构示意图

Stream

Redis5新加的特性,先占个位。

注意:本文基于Redis5.0.8版源码进行分析,其中参考了陈雷老师的《Redis 5设计与源码分析》,这本书非常适合想要深入了解Redis的人。