Redis 没有直接使用 C 语言传统的字符串表示,而是自己构建了一种名为简单动态字符串( simple dynamic string ,SDS) 的抽象 类型,并将 SDS 用作 Redis 的默认字符串表示。
SDS结构
总共有五类SDS结构,根据设置的字符串串长度,选择对应的结构体。
结构体中,len表示字符串的长度,alloc 表示字符串最大的长度,flags 低三位保存sds的类型,buf 占位符。
/* 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__)) hisdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr16 {
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__)) hisdshdr32 {
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__)) hisdshdr64 {
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[];
};
SDS与传统C字符串区别
1.获取字符串长度
C字符串: C 字符串并不记录自身的长度信息,所 以为了获取一个 C 字符串的长度,程序必须遍历整个字符串,直到遇到代表字符串结尾的空字符为止,这个操作的 复杂度为O(n).
SDS:SDS在 len 属性中记录了本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。
2.缓冲区溢出
C字符串: C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。例如strcat将一个字符串拼接到另一个字符串末尾,但是没有给目标字符串分配足够的空间,从而导致冲区溢出。
SDS:SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求,如果不满足的话. API 会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。
3.SDS空间预分配和惰性空间释放
空间预分配用于优化 SDS 的字符串增长操作:当 SDS 的 API 对一个 SDS进行修改, 并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用空间。
如果对 SDS 进行修改之后,SOS 的长度(也即是 len 属性的值)将小于1M, 那么程序分配和 len 属性同样大小的未使用空间。比如,如果进行修改之后, SDS 的 len 将变成13字 节,那么程序也会分配13字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13+13+1=27 字节(额外的一字节用于保存空字符)。
如果对 SDS 进行修改之后, SDS 的长度将大于等于 1M,那么程序会分配 1MB 的未使用空间。举个例子,如果进行修改之后, SOS 的 len 将变成 30M,那么程序会分 配 1M的未使用空间, SDS 的 buf 数组的实际长度将为 30 M+ 1M+1byte
惰性空间释放用于优化 SDS 的字符串缩短操作,当 SOS 的 API 需要缩短 SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是等待将来使用。
4.二进制安全
C字符串: 除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读人的空字符将被误认为是字符串结尾,这些限制使得 C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二 进制数据。
SDS:SDS 的 API 都是二进制安全的,所有 SOSAPI 都会以处理二进制的方式来处理 SDS存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
5.兼容C字符串函数
虽然 SDS 的 API 都是二进制安全的,但它们一样遵循 C 字符串以空字符结尾的惯例: 这些 API 总会将 SDS 保存的数据的末尾设置为空字符,并且总会在为 buf 数组分配空间时 多分配一个字节来容纳这个空字符, 这样SDS可以重用一部分C中的函数。
源码部分
扩容代码
/* Enlarge the free space at the end of the hisds string so that the caller
* is sure that after calling this function can overwrite up to addlen
* bytes after the end of the string, plus one more byte for nul term.
*
* Note: this does not change the *length* of the hisds string as returned
* by hi_sdslen(), but only the free buffer space we have. */
hisds hi_sdsMakeRoomFor(hisds s, size_t addlen) {
void *sh, *newsh;
size_t avail = hi_sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & HI_SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = hi_sdslen(s);
sh = (char*)s-hi_sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < HI_SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += HI_SDS_MAX_PREALLOC;
type = hi_sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so hi_sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == HI_SDS_TYPE_5) type = HI_SDS_TYPE_8;
hdrlen = hi_sdsHdrSize(type);
if (oldtype==type) {
newsh = hi_s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = hi_s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
hi_s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
hi_sdssetlen(s, len);
}
hi_sdssetalloc(s, newlen);
return s;
}
缩减容量
static inline void hi_sdssetlen(hisds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&HI_SDS_TYPE_MASK) {
case HI_SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = (unsigned char)(HI_SDS_TYPE_5 | (newlen << HI_SDS_TYPE_BITS));
}
break;
case HI_SDS_TYPE_8:
HI_SDS_HDR(8,s)->len = (uint8_t)newlen;
break;
case HI_SDS_TYPE_16:
HI_SDS_HDR(16,s)->len = (uint16_t)newlen;
break;
case HI_SDS_TYPE_32:
HI_SDS_HDR(32,s)->len = (uint32_t)newlen;
break;
case HI_SDS_TYPE_64:
HI_SDS_HDR(64,s)->len = (uint64_t)newlen;
break;
}
}
void hi_sdsclear(hisds s) {
hi_sdssetlen(s, 0);
s[0] = '\0';
}
SDS结构
hisds s
s 指向内容起始地址
s[-1] 可以获取到type地址
s-sizeof(hisdshdr) 可以获取到sds内存起始地址