Redis—源码

144 阅读9分钟

1、基础环境

基于redis3.0版本的源码

// 查看当前对象的类型
type key
// 查看当前key的编码
object encoding key

2、简单动态字符串

Redis默认并未直接使用C字符串(C字符串仅仅作为字符串字面量,用在一些无需对字符串进行修改的地方,如打印日志);而是以struct的形式构造了一个SDS的抽象类型。当Redis需要一个可以被修改的字符串时,就会使用SDS来表示。
在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)

2.1、SDS基础结构

struct sdshdr {
    // 记录bug数组中已使用字节的数量
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组,用户保存字符串
    cha buf[];
}

2.2、SDS特性

  • 可以高效的执行长度计算
  • 可以高效的执行追加操作
  • 二进制安全
  • 杜绝缓存区溢出
  • 兼容部分C字符串函数

2.3、与C字符串的区别

C字符串SDS
获取字符串长度的复杂度为O(n)获取字符串长度的复杂度为O(1)
API是不安全的,可能会造成缓冲区溢出API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次”最多“需要执行N次内存重分配
只能保存文本数据可以保存文本、二进制数据
可以使用所有<string.h>库函数可以使用一部分<string.h>库中的函数

2.4、应用范围

  1. 字符串对象

3、链表

Redis链表为双向无环链表;链表是种非常常见的数据结构,在Redis中使用非常广泛,列表对象的底层实现之一就是链表。其它如慢查询,发布订阅,监视器等功能也用到了链表。

3.1、节点结构

// 文件路径:src/adlist.h
typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的值
    void *value;
} listNode;

image.png

3.2、链表结构

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;

由list结构和listNode结构组成的链表 image.png

3.3、Redis的链表实现的特性

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

3.4、应用范围

  1. 列表对象

4、字典

字典是一种用于保存键值对的抽象数据结构;在字典中,一个key可以和一个value进行关联(或者说将键映射为值),这些关联的键和值就称为键值对。

4.1、字典结构

文件路径:src/dict.h
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;

image.png

4.2、哈希表的结构

文件路径:src/dict.h
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    unsigned long sizemask;
    // 哈希表已有的节点数量
    unsigned long used;
} dictht;

image.png

4.3、哈希表节点值结构

文件路径:src/dict.h
typedef struct dictEntry {
    // 健
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下个哈希表节点,形成链表,主要用来解决哈希冲突问题
    struct dictEntry *next;
} dictEntry;

image.png

4.4、redis如何解决哈希冲突

Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了哈希冲突的问题。

4.5、重新哈希

当哈希冲突的key逐渐增多的时候,哈希表的负载因子也会超出合理的范围,同样会影响字典接口使用的性能,无论新增还是读取哈希值,性能都会随着负载因子增高而下降;
同样随着不断的操作哈希表,保存的哈希键值对数可能过多,也可能变少,程序需要对哈希表的大小进行响应的扩展和缩容; redis的通过执行rehash来操作就能实现扩容、缩容,并保证哈希因子在合理范围内。

4.6、负载因子

负载因子 = 哈希表已保存的节点数量/哈希表的大小
load_factor = used / size

4.7、哈希表满足什么条件会扩容和缩容呢

  • 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
  • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
  • 当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作

4.8、应用范围

  1. redis数据库
  2. 哈希对象
  3. 集合对象

5、跳跃表

跳跃表是一个有序数据结构,它通过在每个节点中维护多个指向其他节点的指针,从而达到快速访问节点的目的。

5.1、基本结构

文件路径:src/redis.h

// 跳跃表节点值
typedef struct zskiplistNode {
    // 各节点中所保存的成员对象
    robj *obj;
    // 节点按各自所保存的分值从小到大排列
    double score;
    // 后退指针在程序从表尾向表头遍历时使用
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针用于访问位于表尾方向的其他节点
        struct zskiplistNode *forward;
        // 跨度记录了前进指针所指向节点和当前节点的距离
        unsigned int span;
    } level[];
} zskiplistNode;


// 跳跃表列表
typedef struct zskiplist {
    // 跳跃表的表头节点、表尾节点
    struct zskiplistNode *header, *tail;
    // 记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。
    unsigned long length;
    // 记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
    int level;
} zskiplist;

image.png

5.2、跳跃表的规则

  • 每个跳跃表节点的层高都是1至32之间的随机数。
  • 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
  • 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。

5.3、应用范围

  1. 有序集合【有序结合包含的元素数量比较多或者包含的元素是比较长的字符串】
  2. 集群节点

6、整数集合

整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。

6.1、整数结合基本结构

typedef struct intset {
    // 编码方式 编码方式决定contents数组存储的类型
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;

image.png

6.2、整数集合升级

当一个新元素插入到整数结合中时,当前整数集合的编码不满足新插入的值,会引发整数集合升级,然后才能将新会员数添加到整数集合里面;

整体流程如下

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。

6.3、整数集合使用的规则

  • 整数集合只支持升级操作,不支持降级操作。
  • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存。

6.4、应用范围

  1. 集合对象【只包含整数值元素,并且这个集合的元素数量不多时】

7、压缩列表

压缩列表是redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构;一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

7.1、压缩列表结构

image.png

7.2、压缩列表节点值

previous_entry_length: 以字节为单位,记录了压缩列表中前一个节点的长度。 previous_entry_length属性的长度可以是1字节或者5字节;

encoding:属性记录了节点的content属性所保存数据的类型以及长度;

content:属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。 image.png

7.3、应用范围

  1. 列表对象【列表中仅包含少量列表项,并且每个列表项要么是小整数,要么是比较短的字符串】
  2. 哈希对象【哈希仅包含少量键值对,并且每个键值对的键和值要么是小整数,要么就是长度比较短的字符串】

参考

Redis设计与实现