Redis是C编写的,并在C语言基础上构建了动态字符串SDS(simple dynamic string)抽象类型。终极目标是为了实现Redis的高性能。
1. C语言字符串
值为”Redis“的C语言字符串示例 |
---|
C语言用N+1长度的字符数组来表示长度为N的字符串,字符数组最后一位总是“\0”。
C语言用简单的字符数组方式表示字符串的方式不能满足Redis对字符串安全性、效率和功能方面的要求。
2. SDS结构
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
值为”Redis“的SDS示例 |
---|
当前len为5,表示SDS保存了一个5字节长度的字符串,len的值不包括最后一个空字符“\0”。因此SDS的实际大小=free+len+1字节
*为字符串添加空字符是SDS函数自动完成的,遵循C语言空字符串结尾的好处是可以直接重用C语言字符串函数库里面的函数。
那为什么SDS比C字符串更加适用于Redis对性能和空间的要求呢?
3. SDS相较于C语言字符串的优势
1、以常数复杂度获取字符串长度
C语言需要遍历整个字符串,直到遇到空字符串,代表字符串结束的空字符串位置,获取字符串长度的时间复杂度是O(N)。
SDS种len属性直接记录了字符串长度,时间复杂度是O(1),确保了获取字符串长度的工作不会成为Redis的性能瓶颈。
2、杜绝缓冲区溢出
C字符串不记录自身的长度,所以strcat假定用户在执行这个函数时,已经为dest分配了足够多的内存,可以容纳src字符串中的所有内容,而一旦这个假定不成立时,就会产生缓冲区溢出,导致后续空间部分的内容被修改。
//将src字符串中的内容拼接到dest字符串的末尾
char *strcat(char *dest, const char *src);
SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。
sdscat(s, " Cluster");
sdscat执行之前 |
---|
sdscat执行之后 |
---|
sdscat不仅做了字符串拼接操作,它还为SDS分配了13字节的未使用空间,并且拼接之后的字符串也正好是13字节长。
3、 减少修改字符串时带来的内存重分配次数**
C字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或者缩短一个C字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作。
- 如果程序执行的是增长字符串的操作,比如拼接操作(append),那么在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小——如果忘了这一步就会产生缓冲区溢出。
- 如果程序执行的是缩短字符串的操作,比如截断操作(trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用的那部分空间——如果忘了这一步就会产生内存泄漏。
存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作。Redis经常被用于速度要求严苛、数据被频繁修改的场合,次修改字符串的长度都需要执行一次内存重分配的开销是无法接收的,会对Redis性能造成严重的影响。
SDS实现了空间预分配和惰性空间释放两种优化策略来避免了改变字符串长度时所需的内存重分配操作
空间预分配:优化SDS字符串增长操作,当SDS进行修改并且需要对SDS空间进行修改的时候,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额外未使用的空间。修改后len小于1MB的时候,free值等于len;修改后len大于1MB的时候,free=1MB。
惰性空间释放:用于优化SDS缩短操作,SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
4、二进制安全的
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的(binary-safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
一个保存了特殊数据格式的SDS |
---|
C字符串所用的函数只会识别出其中的"Redis",而忽略之后的"Cluster"。Redis函数可以识别出是"Redis Cluster"。
5、兼容部分C字符串函数
Redis和C一样遵循C字符串以空字符结尾的惯例,SDS可以在有需要时重用<string.h>函数库,从而避免了不必要的代码重复。
例如:如果有一个保存文本数据的SDS值sds,可以重用<string.h>/strcasecmp函数,使用它来对比SDS保存的字符串和另一个C字符串:
strcasecmp(sds->buf, "hello world");
4. 总结
比起C字符串,SDS具有以下优点:
1)常数复杂度获取字符串长度。
2)杜绝缓冲区溢出。
3)减少修改字符串长度时所需的内存重分配次数。
4)二进制安全。
5)兼容部分C字符串函数。
5. 源码阅读(Redis 5.0.14)
SDS结构体
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 记录已经使用的字符串长度,不包含c字符串的末位符 */
uint8_t alloc; /* 记录不包括SDS头部和结尾的NULL字符的情况下,sds能够存储的字符串的最大容量 */
unsigned char flags; /* 记录sds的类型,低三位代表着sds的类型,一般通过与掩码按位与计算, 高5位未使用 */
char buf[] /* 字符数组,真正用来存储字符串,与c语言的字符串一样,末位有‘\0’ */;
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
- 从源码可以看出5.0+版本的Redis定义了多个不同大小的SDS结构体,并且sdshdr5 是永远不会被用到的。
- 源码的sds结构和《Redis设计与实现》使用的结构体略有不同,应该是Redis版本问题,但内容本质上是一致的。
不同类型SDS获取len源码
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
不同类型SDS获取未使用空间大小函数源码
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
}
}
return 0;
}