【Redis】数据结构:SDS、list、ziplist、quicklist

156 阅读5分钟

键值对数据库的实现

  • Key:字符串
  • Value:对应数据结构对象

How?

哈希表:数组+拉链发

  • 扩容时,转移到第二个数组

  • redisDb:Redis 数据库的结构,指向了 dict 结构的指针;

  • dict:存放 2 个哈希表,「哈希表2」只有在 rehash 的时候才用;

  • ditctht:哈希表的结构,存放哈希表数组,每个元素都指向一个哈希表节点结构(dictEntry);

  • dictEntry:哈希表节点的结构,结构里存放了 void key 和 void value 指针

    • key 指向 String 对象
    • value redis对象结构,例如String对象、List 对象、Hash 对象、Set 对象、Zset 等对象
  • redisObject:redis对象结构

    • type:对象的类型(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);

    • encoding:对象使用的底层的数据结构;

    • ptr:指向底层数据结构的指针。

SDS

Why?

C语言字符串是char数组,\0结尾:

  • 求len复杂度O(n)
  • 字符串里不能有\0,只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据
  • 函数不安全:两个字符串拼接有可能缓冲区溢出

How?

  • len:字符串长度。

  • alloc:分配给字符数组的空间长度。 alloc - len 计算剩余空间大小,提前扩容防止缓冲区溢出。

    • sds 长度小于 1 MB,翻倍扩容
    • 大于1MB,扩容到newlen + 1MB
  • flags:表示不同类型的 SDS

    • sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。

      • 对应不同长度的len,量身定制头大小
  • buf[]:字符数组,用来保存实际数据。

  • 特点:O1获得长度、二进制安全、无缓冲区溢出

// __attribute__ ((packed))
// 取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;uint16_t alloc; 
    unsigned char flags; 
    char buf[];
};

双向链表 list(废弃)

  • dup:节点值复制函数
  • free:节点值释放函数
  • match:节点比较函数

缺点:

  • 空间不连续,无法利用好CPU缓存

  • 额外内存开销大

压缩列表 ziplist(废弃)

Why?

list内存不够紧凑,内存浪费,时空效率低。为节约内存开发。

How?

连续内存空间存储一个单向链表,后一个节点记录前一个节点的大小,可以从后往前遍历

头字段:

  • zlbytes:占用对内存字节数,4字节;
  • zltail:「尾部」节点距离起始地址字节数,列表尾的偏移量,4字节;
  • zllen:节点数量,2字节;
  • zlend:结束点,固定值 0xFF(十进制255)

节点字段:

  • prevlen:「前一个节点」的长度,目的是为了实现从后向前遍历;

    • 前一个节点的长度小于 254 字节, prevlen 需要 1 字节;
    • 前一个节点的长度大于等于 254 字节,prevlen 需要 5 字节,第一个字节254固定值为标识,剩下四个字节为长度;
  • encoding:当前节点实际数据的「类型和data长度」,类型主要有两种:字符串和整数。

    • 整数,使用 1 字节的空间
    • 字符串,使用 1 字节/2字节/5字节的空间进行编码
  • data:当前节点的实际数据;

连锁更新问题:

表新增或修改某个元素,空间不不够需要重新分配。新插入元素较大,可能会导致后续元素的 prevlen 占用空间都发生变化,引起「连锁更新」,性能下降。

缺点:

  • 不能保存过多的元素,否则查询效率就会降低;

  • 压缩列表新增某个元素或修改某个元素时,如果空间不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续的元素的prevlen占用空间都发生变化,从而引起连锁更新问题,导致每个元素的空间都需要重新分配,造成访问压缩列表性能下降修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。

listpack

Why?

为了完全解决连锁更新的问题。

How?

每个节点不再包含前一个节点的长度

  • 总字节数:4字节

  • 元素数量:2字节

  • 结尾标识:1字节

  • encoding:元素的编码类型,会对不同长度的整数和字符串进行编码;

  • data:实际存放的数据;

  • len:encoding+data的总长度

压缩列表的entry为什么要保存prevlen呢?listpack改成len之后不会影响功能吗?

  • 压缩列表的 entry 保存 prevlen 是为了实现节点从后往前遍历,知道前一个节点的长度,就可以计算前一个节点的偏移量。

  • listpack 一样可以支持从后往前遍历的。详细的算法可以看:github.com/antirez/lis… 里的lpDecodeBacklen函数,lpDecodeBacklen 函数就可以从当前列表项起始位置的指针开始,向左逐个字节解析,得到前一项的 entry-len 值。

quicklist

Why?🤔

listpack在增修改数据多需要整体数据复制移动,效率太低。 解决办法:分成很多部分,只需要复制一小部分

typedef struct quicklist {
    //quicklist的链表头
    quicklistNode *head;      
    //quicklist的链表尾
    quicklistNode *tail; 
    //所有压缩列表中的总元素个数
    unsigned long count;
    //quicklistNodes的个数
    unsigned long len;       
    ...
} quicklist;

typedef struct quicklistNode {
    //前一个quicklistNode
    struct quicklistNode *prev;     
    //下一个quicklistNode
    struct quicklistNode *next;     
    //quicklistNode指向的压缩列表
    unsigned char *zl;              
    //压缩列表的的字节大小
    unsigned int sz;                
    //压缩列表的元素个数
    unsigned int count : 16;        
    ....
} quicklistNode;

  • 添加元素:

    • 检查插入位置的压缩列表是否能容纳该元素

      • 能:保存到 quicklistNode 的压缩列表
      • 不能:新建一个新的 quicklistNode
  • 控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,并没有完全解决。