青春因磨砺而出彩,人生因奋斗而升华
SDS是什么
simple dynamic string; 简单的动态字符串,redis自己构建的数据结构;遵循以空字符串结尾的惯例,保存空字符的1byte空间,以及添加空字符到字符串末尾等操作都是由sds函数自动完成的,所以这个空字符对于sds的使用者来说完全透明的。遵循空字符结尾的惯例,是可以重用一部分c字符串函数库里面的函数;
特点
- 可动态扩展内存。
- 二进制安全
SDS结构
sds有5中数据结构,是为了让不同长度的字符串可以使用大小不同的header,这样短字符串就能使用较小的header,从而节省内存
sds结构由两部分组成:
- header:通常包含字符串长度、最大容量、flags
- 字符数组:这个字符数组的容量等于最大容量+1;真正有效的字符数据,其长度通常小于最大容量。因为在真正的字符串数据之后,是空余未使用的字节,允许在不重新分配内存的前提下让字符串数据向后做有限的扩展。而且还有一个空字符
五种结构
struct __attribute__((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
}
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
}
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
}
struct __attribute__ ((__packed__)) sdshdr32 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
}
struct __attribute__ ((__packed__)) sdshdr64 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
}
从以上数据结构来看,除了sds5外,其他4个header的结构都包含3个字段:
- len:字符串真正的长度(不包含空字符)
- alloc:字符串的最大容(不包含空字符)
- flags:动态字符串的header类型;总占用1byte,用低3位表示header类型;
根据源码可知各类型表示的最大容量:
#define SDS_TYPE_5 0 // 最大容量 32(2^5)
#define SDS_TYPE_8 1 // 最大容量 0xff(2^8 - 1)
#define SDS_TYPE_16 2 // 最大容量 0xffff(2^16 - 1)
#define SDS_TYPE_32 3 // 最大容量 oxffffffff(2^32 - 1)
#define SDS_TYPE_64 4 // 最大容量 2^64 - 1
各header注意事项:
-
各header使用了__attribute__ ((packed)),是为了让编译器以紧凑模式来分配内存。如果没有这个属性,编译器可能会为struct的字段做优化对齐,在其中填充空字节。那样就不能保证header和sds数据部分前后相邻,也不能按照固定向低地址方向偏移1字节方式来获取flags字段
-
在各个header定义中最后一个buf,是一个没有指明长度的字符数组,是c语言中定义字符数组的一种特殊写法,称为柔性数组,只能定义在一个结构体的最后一个字段上。在这里只起到一个标记作用,表示在flags后面就是一个字符是数组,程序在为header分配内存时,buf并不占用内存空间;eg: sizeof(struct sdshdr16)的值,结果5byte,其中没有buf字段;
-
sdshdr5与其它几个header结构不同,它不包含alloc字段,而长度使用flags的高5bit来存储。因此,它不能为字符串分配空余空间。如果字符串需要动态增长,那么它就必然要重新分配内存才行。所以这种类型的sds字符串更适合存储静态段字符串;
实例结构图
sds的header,隐藏在真正字符串前面定义的好处:
- header和数据相邻,而不用分成两块内存空间单独分配。有利于减少内存碎片,提高存储效率;
空间预分配:
redis可以减少连续执行字符串增长操作所需的内存重分配次数,用于优化SDS字符串增长操作,当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所需的空间,还会额外为SDS分配未使用空间;
-
如果对SDS修改后,SDS长度(len属性) < 1M,那么程序分配和len同样大小的空余空间
-
如果 > 1M,那么多分配1M的空余空间;
例子: 如果进行修改之后,SDS的len变成13字节 < 1M,那么程序也会分配13字节未使用空间,SDS的buf数组实际长度将变成13+13+1=27字节(额外的保存空字符)如果对SDS进行修改之后,SDS的长度 >= 1M, 那么程序会分配1M的未使用空间。 eg: 如果进行修改之后,SDS的len变成30M,那么程序会分配1M的未使用空间,SDS的buf实际长度就是 30M + 1M + 1byte
惰性空间释放
用于优化SDS的字符串缩短操作;当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出的字节,而是将这些字节数量记录起来,并等待将来使用。
思考
sds与c字符串的区别
-
获取字符串的复杂度;
c语言使用长度为N+1的字符数组表示长度为N的字符串,并且字符数组的最后一个元素为'\0',由于c字符并不纪录自身长度,所以获取一个字符串长度,程序必须遍历整个字符串,直到遇到空字符为止;复杂度为O(N) sds在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度为O(1),程序只需要访问SDS的len属性,就可以立马知道SDS的长度;确保了获取字符串长度不会称为Redis的性能瓶颈; -
杜绝缓冲区移除
由于C字符串不记录本身长度,易造成缓冲区溢出;如果将字符串内容拼接到另一个字符串末尾,如果另一个字符串分配的内存不足以容纳拼接字符串,就会产生缓冲区溢出; SDS的空间预分配杜绝了这种可能;当SDS API修改字符串时,API会先检查SDS的空间是否满足所需的要求,如果不满足,则进行扩展,才执行实际的修改操作; -
减少修改字符串时,带来的内存重分配次数
由于c字符串不记录自身长度,所以c字符串底层实现总是一个N+1的字符数组。因为c字符串的长度和底层数组的长度之间存在关联性,所以每次增长或缩短一个C字符串,程序都要对保存这个C字符串的数组进行一次内存冲分配; 如 - 如果增长字符串操作,比如拼接字符串,那么执行前需要先通过内存重分配来扩展底层数组空间,如果忘了,造成内存泄漏; - 如果缩短字符串,比如截断字符串,那么执行操作之前,需要先通过内存重分配来释放字符串不再使用的那部分空间,如果忘了就会产生内存泄漏 为了避免这种缺陷,SDS通过未使用空间解除了字符长度与底层数组长度之间的关联,在SDS中,buf数组的长度不一定就是字符数量+1,数组里面可以包含未使用的字节,而这些字节由SDS的free属性记录,通过未使用空间,SDS实现了空间与分配和惰性空间释放两种优化策略 -
兼容部分c字符串函数
虽然SDS是二进制安全,但同样遵循C字符串的空字符结尾,API总会将SDS保存的数据末尾设置空字符,并且总会在为buf分配空间时多分配一个字节容纳空字符,此举为了让保存文本数据的SDS可以重用一部分C字符串的函数
注意
如果创建一个长度为0的空字符,那么不使用SDS_TYPE_5类型的header,而是使用SDS_TYPE_8类型的header;因为创建空字符后一般接下来操作很可能是追加数据,但SDS_TYPE_5类型的sds字符串不适合追加数据(会引发内存重新分配)
# 源码
sds sdsnewlen(const void *init, size_t initlen) { void *sh;
sds s;
char type = sdsReqType(initlen); // 获取flags
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
......