redis设计与实现-数据结构与对象

192 阅读5分钟

本文主要从redis的基本数据结构开始总结

一、简单动态字符串

1.1 结构描述以及示意图

struct sdshdr{
    // 已使用长度
    int len;
    // 未使用长度
    int free;
    // 实际保存数据的地方
    char buf[];
};

图示:
下图显示的是一个总长度为5,且已经存储了redis这个字符串的结构: image.png


下图显示的是一个总长度为10,且已经存储了redis这个字符串的结构: image.png

1.2 为什么redis要自定义SDS

原因以及优点总结如下:

  • 获取字符串长度只要常数复杂度;
  • 避免缓冲区溢出;
  • 减少修改字符串带来的内存重新分配次数,SDS的解决方式是空间预分配和惰性释放;
  • 二进制安全;
  • 同时SDS也能够兼容大部分C字符串函数。

二、链表

链表作为一种常用的数据结构,内置在很多高级语言中,但C语言偏偏没有提供现成的链表(有知道原因的小伙伴可以评论区留言),因此redis只能自己实现一遍。

2.1 结构描述以及示意图

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的值
    void *value;
} listNode;

通过将listNode串联起来则可以形成一个双向链表: image.png 但是redis定义了list这个结构,除了串联起对应的链表,还加入了一些额外的字段:

typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 复制方法
    void *(*dup)(void *ptr);
    // 释放方法
    void (*free)(void *ptr);
    // 对比方法
    void (*match)(void *ptr, void *key);
    // 节点数量
    unsigned long len;
} list;

image.png

2.2 链表特性

  • 双端:获取前置节点与后置节点的复杂度都是O(1)
  • 无环:链表的访问以NULL为终点
  • 带表头指针和表尾指针:获取表头节点与表尾节点的复杂度为O(1)
  • 带长度计数器:获取长度的复杂度为O(1)
  • 多态:使用void * 指针保存节点值,并且三个方法dup,free,match都可以为节点值设置特定函数,因此链表能存储各种不同类型的值

三、字典

字典是一种用于保存键值对的抽象数据结构,键值对值得是一个键和一个值进行关联。

3.1 结构描述以及示意图

3.1.1 哈希表

typedef struct dictht {
    // 哈希表数组
    dictEntry **table,
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值使用
    unsigned long sizemask;
    // 哈希表已有节点的数量
    unsigned long used;
} dictht;

image.png

3.1.2 哈希表节点

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        unit64_t u64;
        int64_t s64;
    } v;
    // 下一个节点
    struct dictEntry *next;
}

image.png

3.1.3 字典

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht *ht[2];
    // 当没有进行rehash时,值为-1
    int rehashidx;
}

image.png

3.2 字典特性

  • redis中的字典使用哈希表作为底层实现,每个字典有两个哈希表,一个正常使用,一个rehash使用
  • 哈希过程不是一次性完成,而是采用渐进式hash的方式实现
  • 当同一索引上产生冲突时,采用链地址发解决冲突

四、跳跃表

4.1 初识跳跃表

跳跃表作为一种数据结构,通过分层的方式,对于n个排序的元素,实现O(logn)的查找,删除和插入操作。其数据结构利用图表示如下:

image.png 注意:每一列对齐的代表一个节点,而不是多个节点

4.2 redis中跳表结构描述以及示意图

接下来主要看看redis中,是如何定义对应的结构体的
每一个节点的定义如下:

typedef struct zkskiplistNode {
    // 成员对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zkskiplistNode *backward;
    // 层
    struct zkskiplistLevel {
        struct zkskiplistNode *forward;
        unsigned int span;
    } level[];
} zkskiplistNode

整个跳表的定义如下:

typedef struct zkskiplist {
    // 表头节点和表尾节点
    strcut zkskiplistNode *head, *tail;
    // 长度
    unsigned long length;
    // 最高层数
    int level;
} zkskiplist

跳表的示意图如下:

image.png

4.3 跳跃表特性总结

  • 跳跃表是有序集合的实现方式之一
  • 跳跃表的节点按照分值大小进行排序,若分值相同,则按照成员对象大小进行排序

五、整数集合

5.1 场景

当一个集合只包含整数值元素,且包含的元素并不多时,redis就会使用整数集合作为集合键的底层实现

5.2 结构描述以及示意图

typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 长度
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
}intset;

image.png

5.3 总结

  • 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型,即升级
  • 整数集合只支持升级操作,不支持降级操作

六、压缩列表

6.1 场景

压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,且每个列表要么是小整数值,要么就是长度比较短的字符串,此时则采用压缩列表作为列表键的实现。同理,当哈希建只包含少量键值对,且每个键值对的键和值要么是小整数值,要么是长度比较短的字符串,此时采用压缩列表作为哈希建的实现

6.2 结构描述以及示意图

压缩链表的结构如图所示: image.png 每一个entry节点的结构如图所示:

image.png