一:SDS
1.简述
C传统字符串:以空字符结尾的字符数组。
在Redis中,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方。对于可以被修改的字符串值,Redis底层使用SDS来表示字符串值。
除了用来保存字符串值之外,SDS还被用做缓冲区(buffer),AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。
2.定义
每个sds.h/sdshdr结构表示一个SDS值(源码)
struct sdshdr{
//记录buf数组中已使用字节的数量
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存
char buf[];
}
3.SDS与C字符串的区别:
根据传统,C语言使用长度为N+1(+1,最后一个空字符)的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符 '\0'。
C语言使用的这种简单的字符串表示方式,并不能满足对字符串存在安全性,效率以及功能方面的要求。
3.1 常数复杂度获取字符串长度
C字符串获取字符串长度需要遍历整个字符串(获取长度的复杂度为O(n)),而SDS本身记录了字符串的长度(获取长度的复杂度为O(1)),确保了获取字符串长度时不会成为redis的性能瓶颈。
3.2 杜绝缓冲区溢出
C字符串因本身不记录字符串长度,所以带来的另一个问题就是容易造成缓冲区溢出(buffer overflow)。即当追加修改了字符串时("xxx" -> “xxxxx”),当没有提前分配足够的空间时直接追加修改,则会使得数据溢出到其它内存空间,导致其它内存空间内容被意外的修改。
SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足需要修改的需求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后再执行实际的修改操作。所以SDS不需要手动的修改SDS的空间大小,避免了缓冲区溢出的问题。
3.3 减少修改字符串时带来的内存重分配次数
因字符串的长度跟底层数组长度之间都存在着这种关联,所以每次追加(append)或者缩短(trim)字符串时,程序总要对这个C字符串的数组进行一次内存重新分配操作(追加时如果忘了内存重新分配,则可能发生缓冲区溢出,缩短时如果不释放不再使用的内存空间,则会产生内存泄露。所以需要每次重新分配)。
因内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它是个比较耗时的操作。所以每次内存重分配会造成性能方面的影响。
为了避免这样的缺陷,在SDS中,buf数组的长度不一定就是字符数量加1,数组里面可以包含未使用的字节,这些字节的数量就由SDS的free属性记录。
通过未使用空间,SDS实现了空间预分配与惰性空间释放两种优化策略。
3.3.1.空间预分配
当对字符串做追加(append)时,不仅会为SDS分配所需要的空间,还会为SDS分配额外的未使用空间。
额外分配的未使用空间由以下公式决定:
当对SDS修改后,SDS的长度(len属性)小于1M时,那么程序分配和len属性同样大小的未使用空间。即len=free。
当对SDS修改后,SDS的长度(len属性)大于等于1M时,那么程序会分配1M的未使用空间。
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。将连续增长N次字符串所需的内存重分配次数从N次降低为最多N次。
3.3.2.惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作:
当SDS需要缩短字符串时,程序并不立即使用 内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待使用。
通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来 可能会字符串增长操作做了优化。
SDS也提供了释放未使用空间对应的API,我们可以在有需要的时候使用,所以不用担心惰性空间释放会造成内存浪费。
3.4 二进制安全
C字符串里不能包含空字符,否则会被认为是字符串结尾,所以使得C字符串只能保存文本数据,而不能保存图片,音频等等的二进制数据。
为了确保Redis可以适用不同的适用场景,SDS的API都是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放buf数组里的数据。程序不会对其中的数据做任何限制,过滤,或者假设,数据在写入时是什么样的,它被读取时就是什么样的。
使用SDS来保存特殊数据格式就没有问题,因为SDS使用len属性的值来判断字符串是否结束,而不是使用空字符来判断。
通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。
4.总结与回顾
Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS(简单动态字符串)作为字符串表示。
比起C字符串,SDS具有以下优点:
1.常数复杂度获取字符串长度。(len属性)
2.杜绝缓冲区溢出。
3.减少修改字符串长度时所需的内存重分配次数。(预分配和惰性空间释放)
4.二进制安全。
5.兼容部分C字符串函数。
二:链表
1.简述
链表被广泛用于实现Redis的各种功能,比如列表键,发布与订阅,慢查询,监视器等。
2.定义
每个链表节点使用一个adlist.h/listNode结构来表示:
typedef struct listNode{
//前置节点
struct listNode *prev
//后置节点
struct listNode *next
//节点的值
void *value
}
3.要点
每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现的是双端链表。
每个链表使用一个list结构来表示,这个结构带有表头节点指针,表尾节点指针,以及链表长度等信息。
因为链表表头节点的前置节点和表尾的后置节点都指向NULL,所以Redis的链表实现是无环链表。
通过为链表设置不同的类型指定函数,Redis的链表可以用于保存各种不同类型的值。
三:跳跃表
1.简述
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的 成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。除此之外,跳跃表在Redis里面没有其他用途。
2.定义
Redis的跳跃表由redis.h/zskiplistNode和redis.h/zkiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zkiplist结构则用于保存跳跃表节点的相关信息(整个跳跃表),比如节点的数量,以及指向表头节点和表尾节点的指针等等。
zskiplist结构:
header:指向跳跃表表头的节点
tail:指向跳跃表表尾的节点
level:记录层数最大的那个节点的层数。(表头节点的层数不计算在内)
length:记录跳跃表的长度(跳跃表目前包含节点的数量,表头节点不计算在内)
zskiplistNode结构:
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
- 层(level):
节点中用1、2、L3等字样标记节点的各个层,L1代表第一层,L代表第二层,以此类推。
每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越远)。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
跳跃表节点的level数据可以包含多个元素,每个元素都包含一个指向其它节点的指针,程序可以通过这些层来加快访问其它节点的速度,一般来说,层的数量越多,访问其它节点的速度就越快。
- 后退(backward)指针:
节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后退指针,因此每次只能后退一个节点。
- 分值(score):
各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
- 成员对象(oj):
各个节点中的o1、o2和o3是节点所保存的成员对象。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。
3.总结:
1.跳跃表是有序集合的底层实现之一。
2.Redis的跳跃表实现由zkiplist和zkiplistNode两个结构组成,其中zkiplist用于保存跳跃表信息(比如表头节点,表尾节点,长度),而zkiplistNode则用于表示跳跃表节点。
3.每个跳跃表节点的层高都是1-32之间的随机数。
4.在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
5.跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。