Redis中的数据结构

Redis中的数据结构

为《Redis设计与实现》笔记

SDS动态字符串

Redis使用自定义字符串

struct sdshdr {
    // 字符串长度
    int len;
    // buf数组中剩余未使用字节的数目
    int free;
    char buf[];
}

额外增加了变量len来记录字符串长度,其存在以下几个好处

  1. 能够在常数复杂度下获取字符串长度
  2. 能够存储二进制数据。传统C字符串使用'\0'来判断字符串的结尾,但是这种方法无法用来保存二进制数据,若在字符串中间处出现'\0'则被判定为结束会后面的字符被屏蔽

下面的图中C字符只能能读到"Redis"

请添加图片描述

SDS字符串缓冲区

在传统字符串中,使用strcat等函数时不会考虑分配了多大的内存,容易造成缓冲区溢出
在SDS字符串中,会对字符串长度和缓冲区大小进行检查,若缓冲区大小小于字符串长度则进行扩容,,其火绒规则为:

  • 若长度小于1MB,则每一次扩容为原长度的两倍+1个空字符位('\0'
  • 若长度大于等于1MB,则每次扩容增加1MB

在字符串缩短后,未使用的缓存依然保留,以免未来被使用上

链表

链表节点结构,使用双向节点

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

链表结构

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

其list结构图如下所示

Redis中的数据结构_第1张图片

Redis链表特点:

  1. 双端,使用双向链表
  2. 无环,链表头节点的prev和为节点的next都指向NULL
  3. 带有链表长度计数器,能快速获得链表长度
  4. 多态,使用void*存储节点值,并定义了三个函数指针报对不同值类型的节点进行操作

字典

哈希表结构

哈希表节点

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

哈希表结构

typedef struct dictht {
    // 哈希表数组,为数组指针
    dictEntry **table;
    // 大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,通常位size-1
    unsigned long sizemask;
    // 已使用的节点数
    unsigned long used;
} dictht;

对于相同索引的节点使用单向链表的方式存储,其结构图如下

Redis中的数据结构_第2张图片

字典

字典结构如下

typedef struct dict {
    // 针对哈希表存储类型的操作函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表,这里有两张哈希表,用于进行rehash操作
    dictht ht[2];
    // rehash索引,在未进行rehash时为-1
    int trehashidx;
} dict;

dictType保存了一系列针对指定类型的操作函数,其结构体定义如下:

typedef struct dictType {
    // 计算哈希值的函数
    insigned int (*hashFunction) (const void *key);
    // 复制键的函数
    void *(*keyDup) (void *privadata, const void *key);
    // 复制值的函数
    void *(*valDup) (void *pridata, const void *obj);
    // 对比键的函数
    void *(*keyCompare) (void *pridata, const void *key1, const void *key2);
    // 销毁键的函数
    void *(*keyDistructor) (void *privadata, const void *key);
    // 销毁值的函数
    void *(*valDistructor) (void *pridata, const void *obj);
} dictType;

当有新的键值对插入时,调用dict->type->hashFunction()方法得到哈希值索引,并通过该值添加到dict->ht[0]

rehash

当哈希表中的数据过多或者过小,需要通过rehash操作重塑哈希表的结构,这个时候就对dict->ht[1]进行操作,其扩展和缩小规则如下:

  • 当服务器未执行BGSAVEBGREWRITEAOF时,哈希表的负载因子>=1则进行rehash
  • 当服务器正在执行BGSAVEBGREWRITEAOF时,哈希表的负载因子>=5则进行rehash
  • 负载因子计算公式:
load_factor = ht[0].used / ht[0].size()
  • 当哈希表的负载因子<0.1时执行缩小操作
  • rehash操作中,ht[1]的大小为第一个大于ht[0].used*2的2的n次幂

渐进式rehash

当哈希表过大时,无法在短时间内一次性完成rehash,所以redis使用多次的渐进式的rehash操作,其操作步骤如下:

  1. ht[1]分配空间,同时使用ht[0]ht[1]两张表
  2. rehashidx设置为0,表示rehash开始进行
  3. rehash开始后,在对字典执行增删改查的同时,顺带将ht[0]中的数据搬运到ht[1]中,每搬运一次rehashidx的值+1
  4. 当所有的数据都从ht[0]搬运到ht[1]后,rehashidx设置为-1,表示操作已完成
  5. 在rehash期间,新的键值对会被保存到ht[1]中,查找优先在ht[0]中进行,找不到则在ht[1]中继续查找

跳表

在Redis中,跳表只在两个地方有使用,一是用于实现有序集合键,二是在集群节点中用作内部数据结构

跳表节点

Redis中,节点包含层信息,即每一层中的信息都包含在同一个节点之中,以数组的形式进行保存,而不是每一层中各包含多少个节点

typedef struct zskiplistNode {
    // 前向指针,指向前一个节点
    struct zskiplistNode *backward;
    // 分值,节点按照分值大小来排列
    double score;
    // 成员对象,为一个指针,指向一个字符串对象
    robj *obj;

    // 层
    struct zskiplistLevel {
        // 后向节点
        struct zskiplistNode *forward;
        // 跨度,记录该层中两个节点之间的距离
        unsinged int span;
    } level[];
} szkiplistNode;

跳表结构

typedef struct zskiplist {
    //表头节点和表尾节点
    struct skiplistNode *header, *tail;
    // 节点数目
    unsinged long length;
    // 层数
    int level;
} zskiplist;

Redis中的数据结构_第3张图片

整数集合

Redis中,若一个集合只包含整数值元素,并且元素数量不多时,就会使用整数集合作为集合键的底层

整数集合结构体为

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

对于encoding,有:

  1. 如果其值为INTSET_ENC_INT16,那么contents就是一个int16_t类型的数组
  2. 如果其值为INTSET_ENC_INT32,那么contents就是一个int32_t类型的数组
  3. 如果其值为INTSET_ENC_INT64,那么contents就是一个int64_t类型的数组

content虽然定义的是int8_t类型的数组,但是却不存储int_8类型的数值

升级

当新添加的元素比集合中存储的元素对应的类型所占的空间要大时,需要先对整数集合进行升级,其分为三步:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并未新元素分配空间
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素防止到正确的位置上,并在放置元素的过程中,位置底层数组的有序性质不变
  3. 将新元素添加到底层数组中

使用升级策略而不是直接统一使用int64_t类型数据是为了尽可能的节省存储空间

压缩列表

Redis中,压缩列表是列表键和哈希键的底层实现之一

当一个哈希键只包含少量键值对,并且每个键值对的键和值要么时小整数值,要么是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现

压缩列表结构

一个压缩列表包含多个节点(entry),每个节点保存一个字节数组或者整数值,其结构图如下:

请添加图片描述

其各部分的含义为:

属性 类型 含义
zlbytes uint32_t 压缩列表占用的字节数
zltail uint32_t 偏移量,记录列表尾节点距离起始节点有多少个字节,起始地址加上偏移量为表尾节点地址的取值
zllen uint16_T 压缩列表锁包含的节点数量
entry 列表节点
zlend uint8_t 用于标记压缩列表的结尾,值为0xFF

节点

节点用于保存字节数组或者整数值,其包含的属性和含义如下:

属性 含义
previous_entry_length 记录前一个节点的长度,以字节为单位
encoding content属性所保存的数据对应的数据类型
content 节点的值

previous_entry_length的长度可以是1字节或者5字节:

  • 当前一个节点长度小于254字节时,previous_entry_length长度为1字节,保存前一节点的长度
  • 当前一节点的长度大于254字节时,previous_entry_length长度为5字节,第一个字节值为0xFE,只有四个字节用于保存前一个节点的长度

对象

Redis没有直接使用前面所说的数据结构来实现数据库,而是使用这些数据结构来构建出一个对象系统,每个对象系统包含至少一个数据结构。
当用户在Redis中创建了一个键值对时,会响应地生成一个键对象和一个值对象。

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构地指针
    void *ptr;
    // ...
} robj;

type

对象中type决定了该对象的类型,取值情况如下

类型常量 对应的类型
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

encoding

对象中encoding属性表示该对象的底层实现,其取值情况如下

编码常量 对应的数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳表和字典

不同对象的底层实现可能如下:

对象类型 底层实现
REDIS_STRING REDIS_ENCODING_INT,REDIS_ENCODING_EMBSTR,REDIS_ENCODING_RAW
REDIS_LIST REDIS_ENCODING_LINKEDLIST,REDIS_ENCODING_ZIPLIST
REDIS_HASH REDIS_ENCODING_HT,REDIS_ENCODING_ZIPLIST
REDIS_SET REDIS_ENCODING_HT,REDIS_ENCODING_INTSET
REDIS_ZSET REDIS_ENCODING_ZIPLIST,REDIS_ENCODING_SKIPLIST

字符串对象

字符串对象的编码方式可以是intraw或者embstr,其条件如下:

编码
可以用long类型保存的整数 int
可以用long double类型保存的浮点数 embstr或raw
字符串数值,或者长度太大的整数或浮点数 embstr或raw

rawembstr两种类型均为sdshdr字符串实现,不同点在于raw两次分配内存,而embstr值分配一次内存,RedisObjectsdshdr内存在相邻位置,两者的结构图如下

Redis中的数据结构_第4张图片
Redis中的数据结构_第5张图片

使用embstr有以下好处:

  1. 只需要分配一次内存,同时页只需要释放一次内存
  2. 所有数据都放在一块连续的内存里,能够更好的利用缓存带来的优势

在实际使用中,只有最开始使用时可能为embstr类型,rawint类型不会转换为embstr类型,所以相当于embstr类型仅为只读类型,当进行修改后会转化为raw类型

列表对象

列表对象的编码可以是ziblist或者linkedlist,当满足以下条件时使用ziplist

  • 列表中中保存的所有字符串元素的长度都小于64字节
  • 列表中保存的元素小于512个

哈希对象

哈希对象的编码可以是ziplist或者hashtable,使用ziplist存储时键和值紧挨在一起,为相邻的两个entry
哈希对象编码的选择方式同列表对象

集合对象

集合对象的编码和意识intset或者hashtable,当满足以下条件时使用intset进行存储:

  • 集合对象保存的所有元素均为整数值
  • 集合对象保存的元素数量不超过512个

有序集合对象

有序集合对象的编码可以是ziplist或者skiplist+hashtable
为了能够高效实现各种操作,有序集合对象同时使用skiplisthashtable实现

当满足以下条件按时,有序集合对象使用ziplist

  • 保存的元素不超过128个
  • 所有元素的长度都小于64字节

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