键值对数据库的实现
- 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 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,并没有完全解决。