探究Redis中的那些高效率的数据结构

要问redis为什么这么快,那和它底层高效率的数据结构脱不了关系。

1、SDS简单动态字符串

源码如下所示

c 复制代码
struct sdshdr{
    //记录buf数组中已使用的字节的数量,也是SDS所保存字符串的长度
    int len;
    
    //记录buf数组中未使用字节的数量
    int free;
    
    //字节数组,用于保存字符串
    char buf[];
}

为什么redis不直接使用C语言的字符串?因为C语言自带的字符串以'\0'作为结束符,只能保存文本数据,而SDS根据len这个字段来判断字符串是否结束的,可以保存图片、视频这样的二进制数据。

SDS可以直接记录了当前字符串的长度,获取字符串长度为时间复杂度O(1),而C语言的strlen()函数为O(n)

空间预分配:在扩展时会额外扩展一部分空间,而不仅仅是扩展至所需大小,防止频繁扩展

  • 若SDS的长度小于1M,那么分配和len属性同样大小的未使用空间
  • 若大于1M,那么分配1M大小的未使用空间

惰性空间释放:

在缩短SDS保存到字符数组时,不用立即回收所需内存,而是记录在free属性里,用来以后使用

2、链表

链表节点listNode

c 复制代码
typedef struct listNode{
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点值
    void *value;
}listNode;

链表头节点

c 复制代码
typedef struct list{
    //表头结点
    listNode *head;
    //表尾节点
    listNode *tail;
    //链表所包含的节点数量
    unsigned long len;
    //节点值赋值函数,用于复制节点所保存的值
    void *(*dup)(void *ptr);
    //节点值释放函数,用于释放链表节点所保存的值
    void *(*free)(void *ptr);
    //节点值对比函数,用于比较链表节点所保存的值和另一个输入值是否相等
    void *(*match)(void *ptr,void *key);
}list;
特性

双端:链表节点带有prev和next指针

无环:表头结点的prev和表尾节点的next都指向NULL,对链表的访问以NULL为终点。

带表头和表尾节点:获取表头和表尾结点的时间复杂度为O(1);

多态:用void* 保存节点值,可以保存各种不同的值。

3、字典

字典dict

c 复制代码
typedef struct dict{
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表,ht[0]用作哈希表,ht[1]只在rehash期间使用,在rehash开始时分配内存
    dictht ht[2];
    //rehash索引
    //当rehash不在进行时,值为-1;
    int trehashidx;
}dict;

哈希表dictht

c 复制代码
typedef struct dictht{
    //哈希表数组,指向一个dictEntry结构数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值,总是等于size-1
    unsigned long sizemask;
    //该hash表已有节点数量
    unsigned long used;
}dictht;

哈希表节点dictEntry

c 复制代码
typedef struct dictEntry{
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    }v;
    //指向下个哈希表节点,形成链表,用来解决hash冲突
    struct dectEntry *next;
}dictEntry;
rehash
扩展与收缩条件
  • 扩展条件

    • 服务器目前没有在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1;
    • 服务器目前正在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
    • 扩容大小为第一个大于等于已使用节点*2的2的n次幂大小
  • 收缩条件

    • 当哈希表负载因子小于0.1时开始执行收缩操作
    • 收缩大小为第一个大于等于已使用节点的2的n次幂大小
渐进式rehash
  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设为0,标识rehash工作正式开始
  3. 在rehash期间,每次对字典执行增删改查时,都会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash完成后,rehashidx的值增一
  4. 随着操作的不断执行,ht[0]的所有键值对操作全部迁移完成,将rehashidx变为-1,并将ht[1]设为ht[0];

判断迁移完成

  • Redis 使用一个叫 trehashidx 的索引来记录当前迁移的桶
  • trehashidx 小于 ht[0].size 时,说明迁移还在进行中
  • trehashidx >= ht[0].size 时,意味着迁移已完成

注: 在rehash期间,删除、更新、查找会在两个哈希表上进行,现在ht[0]查找,ht[0]没有再去ht[1]查找。添加操作只会在ht[1]时进行。

4、 跳跃表

跳跃表节点zskiplistNode

c 复制代码
typedef struct zskiplistNode{
    //后退指针
    struct zskiplistNode *backward;
    //分值,列表中的所有节点都按分值大小从小到大排序
    double score;
    //成员对象,指向一个字符串对象
    robj *obj;
    //层
    struct zskiplistLevel{
        //前进指针
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    }level[];
}zskiplistNode;

zskiplist

c 复制代码
typedef struct zskiplist {
    // 跳表头节点(虚拟节点,无实际数据)
    struct zskiplistNode *header; 
    // 跳表中元素的数量
    unsigned long length;
    // 最后一个节点
    struct zskiplistNode *tail; 
     // 当前最高层数
    int level;                  
} zskiplist;
层数生成

生成一个节点时,该节点的层数是随机生成的,最大为32,层数越高概率越小。

5、 压缩列表

c 复制代码
typedef struct {
    unsigned char *zl;        // 指向列表字节数组
    unsigned long long zlbytes;  // 结构体总字节数
    unsigned int zltail;         // 结构体中尾部元素偏移
    unsigned int zllen;          // 元素数量(可能会用特殊值表示)
    unsigned int zlend;          // 结构体末尾偏移(通常为zlbytes-1)
} dictZiplist;

压缩列表节点构成

previous_entry_length encoding content

previous_entry_length:以字节为单位,记录前一个节点的长度

  • 若前一个节点长度小于254字节,那么previous_entry_length用1字节空间记录
  • 若前一个节点长度大于等于254字节,那么previous_entry_length用5字节空间记录

encoding:记录了节点的content属性所保存数据的类型和长度,最高两位作为类型标志位,其他位表示长度

content:记录节点所保存的值

连锁更新

指的是因为前一个节点的更新引起长度变化,而导致的后续节点的连续更新。

例:在一个压缩列表中保存着多个连续的、长度介于250字节到253字节之间的节点e1至eN,此时每个节点的previous_entry_length都占用1字节,若这时某处因为变更(更新、插入或删除)出现在一个长度大于等于254的节点,那么该处的后一个节点为了记录这个长节点,自身的previous_entry_length就要进行扩容,扩容之后自身的长度就要大于等于254字节,后续的节点也需要像这个节点一样进行扩容,导致连锁更新。

6、quicklist

quicklist

c 复制代码
typedef struct quicklist{
    //链表头
    quicklistNode *head;
    //链表尾
    quicklistNode *tail;
    //所有压缩列表中的元素个数
    unsigned long count;
    //quicklistNoded的个数
    unsigned long len;
}quicklist;

节点quicklistNode

c 复制代码
typedef struct quicklistNode{
    //前一个结点指针
    struct quicklistNode *prev;
    //后一个结点指针
    struct quicklistNode *next;
    //指向的压缩列表
    unsigned char *zl;
    //压缩列表的字节大小
    unsigned int sz;
    //压缩列表的元素个数
    unsigned int count;
}

每个quicklist节点都指向了一个压缩列表,这有效防止连锁更新,即使发生也只是发生在一个quicklistNode 里面。

每个节点的ziplist最大长度:由ZIPLIST_MAX_SIZE参数控制,默认为 1024 字节(即1KB)。可以通过ziplist-max-ziplist-value进行修改配置

当前ziplist空间达到最大限制(如1024字节)后,Redis会创建一个新的节点 ,并将新元素写入到新节点的ziplist中。

7、listpack

数据结构和压缩列表差不多,但每个entry里面存放的len用来记录自身的长度,不再记录前一个节点的长度。

压缩列表节点构成

encoding data len

因为里面只存储自身的长度,所以自身的长度改变也只会影响自身,不会扩散至其他节点。解决了连锁更新问题。

8、整数集合

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

当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis会使用整数集合作为底层实现。元素在数组中有序存放

例如 SADD number 1 3 5 7 9

升级

当添加一个新元素时,新元素的类型比整数集合现有所有元素都要长时,整数集合需要先进行升级,然后才将新元素添加到整数集合中。

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

redis中各个对象的长度不同时所使用的数据结构也是不同的。

对象类型 小元素底层结构 阈值 大元素底层结构
String 整形集合 保存的是整数值 SDS
Hash ziplist 元素个数小于512,且每个元素均大小均小于64字节 字典
List ziplist 元素个数小于512,且每个元素均大小均小于64字节 list(在3.2之后任何情况那个都是quicklist)
Set 整数集合 集合中的元素都是整数且数量小于512个 字典
ZSet 压缩列表 元素个数小于128,且每个元素均大小均小于64字节 跳表+字典

注:Redis 7.0之后ziplist全部被quick list进行取代。

相关推荐
我最厉害。,。1 小时前
接口安全&SOAP&OpenAPI&RESTful&分类特征导入&项目联动检测
后端·restful
AntBlack3 小时前
计算机视觉 : 端午无事 ,图像处理入门案例一文速通
后端·python·计算机视觉
福大大架构师每日一题5 小时前
2025-06-02:最小可整除数位乘积Ⅱ。用go语言,给定一个表示正整数的字符串 num 和一个整数 t。 定义:如果一个整数的每一位都不是 0,则称该整数为
后端
Code_Artist5 小时前
[Mybatis] 因 0 != null and 0 != '' 酿成的事故,害得我又过点啦!
java·后端·mybatis
程序员博博5 小时前
看到这种代码,我直接气到想打人
后端
南雨北斗5 小时前
php 图片压缩函数
后端
L2ncE5 小时前
ES101系列08 | 数据建模和索引重建
java·后端·elasticsearch
还是鼠鼠5 小时前
Maven---配置本地仓库
java·开发语言·后端·maven
无问8175 小时前
SpringBoot:统一功能处理、拦截器、适配器模式
spring boot·后端·适配器模式
一只叫煤球的猫6 小时前
MySQL虚拟列:一个被低估的MySQL特性
数据库·后端·mysql