Redis关键数据结构深度剖析

深度剖析Redis关键数据结构

动态字符串SDS

字符串是Redis中常用的数据结构之一。

C语言字符串的局限

Redis未直接采用C语言原生字符串,原因在于C语言字符串存在以下弊端:

  • 长度获取:需线性时间复杂度(O(n))遍历数组来获取长度。
  • 非二进制安全:以\0作为结束标识,致使字符串无法容纳含\0的二进制数据,像图片、音频等无法存储。
  • 操作不便:不可直接修改,进行拼接等操作时需手动处理内存,存在风险。

SDS详解

Redis构建了简单动态字符串(SDS)来替代C语言字符串。

SDS结构

Redis用C语言实现,SDS是结构体,源码示例:

struct __attribute__ ((packed)) sdshdr8 {  
    uint8_t len; /* buf中已存字符串字节数,不包含结束标记 */  
    uint8_t alloc; /* buf申请的总字节数,不包含结束标记 */  
    unsigned char flags; /* 用于区分不同SDS头类型,控制头大小 */  
    char buf [];  
};

为适配不同存储需求,还有sdshdr16sdshdr32sdshdr64等,数字对应int的位长,sdshdr5已弃用。各成员变量作用如下:

成员变量 描述
len 记录字符串长度
alloc 分配给字符数组的空间长度
flags 标识SDS类型
buf[] 字节数组,存储实际数据

例如,存储字符串“name”的SDS结构示意:

SDS结构示例.png

动态扩容

SDS具动态扩容能力。通过alloc-len计算剩余空间,判断缓冲区是否能满足操作。空间不足时,Redis会自动扩展。扩容规则:若所需长度小于1MB,按翻倍扩容;超过1MB则进行内存预分配,新增长度为原长度加1MB,减少频繁内存分配,优化性能。示例代码:

hisds hi_sdsMakeRoomFor(hisds s, size_t addlen) 
{ 
    ...
    // 若剩余空间足够,直接返回
    if (avail >= addlen) return s; 
    // 获取当前长度
    len = hi_sdslen(s); 
    sh = (char *)s - hi_sdsHdrSize(oldtype); 
    // 计算扩展后至少需的长度
    newlen = (len + addlen); 
    // 根据新长度分配空间
    if (newlen < HI_SDS_MAX_PREALLOC)
        newlen *= 2; 
    else
        newlen += HI_SDS_MAX_PREALLOC; 
        ... 
}

SDS解决了C字符串的问题:获取长度通过len实现(O(1))时间复杂度;二进制安全靠遍历len个字符;支持动态扩容,减少内存分配次数。

IntSet

IntSet是Redis中set集合的一种实现,基于整数数组,具长度可变、有序等特性。

结构

typedef struct intset {
    uint32_t encoding;/* 编码方式,支持16位、32位、64位整数 */
    uint32_t length;/* 元素个数 */
    int8_t contents[];/* 整数数组,存储集合数据 */
} intset;

其中encoding有三种模式,对应不同整数大小:

/* 编码有序,INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64 */
#define INTSET_ENC_INT16(sizeof(int16_t))/* 2字节整数,类似Java的short */
#define INTSET_ENC_INT32(sizeof(int32_t))/*4字节整数,类似Java的int */
#define INTSET_ENC_INT64(sizeof(int64_t))/* 8字节整数,类似Java的long */

元素按升序存储在contents数组,结构示例:

IntSet结构.png

IntSet升级

以元素为[5,10,20](编码为INTSET_ENC_INT16)为例,添加50000时会升级编码。流程:

  1. 升级编码为INTSET_ENC_INT32,扩容数组,计算所需空间。
  2. 倒序拷贝原数组元素到新位置,避免覆盖。
  3. 插入新元素到数组末尾。
  4. 更新编码和长度属性。

部分源码示例:

intset *intsetAdd(intset *is,int64_t value,uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value); // 获取值编码
    uint32_t pos; // 插入位置
    if(success) *success=1;
    // 若编码超出当前IntSet编码,升级
    if(valenc >intrev32ifbe(is->encoding)) {
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 查找元素位置
        if(intsetSearch(is,value,&pos)) {
            if(success) *success =0; // 找到则不插入
            return is;
        }
        // 扩容数组
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 移动元素腾出空间
        if(pos <intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 插入新元素
    intsetSet(is,pos,value);
    // 更新元素长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    int prepend = value < 0 ? 1 : 0;
    // 更新编码
    is->encoding = intrev32ifbe(newenc);
    // 扩容数组
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* 倒序拷贝元素 */
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* 插入新元素 */
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    // 更新长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

IntSet特点:元素唯一有序、支持类型升级、底层用二分查找。

Dict

Redis键值映射靠Dict实现。

结构

Dict由哈希表(dictht)、哈希节点(dictEntry)、字典(dict)组成。

typedef struct dictht {
    dictEntry **table; // entry数组,元素是指向entry的指针
    unsigned long size; // 哈希表大小(2的幂)
    unsigned long sizemask; // size-1,用于计算索引
    unsigned long used; // entry个数
} dictht;

typedef struct dictEntry {
    void *key; // 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; // 值
    struct dictEntry *next; // 下一个entry指针(解决哈希冲突)
} dictEntry;

typedef struct dict {
    dictType *type; // dict类型,含哈希函数
    void *privdata; // 私有数据,用于特殊哈希运算
    dictht ht[2]; // 两个哈希表,ht[0]存数据,ht[1]用于rehash
    long rehashidx; // rehash进度,-1表示未rehash
    int16_t pauserehash; // rehash暂停标志
} dict;

结构示例:

Dict结构示例.png

添加元素

添加键值对时,先计算key的hash值,通过h & sizemask确定存储索引,哈希冲突用头插法解决。

Dict扩容与收缩

Dict根据负载因子(LoadFactor=used/size)触发扩容或收缩。负载因子≥1且无后台进程时扩容;负载因子<0.1时收缩。扩容大小为第一个≥used+1的2的幂,收缩大小为第一个≥used的2的幂(不小于4)。

渐进式rehash

rehash分多次完成,每次操作时将ht[0]的entry链表rehash到ht[1],最终切换哈希表,确保ht[0]只减不增。

ZipList

ZipList是特殊的连续内存块结构,可双端操作,时间复杂度(O(1))。

结构

属性 类型 长度 用途
zlbytes uint32_t 4字节 记录压缩列表占用内存字节数
zltail uint32_t 4字节 表尾节点距起始地址偏移量
zllen uint16_t 2字节 节点数量,最大值65534
entry 列表节点 不定 存储的节点数据
zlend uint8_t 1字节 标记压缩列表末端

ZipListEntry

节点含前一节点长度、编码、内容。前一节点长度占1或5字节(<254用1字节,≥254用5字节);编码分字符串和整数,字符串以“00”“01”“10”开头,整数以“11”开头。

连锁更新问题

当连续节点长度在250 - 253字节间,插入长度为254字节的数据时,会引发后续节点的连续空间扩展,即连锁更新。

QuickList

为解决ZipList连续内存申请问题,Redis引入QuickList,是节点为ZipList的双端链表。

结构

typedef struct quicklist {
    quicklistNode *head; // 头节点指针
    quicklistNode *tail; // 尾节点指针
    unsigned long count; // 所有ziplist的entry总数
    unsigned long len; // ziplists总数
    int fill : QL_FILL_BITS; // ziplist的entry上限
    unsigned int compress : QL_COMP_BITS; // 首尾不压缩节点数
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

typedef struct quicklistNode {
    quicklistNode *prev; // 前一节点指针
    quicklistNode *next; // 下一节点指针
    unsigned char *zl; // 当前节点的ZipList指针
    unsigned int sz; // 当前节点ZipList字节大小
    unsigned int count : 16; // 当前节点ZipList entry个数
    unsigned int encoding : 2; // 编码方式(ZipList或lzf压缩)
    unsigned int container : 2; // 数据容器类型
    unsigned int recompress : 1; // 是否解压缩
    unsigned int attempted_compress : 1; 
    unsigned int extra : 10; /* 预留字段 */
} quicklistNode;

配置

通过list-max-ziplist-size控制ZipList entry数量,通过list-compress-depth控制节点压缩,首尾不压缩。

SkipList

SkipList是有序链表,节点含分数和值,有多级指针提高查询效率。

结构

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头尾节点指针
    unsigned long length; // 节点数量
    int level; // 最大索引层级
} zskiplist;

typedef struct zskiplistNode {
    sds ele; // 节点值
    double score; // 节点分数,用于排序
    struct zskiplistNode *backward; // 前一节点指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 下一节点指针
        unsigned long span; // 索引跨度
    } level[]; // 多级索引数组
} zskiplistNode;

RedisObject

Redis数据类型的键值封装为RedisObject,含16字节头部。

编码方式

Redis有11种编码方式,常见:

编号 编码方式 说明
0 OBJ_ENCODING_RAW raw编码动态字符串
1 OBJ_ENCODING_INT long类型整数的字符串
2 OBJ_ENCODING_HT hash表(字典dict)
5 OBJ_ENCODING_ZIPLIST 压缩列表
6 OBJ_ENCODING_INTSET 整数集合
7 OBJ_ENCODING_SKIPLIST 跳表
8 OBJ_ENCODING_EMBSTR embstr的动态字符串
9 OBJ_ENCODING_QUICKLIST 快速列表

五种数据类型的编码

  • String:分INT、EMBSTR、RAW编码,根据长度和内容选择。
  • List:3.2前用ZipList和LinkedList,3.2后用QuickList。
  • Set:分INTSET和HT编码,元素为整数且数量少时用INTSET,否则用HT。
  • Zset:分ZipList、HT、SkipList编码,元素少时用ZipList,多时用HT+SkipList。
  • Hash:分ZipList和HT编码,元素少时用ZipList,多时用HT。

文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/12587.html

(0)
LomuLomu
上一篇 2025 年 6 月 19 日
下一篇 2025 年 6 月 19 日

相关推荐

  • 2025年最新PyCharm激活码与永久破解教程(支持2099年)

    前言 本教程适用于JetBrains全家桶软件,包括但不限于PyCharm、IDEA、DataGrip、Goland等开发工具。下面先展示最新PyCharm版本成功破解至2099年的截图证明: 本文将详细介绍如何通过简单步骤永久激活PyCharm开发环境。该方法具有以下优势:- 支持Windows/Mac/Linux全平台- 兼容新旧所有版本- 成功率高达1…

    PyCharm激活码 2025 年 8 月 19 日
    19300
  • 2024 PyCharm最新激活码,PyCharm永久免费激活码2025-01-11 更新

    PyCharm 2024最新激活码 以下是最新的PyCharm激活码,更新时间:2025-01-11 🔑 激活码使用说明 1️⃣ 复制下方激活码 2️⃣ 打开 PyCharm 软件 3️⃣ 在菜单栏中选择 Help -> Register 4️⃣ 选择 Activation Code 5️⃣ 粘贴激活码,点击 Activate ⚠️ 必看!必看! 🔥 获取最…

    2025 年 1 月 11 日
    80100
  • 2025年最新IDEA激活码分享:永久破解IDEA至2099年教程

    JetBrains全家桶通用破解指南(IDEA/PyCharm/DataGrip等) 先给大家展示最新IDEA版本成功破解的截图,有效期直达2099年,完全无忧使用! 下面将用详细的图文步骤,教大家如何将IDEA激活至2099年。这个方法同样适用于旧版本,无论你使用什么操作系统或版本,都能轻松搞定。 第一步:获取IDEA安装包 如果已经安装可跳过此步骤 前往…

    IDEA破解教程 2025 年 8 月 16 日
    28100
  • Java之Spring MVC篇三

    ![](https://pic.it1024doc.com/csdn/202412/1a8934ea1b097949f559a12ad79fd34c.gif)​​​​​​​ **目录** [响应](#响应) [返回静态页面](#返回静态页面) [@RestController 和 @Controller的区别和联系](#@RestController-和-@…

    2024 年 12 月 27 日
    41200
  • 2025年最新PyCharm激活码及永久破解教程(支持2099年)

    JetBrains系列工具(包括PyCharm、IDEA、DataGrip、Goland等)的破解方法其实很简单,下面就以PyCharm为例,手把手教你如何永久激活到2099年! 先看看最新PyCharm版本成功破解的截图,有效期确实延长到了2099年: 这个破解方法适用于:- 所有操作系统(Windows/Mac/Linux)- 任何版本的PyCharm-…

    2025 年 5 月 8 日
    1.0K00

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信