今天主要介绍一下Redis中用到的底层数据结构,其主要包含6种,分别为动态字符串、链表、跳跃表、压缩链表、字典、整数集合。

1. 动态字符串

SDS

int len;  //代表实际长
int free; // 代表 buf 中未使用的长度
char[] buf; // 实际存储东西的地方

优势:

  1. O(1) 时间获得长度
  2. 减少修改字符串长度时,内存充分配次数
  3. 可重用 C 里面部分关于字符串的函数

2. 链表

链表节点的实现

typedef struct listNode {
    ListNode  *prev
    ListNode *next
    void * value
}

链表的实现

typedef struct list {
	listNode *head;
	listNode *tail;
	unsigned long len;
	
	// 节点值复制函数
	void *(*dup) (void *ptr)
	// 节点值释放函数
	void *(*free) (void *ptr)
	// 节点值对比函数
	void *(*match) (void * ptr)
}

通过以上结构,可以看出,Redis 中链表的几个特性,

  1. 双端
  2. 无环
  3. O(1)获得链表头部和链表尾部
  4. O(1)获得链表长度
  5. 多态: 因为链表节点中使用 void 来代表链表的值,因此其可以实现保存不同类型的值的方式,并且 list 结构中也提供了用于复制、释放、对比链表节点值的函数的指针。

3. 字典

redis 中字典采用 hash 表实现。

hash 表

hash 表定义如下:

typedef struct dictht {

    dictEntry **table;//哈希表数组

    unsigned long size; //哈希表大小

    unsigned long sizemask;//哈希表大小掩码,用于计算索引值 ,总是等于size-1

    unsigned long used;//该哈希表已有节点的数量

} dictht;

其中 table 是一个数组,数组的每一项是一个指向 dictEntry 结构的指针。sizemask 用于计算某个 hash 值对应的索引,通过求余运算可快速的到索引值(和 java 中 hashmap 计算索引值的方法一样)。dictEntry 结构定义如下:

typedef struct dictEntry {

    void *key;  //键
 
   union{

        void *val;

        uint64_t u64;

        int64_t s64;

    } v;  //值

    struct dictEntry *next;    //指向下个哈希表节点,形成链表

} dictEntry;

其中 键值对的值可以是一个指针或 uint64_t 或 int64_t 类型的值。
next 属性用来解决 hash 冲突(链地址法)。

字典结构

redis 中的字典结构如下:

typedef struct dict {

    dictType *type; //类型特定函数

    void *privdata;//私有数据

    dictht ht[2];    //哈希表

    // rehash索引
    //当rehash不在进行时,值为-1
    int rehashidx;

} dict;

ht 属性包含两个 hash 表,其中一个在 rehash 的时候配合 rehashidx使用。
type 属性和 privatedata 属性是针对不同类型的键值对而设立的。

  • type

type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。

  • privatedata

而privdata属性则保存了需要传给那些类型特定函数的可选参数。

typedef struct dictType {

    //计算哈希值的函数
	unsigned int (*hashFunction)(const void *key);

    //复制键的函数
    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;

4. 跳跃表

跳跃表是一种有序的数据结构,它通过在每个节点维护多个指向其他节点的 指针,从而达到快速访问其他节点的功能。Redis 中只有两个地方用到了跳跃表:

  1. 有序集合键
  2. 集群节点中用作内部数据结构

在 redis 实现中,跳跃表主要定义了两个结构,zskiplist 和 zskiplistnode 结构。
zskiplist 结构如下:

typedef struct zskiplist {
    struct zskiplistNode* header,  *tail // 头节 点,尾节点
    unsigned long level // 节点的最大层级,表头节点不计算在内
    int length// 长度,表头节点不计算在内
}

zskiplistnode 结构如下:

typedef struct zskiplistNode {
    //层
     zskiplistLevel {
			//前进指针
            struct zskiplistNode *forward;
			//跨度
            unsigned int span;
    } levle[];
		// 后退指针
    struct zskiplistNode *backward;
		// 分值
    double score;
    robj* obj;
}
  1. 后退指针
    用于表尾向表头遍历访问
  2. 分值和成员
    跳跃表中,各节点按照分值从小到大排列,分值相同的,按照成员对象的字典序排序。成员对象是一个指针,指向一个字符串对象
  3. level 数组
    1 个节点分属于多个层,在每次生成新节点的时候,从 1 至 32(redis 中宏定义) 中随机生成一个值作为数组大小。
  • 前进指针
    每层的前进指针,用于向前遍历,也用于加快搜索速度
  • 跨度
    记录两个节点之间的距离,计算两个节点 的 rank,在查找某节点时,将遍历过程中访问过的层的跨度加起来即得到该节点的 rank。

5. 整数集合

typedef struct intset {
	uint32_t encoding;
	uint32_t length;
	int8_t[] contents;
}

contents 是保存实际内容的数组,虽然其类型标明为 int8_t,但是其实际保存内容的类型根据 encoding 的格式来确定,目前,encoding 的格式有 INTSET_ENC_INT64 等。

整数集合存在升级的概念,但不存在降级的概念,即当新插入的数值,在原来 encoding 已经无法保存的情况下,整数集合会进行升级的操作,即提高 encoding ,来增大数组所能保存的值的范围,当进行 encoding 升级之后,无法进行降级操作了。 有以下几个优势:

  1. 提升灵活性:自动进行数值的管理,不会发生溢出的情况。
  2. 节约内存:从使用者的角度,当只保存 int16 的值的情况,其每个数占 2 个字节,当保存 大于 int16 的情况,其才进行升级操作。

6. 压缩链表

zip list,从名字就可以看出来,压缩链表是为了节约内存而开发的。其虽然叫链表,但是其使用的是内存中连续的内存块。
组成:压缩链表可以包含任意多个节点(entry),每个 entry 可以包含一个字节数组或整数值。
在压缩链表的那块连续内存块中,包含了以下 5 个部分:

  1. zlbytes:4 个字节,记录压缩链表使用了多少个字节。
  2. zltail:4 个字节,记录最后一个 entry 的偏移量,
  3. zllen:2 个字节,entry的数量,如果值超过了 UINT16_MAX 时,就需要遍历来得到 entry数目了。
  4. entryx:
  5. zlend:1 个字节,0xff,压缩链表的结束标志
    每个 entry 呢,其包含了以下几个部分:
  6. previous_entry_length: 1 个字节或 5 个字节,以字节为单位记录前一个 entry 的长度,1 字节的话,最大表示到 253 长度,当前一个 entry 大于等于 254 时,就用 5 字节表示了。
  7. encoding: 1 字节,2 字节或 5 字节,记录了 content 的属性及长度,当最高两位为 11 时,表示 content 的整数值,当为其他时,content 为字节数组,其余位的内容,表示了 content 的长度或者类型属性
  8. content:保存节点的真实内容
    ps: 从前往后找节点,都是根据字节数来找的,nb。

可能出现的问题

  1. 连锁更新
    previous_entry_length 值刚好都为 253,这时来了个 256 字节的 entry 作为头节点,后面的都得更新 previous_entry_length

本篇文章总结于黄健宏老师的《Redis 设计与实现》