Redis设计与实现——数据结构与对象

前言

Redis是一个KV数据库,常用于实现缓存,因为基于内存实现,所以速度极快。最近阅读《Redis设计与实现》一书,整理几篇文章,本文介绍Redis数据结构相关内容。

I. 数据结构

我们通常说的Redis支持的数据类型有五种,包括字符串、哈希、列表、集合、有序集合,其实这只是存储的数据类型,底层用于存储数据的数据结构并不是这些,而是动态字符串(SDS)、链表、字典(哈希表)、跳跃表、整数集合、压缩列表。

Redis的每一种数据类型其底层所用的数据结构并不只有一种,往往会在合适的情况下采用合适的数据结构,也是基于性能考虑。对照下面导图,我们可以简单的了解每种数据类型有哪些数据结构实现方式。

Redis设计与实现——数据结构与对象_第1张图片

下面具体介绍Redis中定义的几种数据结构。

动态字符串 SDS

Redis中自定义了一个动态字符串取代了C语言中的字符串作为默认字符串数据结构,凡是可能会变化的字符串Redis都会用SDS作为实现。

用途

  • KV中键和值都会用
  • 缓冲区:AOF缓冲区,客户端状态中的输入缓冲区

定义——ArrayList的感觉:

struct sdshdr {
    int len;    // 实际buf使用长度
    int free;   // 剩余长度
    char buf[]; // 存储字节数据数组
}

优势

  • 记录了长度信息,获取长度 O ( 1 ) O(1) O(1)复杂度

  • 自动扩容机制——缓冲区不会溢出(莫名其妙更改了别的内存空间数据)

  • 空间预分配和惰性空间释放——减少增减字符串时带来的内存重分配次数

  • len 来判断数据结束——不仅仅可以存储字符,其实可以是二进制数据

双端链表 LinkedList

Redis定义的双端链表其实也很正常,就是一个能够双向遍历,含有头尾指针的链表数据结构,当然也就具有链表的优势与劣势——增删快,查询慢。

用途

  • 列表底层实现之一(数量比较多或者元素都是比较长的字符串)
  • 发布订阅、慢查询、监视器等功能
  • Redis保存多个客户端的状态信息
  • 客户端输出缓冲区

节点定义——双向链表节点:

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

链表定义——双端双向链表:

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

优势

  • 双端双向——前后节点查询复杂度 O ( 1 ) O(1) O(1)
  • 无环——首尾指针的指向都是 NULL
  • 长度计数器 len——获取列表元素数量复杂度 O ( 1 ) O(1) O(1)
  • 多态——节点使用 void* 指针保存节点值,可以是多态的

字典 HashTable

字典的实现也是一个Key-Value的映射,也就是Map数据结构,实现方法熟知的就是Java里的 HashMapTreeMap。Redis实现的方式是 HashMap ,也就是利用hash表的方式。

用途

  • Redis数据库的实现,对于数据的增删改查都是基于字典的操作
  • 哈希类型的底层实现之一

哈希节点定义——哈希表中的每个元素:

typedef struct dicEntry {
    void *key;                           // 键
    union {
        void *val;    // 指针
        uint64_t u64; // uint64_t整数
        int64_t s64;  // int64_t整数
    } v;                                // 值有三种形式
    struct dicEntry *next;              // 指向下一个哈希节点,拉链法解决哈希碰撞
} dicEntry;

哈希表定义——存储数据元素:

typedef struct dictht {
    dicEntry **table;       // 哈希数组
    unsigned long size;     // 数组长度,会扩容的
    unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,等于size-1
    unsigned long used;     // 已经使用的数量
} dicht;

字典定义

typedef struct dict {
    dictType *type;   // 保存了一簇用于操作特定类型键值对的函数,Redis会为不同的字典设置不同的dictType
    void *privdata;   // 私有数据。
    dictht ht[2];     // 哈希表数组,两个哈希表,ht[1]专为扩容缩容使用
    long rehashidx;   // 用于记录rehash进度,-1表示结束
} dict;

字典类型定义

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);        // hash函数
    void *(*keyDup)(void *privdata, const void *key); // key复制
    void *(*valDup)(void *privdata, const void *obj); // val复制函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 两个key值比较
    void (*keyDestructor)(void *privdata, void *key); // key的析构函数
    void (*valDestructor)(void *privdata, void *obj); // val的析构函数
} dictType;

特性

关于 HashMap 这种数据结构,其需要关注的几个特性相信都是老生常谈:

  • 哈希碰撞:Redis利用头插拉链法解决碰撞问题(dicEntrynext 指针用于拉链)
  • 扩容缩容机制:
    • Redis为每个字典定义了两个哈希表,0号表是用于存数据的,1号表用于扩容缩容时使用的
    • 扩容量: ≥ h [ 0 ] . u s e d ∗ 2 的 2 n ≥h[0].used * 2的2^n h[0].used22n 缩容量: ≥ h [ 0 ] . u s e d 的 2 n ≥h[0].used的2^n h[0].used2n
    • 分次渐进式的迁移,利用 rehashidx 来记录h[0]迁移h[1]数据的进度,-1表示结束,≥0表示哈希到ht[0]的下标进度
    • 扩容缩容期间,迁移数据的工作平均分摊到每次对字典的增删改查操作中,防止长时间rehash,Redis不响应
  • 扩容缩容时机:
    • 负载因子:哈希表已有节点数量/哈希表大小 (因为哈希碰撞,所以会大于1)
    • 服务器没有在执行 BGSAVEBGREWRITEAOF 命令,且负载因子≥1
    • 或者服务器正在执行 BGSAVEBGREWRITEAOF 命令,但负载因子已经≥5

跳跃表 SkipList

跳跃表是种有序的数据结构,其本质是加速有序链表的插入删除速度,其实链表插入删除如果不是在头尾的话,首先需要定位,定位的复杂度就不是 O ( 1 ) O(1) O(1)了。对于跳跃表,不熟悉的可以参考:以后有面试官问你跳跃表,你就把这篇文章扔给他

用途

  • 实现有序集合类型

跳跃表节点定义

typedef struct zskiplistNode {
    struct zskiplistNode *backward; // 后退指针,也就是支持逆向遍历
    double score;                   // 分值
    robj *obj;                      // 成员对象,也就是跳跃表该节点保存的数据
    
    // 该节点的层数组,表示该节点定义多少层
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 每层中包含前向指针
        unsigned int span;              // 跨度,表示前进几步到下一个节点
    } level[];
}

跳跃表定义

一个跳跃表是由一个个节点顺序连接而成。

typedef struct zskiplist {
    /* 跳跃表的头结点和尾节点,尾节点的存在主要是为了能够快速定位出当前跳跃表的最后一个节点,实现反向遍历 */
    struct zskiplistNode *header, *tail;
    /* 当前跳跃表的长度,保留这个字段的主要目的是可以再O(1)时间内获取跳跃表的长度 */
    unsigned long length; 
    /* 所有节点中level最大值,不包括头结点(头结点的level永远都是最大的值---ZSKIPLIST_MAXLEVEL = 32)。level的值随着跳跃表中节点的插入和删除随时动态调整 */     
    int level;
} zskiplist;

特性

  • 加速了有序链表的增删改查复杂度,都为 O ( l o g 2 n ) O(log_2n) O(log2n)
  • 统计排位有 span 的加速,统计个数有 length 的加速
  • 每个节点的层数都是 1~32 的随机数
  • 分值相同的节点按成员对象的大小进行排序

整数集合 intset

整数集合其实就是用于保存整数数值的有序无重复集合,其实现方式底层就是数组,数组类型则可以是 int16_tint32_tint64_t,数据类型会自动升级。

用途

  • 实现整数数值的集合数据类型

整数集合定义——数组实现的Set:

typedef struct intset {
    uint32_t encoding;   // 编码方式,其实就是标识当前的整数集合类型(有符号16位、32位和64位)
    uint32_t length;     // 集合中元素数量
    int8_t contents[];   // 保存整数数据,从小到大有序排列
} intset;

特性

  • 类型升级
    • 新插入元素如果带来类型升级,那么必然其插入位置为头或者尾
    • 升级时,先分配好需要的内存空间,然后按原数组顺序拷贝数组,最后再插入新元素
    • 有了自动类型升级,我们可以灵活的添加三种类型的集合不受限制。同时节约了内存,按需分配。
  • 类型降级——不支持降级

压缩列表 ZipList

压缩列表的本质是一系列特殊编码的连续内存块组成的顺序性数据结构,其实是Redis为了节约内存而设计的数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值

Redis设计与实现——数据结构与对象_第2张图片

用途

  • 实现列表数据类型
  • 实现哈希数据类型

Entry节点定义

struct entry {
    int<var> prevlen;        // 前一个节点的占用字节长度,prevlen属性的长度可以是1字节或5字节(取决于前一个节点字节长度是否小于254字节),
    int<var> encoding;       // 元素类型编码,保存了数据的类型和长度
    optional byte[] content; // 元素内容,可以是字节数组,或者是整数类型数据
}

压缩列表定义

struct ziplist<T> {
    int32 zlbytes;       // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength;      // 元素个数,当该值等于65535(可能发生溢出)时,需要遍历才能确定真实元素个数
    T[] entries;         // 元素内容列表,挨个紧凑存储
    int8 zlend;          // 标志压缩列表的结束,值恒为0xFF(十进制255)
}

特性

  • 压缩列表逆向遍历——利用 prevlen 属性可以快速的定位前一节点的地址
  • 由于 prevlen 属性的长度依赖前一个节点的具体长度,本身可以是1字节或者5字节。如果其中一个节点的长度更改,引发后继节点的 prevlen 属性从1字节变为5字节,进而又导致后继的后继也需要调整…这就是连锁更新的问题。但其实也无大碍,实际中很难有这种极端的连锁下去的情况。

你可能感兴趣的:(Redis,数据库,数据结构,数据库)