Redis 五种数据结构

一、SDS 简单动态字符串

Redis 在存储 可能发生改变、非字面量 的字符串时,都会使用 SDS,比如:redis 的 key 和 value 等。

1. 结构体定义
struct sdshdr{
    // 字符串真是长度
    int len;
    // buf 中空闲的长度
    int free;
    // 存储字符串的 char 数组
    char buf[];
}
  • len,字符串真实长度。
  • freebuf 中空闲的长度(与 预分配惰性释放 有关)
  • buf,用于存储字符串的 char 数组。
2. SDS 和 C 字符串的区别
  • SDS 获取字符串长度的时间复杂度是 O(1)C 获取字符串长度的时间复杂度是 0(n)
  • 通过 SDS 的 API 操作字符串(末尾追加、截取等)完全避免了 内存的溢出和泄露,而 C 字符串则需要手动维护内存,存在溢出和泄露风险
  • SDS 减少了 内存分配的次数
    • 内存预分配SDS 通过 alloc = (len <= 1024) ? 2 * len : len + 1024 的方式,预留了一部分内存空间,相比 C 字符串,在 增加字符串长度 的场景上,有效的减少了内存分配的次数。
    • 惰性释放SDS 在释放空间时,并不立即释放空间,而是 在 free 字段中记录了【空闲】空间的长度,等待未来使用。C 字符串则每次都需要手动释放。且 SDS 的 API 也支持像 C 字符串那样的 立即释放
  • 相比 C 字符串,SDS二进制安全的(不会像 C 字符串一样,以为空格是结尾)

二、链表

redis 通过链表,实现了 队列 相关的操作。

1. 结构体
  • 链表节点:listNode(双向)
typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点值
    void *value;
} listNode;
  • 链表:list
typedef stuct list {
    // 头结点
    listNode *head;
    // 尾节点
    listNode *tail;
    // 链表长度
    unsigned long len;
    // 节点复制函数
    void *(*dup)(void *ptr);
    // 节点释放函数
    void (*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr, void *key);
} list;
2. 特点
  • 双向:获取某个节点的前置、后置节点的时间负责度是 0(1)
  • 无环head.prevtail.nextnull
  • 带头、尾指针:获取链表的 表头节点表尾节点 的时间复杂度是 O(1)
3. 部分API
函数 作用 时间复杂度
listLength 获取链表长度 0(1)
listFirst 获取链表头节点 0(1)
listLast 获取链表尾节点 0(1)
listPrevNode 获取指定节点的前置节点 0(1)
listNextNode 获取指定节点的后置节点 0(1)
listNodeValue 获取指定节点的值 0(1)
listAddNodeHead 设置新的头节点 0(1)
listAddNodeTail 设置新的尾节点 0(1)

三、字典

详见:
redis 源码分析(一)HashTable 上篇
redis 源码分析(二)HashTable 下篇

四、跳跃表

redis 的有序结合,当有序集合的 元素数量比较多 或者 元素的值是比较长的字符串 时,redis 使用跳跃表来实现 有序集合

1. 结构体
  • 节点:zskiplistNode
typedef struct zskiplistNode {
    // 前一个节点的指针
    struct zskiplistNode *backword;
    // 当前节点的分值
    double score;
    // 当前节点的值对象
    robj *obj;
    struct zskiplistLevel {
        // 下一个节点的指针
        zskiplistNode *forword;
        // 到下一个节点的跨度
        unsigned int span;
    } level[];
    
} zskiplistNode;
  • 跳跃表:zskiplist
typedef struct zskiplist {
    // 跳跃表头节点
    struct zskiplistNode *head;
    // 跳跃表尾节点
    struct zskiplistNode *tail;
    // 跳跃表的长度
    unsigned long length;
    // 跳跃表中层数最大的节点的层数
    int level;
} zskiplist;
2. 有序集合的 查找
  • 普通的双向链表节点:DoubleLinkedListNode
typedef struct DoubleLinkedListNode {
    // 后一个节点的指针
    struct DoubleLinkedListNode *next;
    // 前一个节点的指针
    struct DoubleLinkedListNode *prev;
    // 当前节点的分值
    double score;
    // 当前节点的值对象
    robj *obj;
} DoubleLinkedListNode;
  • 普通的双向链表节点,查找的逻辑就是遍历,时间复杂度是 O(n)
    10万元素跳跃表.png

如上图,假设一个跳跃表存储了 a1 到 a100001 这 10 万个元素,他们的分数分别是 1.0 到 100001.0

  • 上述跳跃表有 五层
    • 第一层跨度为1,即:1->2->3...
    • 第二层跨度为10,即:1->11->21...
    • 第三层跨度为100,即:1->101->201...
    • 第四层跨度为1000,即:1->1001->2001...
    • 第五层跨度为10000,即:1->10001->20001...
  • 查找 65536 的过程:
    1. 第五层 查找 (8次) :1->10001->20001->30001->40001->50001->60001,看到 70001 时发现:比 65536 大,去 第四层 继续查找。
    2. 第四层 查找 (6次):61001->62001->63001->64001->65001,看到 66001 时发现:比 65536 大,去 第三层 继续查找。
    3. 第三层 查找 (6次):65101->65201->65301->65401->65501,看到 65601 时发现:比 65536 大,去 第二层 继续查找。
    4. 第二层 查找 (4次):65511->65521->65531,看到 65541 时发现:比 65536 大,去 第一层 继续查找。
    5. 第一层 查找 (4次):65532->65533->65534,看到 65535 时发现目标,得到结果。
  • 结论:
    • 跳跃表的节点,将普通双向链表的节点的 next 指针,从1个升级到了多个,以层的方式管理
    • 不同层级的指针,跨度不一样:第一层就是普通双向链表节点的 next 指针。以后的各层,跨度不断增大,以达到 跳过一些节点,进行查找的目的
    • 指针的层数越高,对查找的性能优化越明显,即:时间复杂度从 O(n) 减少到了 O(logN)
3. 其他有序集合操作

其他有序集合操作,像 范围查找(正向、逆向)是否存在 等,其实都是基于 查找 的结果,利用 前置、后置指针 进行的遍历。

五、整数集合

当集合中的元素都是整数时,Redis 会使用 整数集合 作为集合的底层实现

1. 结构体
typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合长度
    uint32_t length;
    // 集合元素
    int8_t contents[];
} intset;

其中,encoding 指明了 contents 中的数据类型:

  • INTSET_ENC_INT16,范围:[-2^15=-32768, 2^15-1=32767]
  • INTSET_ENC_INT32,范围:[-2^31=-2147483648, 2^31-1=2147483647]
  • INTSET_ENC_INT64,范围:[-2^63, 2^63-1]
2. 编码升级

当一个编码为更小范围,如 INTSET_ENC_INT16 ,的整数集合,要添加一个更大范围,如 INTSET_ENC_INT32,的元素时,整体集合的编码方式需要升级。具体思路是:

  • 根据新元素的类型和集合的元素数量,扩展集合空间。
  • 将集合中的其他元素升级,并保证之前的有序顺序。
  • 将新元素添加到集合中(因为范围更大,所以位置一定是:第一个或者最后一个)
3. 升级的好处
  • 集合更加灵活。屏蔽了不同范围整数的存储实现。
  • 更加节省空间。在有需要的时候再进行升级,避免了一开始就使用 INTSET_ENC_INT64 来存储集合元素。
4. 不存在降级

升级后的整数集合,并不会因为,唯一一个更大范围的元素被删除,就进行降级。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容