Redis学习笔记 -《Redis设计与实现》
数据结构
SDS
SDS 是Redis构建的一种名为简单动态字符串的抽象类型,并且被Redis用作默认字符串表示
SDS定义
struct sdshdr {
// 记录buf数组中已使用字节的数量
// 等于SDS所保存 字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS 遵循C字符串以空字符结尾的习惯,保存空字符的1字节空间不计算在SDS的len属性中。
好处:可以直接重用C字符串函数库里面的函数
SDS与C字符串的区别
- 获取字符串长度的复杂度
- C字符串:O(N),C字符串不记录自身长度,程序必须遍历字符串,直到遇到空字符为止
- SDS:O(1),SDS在len属性中记录SDS本身的长度
- 缓冲区溢出
- 当程序在两个紧邻的字符串上执行字符串拼接而没有事先对被拼接的字符串分配足够的空间时,就会造成数据溢出
- 当SDS API对SDS进行修改时,API先检查SDS的空间是否满足修改所需的要求,不满足则自动将空间扩展至执行修改所需的大小,再进行修改
- 减少修改字符串时带来的内存重分配次数
- C字符串在每次执行修改时都会对空间进行重分配
- SDS 通过未使用空间解除字符串长度和底层数组长度之间的关联:SDS中buf数组的长度不一定是字符数量加一,数组里面还可以包含未使用的字节。
- 空间预分配
- 惰性空间释放
- 二进制安全
- C字符串必须符合某种编码,并且只能在末尾有空字符,所以它只能用来保存文本数据。
- SDS通过len来判断字符串是否结束,而不是空字符,因此他可以保存任意格式的二进制数据
空间预分配:用于优化SDS增长操作。当对SDS进行扩展修改时,不仅会分配必须的空间,还会分配额外的未使用空间。
额外空间长度:
- SDS修改后,长度将小于1MB,额外空间大小等于len大小(len == free)
- SDS修改后,长度大于等于1MB,额外空间大小等于1MB
惰性空间释放:用于优化SDS缩短操作。当对SDS进行缩短操作时,程序不会立即重分配内存,而使用free属性记录字节数量,等待再次使用
SDS 提供API在真正需要时释放SDS的未使用空间
链表
链表提供高效的节点重排能力,以及顺序行的节点访问,并且可以通过增删节点来领货的调整链表的长度
- Redis 使用链表作为列表键的底层实现之一(当列表键包含元素较多,或者元素都是较长的字符串时)
- Redis 在发布与订阅、慢查询、监视器等功能也用到链表
链表和链表节点
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
多个链表可以通过prev
和next
组成双向链表
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup) (void *ptr);
// 节点释放函数
void (*free) (void *ptr);
// 节点对比函数
int (*match) (void *ptr,void *key);
} list;
Redis 链表实现的特性
- 双端:链表节点带有
prev
和next
指针,获取前后节点的复杂度都是O(1) - 无环:表头节点的
prev
指针和表尾节点的next
指针都指向NULL
- 带表头和表尾指针
- 带链表长度计数器
- 多态:链表节点使用vlid*指针来保存节点值,并且可可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值
字典
跳跃表
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针达到快速访问节点的目的。
- 跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找
- 在大部分情况下,跳跃表的效率可以和平衡树相媲美,且实现比平衡树简单
- Redis 使用跳跃表作为有序集合健的底层实现之一(当有序集合包含的元素数量较多,或是有序集合中的元素对象是比较长的字符串时)
Redis 只在两个地方用到跳跃表
- 有序集合
- 集群节点中用作内部数据结构
跳跃表由zskiplistnode
和 zskiplist
两个结构定义
跳跃表节点
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
- 层
跳跃表的层可以包含多个元素,每个元素都包含一个指向其他节点的指针。一般情况下,层数越多,访问其他节点的速度越快。每次创建跳跃表节点的时候,程序都根据幂次定律随机生成一个介于1-32的值作为层的大小,也即最大层数为level[31]. - 前进指针
每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。 - 跨度
层的跨度用于记录两个节点之间的距离。
- 两个节点之间的跨度越大,表示他们距离越远
- 所有指向
NULL
的前进指针,他们的跨度都为0
- 后退指针
后退指针用于从表尾向表头方向访问节点。每个节点都只有一个后拖指针,所以每次只能后退至前一个节点。 - 分值和成员
- 分值是一个double类型的浮点数,用于跳跃表中所有节点的排序(从小到大)
- 成员对象是一个指针,指向一个字符串对象,字符串对象保存着一个SDS值
同一个跳跃表中,各节点保存的成员对象必须唯一,不同节点的分值可以相同。当分值相同时,按照成员对象在字典序中的大小进行排序,较小的靠近表头方向
跳跃表
typedef struct zskiplist {
// 表头节点和表尾节点
structz skiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中最大的层数值
int level;
} zskiplist;
header 和 tail 分别指向表头和表尾节点,程序定位表头和表尾节点的复杂度为O(1).
通过length属性记录节点数量,获取长度的复杂度为O(1)
整数集合
Redis 使用整数集合作为集合健的底层实现之一(当一个集合只包含整数值元素,并且集合的元素不多时)
实现
整数集合在 Redis 中用于保存 int16_t
、int32_t
、int64_t
类型的整数值,并且不会出现重复。
typedef struct intest {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
- contents 数组是整数集合的底层实现
- 整数集合中每个元素都是数组中的元素
- 数组中的元素从小到大排序
- 数组中不包含重复项
- length记录整数集合包含的元素数量 -- contents长度
- encoding 决定 contents 数组中真正存储的数据类型
INTSET_ENC_INT16
数组中存储的是int16_t类型的值(-32768~32767, (-2^{15} — 2^{15}-1) )INTSET_ENC_INT32
数组中存储的是int32_t类型的值((-2^{31} - 2^{31}-1))INTSET_ENC_INT64
数组中存储的是int64_t类型的值((-2^{63} - 2^{63}-1))
升级
在向整数集合中添加元素时,如果新元素比集合中所有元素的类型都要长时,就需要对整数集合进行升级。
升级步骤
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 将底层所有元素都转换成与新元素相同的类型,同时将转换类型后的元素放到正确的位置上
- 将新元素添加到数组中
升级策略提升整数集合的灵活性,并且尽可能地节约内存
- 灵活性:C语言中,不会将两个不同类型的值放在同一个数据结构中,但是整数集合可以通过升级来存放不同类型的数据
- 节约内存: 为了让集合能够同时存放三种类型的数据,最简单的方法就是用
int64_t
类型来作为集合的实现,但是存放在数组中的数据不一定包含有int64_t
类型的数据,这样就造成内存的浪费,而通过升级就可以在需要的时候分配内存。
降级
整数集合不支持降级,一旦对数组进行了升级,编码就会一直保持升级后的状态
压缩列表
Redis 使用压缩列表作为列表键和哈希键的底层实现之一(列表键只包含少量列表项,且每项都是小整数值或短字符串;哈希键只包含少量键值对,且键和值都是小整数或者短字符串)
结构
压缩列表是一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩列表可以包含任意节点,每个节点保存一个字节数组或整数值
zlbytes | zltail | zllen | entry1 | ... | entryN | zlend |
---|
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用字节数:在对压缩列表进行内存重分配或者计算zlend位置时使用 |
zltail | uint32_t | 4字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无需遍历列表即可确认表尾节点地址 |
zllen | uint16_t | 2字节 | 压缩列表节点数量,当这个属性值小于UINT16_MAX (65535)时,节点的数量等于属性值,当属性值大于等于UINT16_MAX 时,节点数量需要遍历列表 |
entryX | 列表节点 | 不定 | 列表节点 |
zlend | uint8_t | 1字节 | 特殊值0xFF,标记列表结尾 |
列表节点结构
previous_entry_length | encoding | content |
---|
节点可以保存字节数组及整数值
- 字节数组
- 长度小于等于((2^6-1))字节的字节数组
- 长度小于等于((2^{14}-1))字节的字节数组
- 长度小于等于((2^{32}-1))字节的字节数组
- 整数值
- 4位长,介于0~12的无符号整数
- 1字节长的有符号整数
- 3字节长的有符号整数
- int16_t类型整数
- int32_t类型整数
- int64_t类型整数
previous_entry_length
此属性记录列表前一个节点的长度,长度可以是1或5(字节)
- 前一节点的长度小于254字节,则属性长度为1字节
- 前一节点的长度大于等于254字节,则属性长度为5字节,切属性的第一个字节为0xFE ,后面四个字节保存前一节点的长度
程序可以根据此属性倒序变了列表。
encoding
此属性不仅保存content 属性的类型,还保存了content 的长度
编码 | 编码长度 | content属性保存的值 |
---|---|---|
00xxxxxx | 1字节 | 长度小于等于63字节的字节数组 |
01xxxxxx xxxxxxxxx | 2字节 | 长度小于等于16383字节的字节数组 |
10_ _ _ _ _ _ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx |
5字节 | 长度小于等于((2^{32}-1))的字节数组 |
11000000 | 1字节 | int16_t类型的整数 |
11010000 | 1字节 | int32_t类型的整数 |
11100000 | 1字节 | int64_t类型的整数 |
11110000 | 1字节 | 24位有符号整数 |
11111110 | 1字节 | 8位有符号整数 |
1111xxxx | 1字节 | 没有content属性,编码本省保存0~12之间的值 |
_ 表示留空,x表示二进制数
content
此属性保存节点的值(字节数组或整数)
连续更新
Redis 将在特殊情况下产生的连续多次空间扩展操作称为连锁更新
连续更新既可以发生在添加节点时,也可能发生在删除节点时
连续更新发生的条件
- 压缩列表恰好有多个连续的、长度介于250~253字节的节点
总结
- 当列表键包含元素较多,或者元素都是较长的字符串时 ----链表
- 集合只包含整数值,且元素不多 ----整数集合
- 列表键包含少量列表项,且列表项时小整数或短字符串 ----压缩列表
- 哈希键包含少量哈希键,且键和值都是小整数或者短字符串 ----压缩列表