如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里
说白了就是 C 语言中的字符串不能满足 Redis 的需要,所以需要对其进行一定的改造
从 《Redis设计与实现》这本书中,我们可以知道 Redis 的 SDS (Simple Dynamic String)与 C 字符串有如下区别
DIFF | SDS | C 字符串 |
---|---|---|
获取字符串长度的时间复杂度 | O(1) | O(n) |
字符串函数是否会造成缓冲区溢出 | 否 | 是 |
每次修改字符串需要重新分配内存 | 否 | 是 |
二进制安全 | 是,可以保存文本,以及其他二进制 (图片等) 数据 | 否,只能存储文本 |
函数重用 | 能使用 |
自然可以使用 |
说白了,最重要的特性就是
SDS 定义
typedef char *sds;
3.2 之前的 SDS 结构体,《Redis 设计与实现》 就是基于这个版本的 SDS 源码讲解的
struct sdshdr {
// 记录 buf 数组中已经使用字节的数量
// 等于 SDS 所保存字符串的长度
unsigned int len;
// 记录 buf 数组中还未使用字节的数量
unsigned int free;
// 字节数组,数据域,保存字符数据
char buf[];
};
src/sds.h/sdshdr
都表示一个 SDS 值3.2 之后的 SDS 结构体, 相比以前稍微复杂,其根据字符串的长度,划分了 5 种结构体
sdshdr5
,sdshdr8
,sdshdr16
,sdshdr32
,sdshdr64
/* 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 {
// 低三位存储类型,其余 5 位存储长度
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
// 字符串在 buf 中实际占用的字节数 (不包含结束符 '\0')
uint8_t len; /* used */
// buf 去掉去除头长度和结束符的总长度,即 head + end + len + free
uint8_t alloc; /* excluding the header and null terminator */
// 低位 3 bit 表示结构类型,其余 5 位未使用
unsigned char flags; /* 3 lsb of type, 5 unused bits */
// 数据域,与 3.2 之前版本一致
char buf[];
};
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, 2^5) | 31 |
8 | [2^5, 2^8) | 2^8-1 |
16 | [2^8, 2^16) | 2^16-1 |
32 | [2^16, 2^32) | 2^32 -1 |
64 | [2^32, 2^64) | 2^64 -1 |
len
表示 SDS 已使用的长度,与 3.2 版本之前一致 (不包含 ‘\0’)alloc
表示 SDS 的最大容量,即 buf 真实可用于存储字符串的空间 (buf 的真实分配大小 - 头尾空间),略小于 buf 字节数组的长度
剩余空间 free = alloc - len
flags
表示字符串类型,占用 1 个字节,低 3 位表示 header 的类型
buf
是柔性数组 (flexible array member)毕竟 C 太弱,所以直接贴别人的理解,似乎挺重要的
__attribute__ ((__packed__))
是C语言的一种关键字,这将使这个结构体在内存中不再遵守字符串对齐规则,而是以内存紧凑的方式排列。目的时在指针寻址的时候,可以直接通过 sds[-1]
找到对应 flags,有了flags就可以知道头部的类型,进而获取到对应的len,alloc信息packed
或者 attribute__((packed))
关键字的作用就是用来打包数据的时候以 1 来对齐,比如说用来修饰结构体或者联合体的时候,那么这些成员之间就没有间隙(gaps)了。如果没有加,那么这样结构体或者联合体就会以他的自然对齐方式来对齐。比如某 CPU 架构的编译器默认对齐方式是4, int 的 size 也是 4,char 的 size 是 1,那么类似typedef __packed struck test_s {
char a;
int b;
} test_t;
这样定义结构体的 size 就是 8 个字节 (4 字节对齐的情况下)。如果加上 packed
,size 就会变成 5 个字节,中间是没有 gaps 的
这个概念很重要,redis 源码中不是直接对 sdshdr 某一个类型操作,往往参数都是sds,而 sds 就是结构体中的 buf,在后面的源码分析中,你可能会经常看见 s[-1] 这种魔法一般的操作,而按照 sdshdr 内存分布 s[-1] 就是sdshdr中flags变量,由此可以获取到该 sds 指向的字符串的类型
redis源码解析-字符串 - @静夜
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7 // 2^3 - 1
#define SDS_TYPE_BITS 3
flags & SDS_TYPE_MASK
来获取动态字符串对于的字符串类型, 说白了就是 flags % 2^3
flags & SDS_TYPE_MASK
= 5,6,7 时,会是什么?#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
结构体的首地址
= (sds 的字节数组)
- (对应结构体定义的 size)
获得对于 sds 的引用 (按 Java 的话说)static inline size_t sdslen(const sds s) {
// 通过 s[-1] 获得到该 sds 的 flags
unsigned char flags = s[-1];
// 获得指定的
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
// 获取 SDS_TYPE_BITS = 3
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
// 通过 SDS_HDR 获得结构体首地址
// 通过 len 获得对于大小
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;
}
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
// 5 没有多余空间,适合静态字符串,需要扩容需要重新分配
return 0;
}
case SDS_TYPE_8: {
// 获得 sh, 使得 sh 可以访问 alloc 和 len
// 剩余可用空间 free = alloc - len
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;
}
free = alloc - len
我们知道传统 C 语言要想知道字符串的长度,需要 O(n) 的时间复杂度去遍历长度,直到找到结束符 \0
。 SDS 是不需要的,只需要得到结构体的 len 即可。
但我们看到方法参数中的 sds, 即 s, 并不是某个 sdshdr 结构体的引用,而是该结构体的字节数组。换成 Java 的话就是,我没有拿到你的对象,你给我的是这个对象变成字节数组的形态,我无法通过 object.len 的方法获得 len,那怎么办呢?看下面代码
SDS_HDR(8,s);
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
// 翻译后
((struct sdshdr8 *)((s) - (sizeof(struct sdshdr8))))
结构体的首地址
= (sds 的字节数组)
- (对应结构体定义的 size)
sdshdr8
在取消内存对齐后的结构体定义 size 是 3 个字节, uint8_t len
, uint8_t alloc
, flags
各占用 1 个字节,buf
是柔性数组,初始值为 0,不占用空间总之然后我就可以通过该引用获得 len 的大小
/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
* If SDS_NOINIT is used, the buffer is left uninitialized;
*
* The string is always null-termined (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3);
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header.
*
* 方法:生成新的 sds
* 参数:
* 1. *init 初始化内容
* 2. initlen 初始大小
*/
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
// 根据传入的初始大小获得 sds 类型 (SDS_TYPE)
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
// 因为字符串都会追加操作,所以最低使用 sdshdr8 类型
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 获得对于的结构体长度
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
// 分配内存
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
sh = s_malloc(hdrlen+initlen+1);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
// 根据 SDS_TYPE 来初始 sds 内容
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
// 新 sds 初始完后,将 init 内容拷贝到 sds 中
if (initlen && init)
memcpy(s, init, initlen);
// sds 末尾置为结束符
s[initlen] = '\0';
return s;
}
size = hdrlen + initlen + 1
\0
/* Enlarge the free space at the end of the sds 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 sds string as returned
* by sdslen(), but only the free buffer space we have.
*
* 方法:为 sds 扩容 addlen 大小
* 参数:
* 1. s 对于 sds
* 2. addlen 至少要多满足的长度
* */
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
// 获取当前 sds 的剩余空间
size_t avail = sdsavail(s);
size_t len, newlen;
// 获得 SDS_TYPE
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
// 如果 SDS 剩余空间足够,就无需扩容了
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
// 当前 sds len 大小
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
// 扩容后的新大小 = len + addlen, 以及后续对 newlen 的处理
newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
// newlen 长度处理
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 根据新长度,获得新的 SDS_TYPE
type = 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 sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
// 获得新 type 的 hdrlen
hdrlen = sdsHdrSize(type);
assert(hdrlen + newlen + 1 > len); /* Catch size_t overflow */
// 如果旧类型和新类型一致,则直接对旧结构体分配更多内存空间即可
if (oldtype==type) {
newsh = 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 = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
// 拷贝旧结构体数据到新结构体中
memcpy((char*)newsh+hdrlen, s, len+1);
// 释放旧内存数据
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
// 更新 sds 已使用空间长度,即更新 len
sdssetlen(s, len);
}
// 更新 alloc
sdssetalloc(s, newlen);
return s;
}
SDS 在缩容的时候,并非立即释放内存,而已通过调整 len 的大小,逻辑缩容,并非物理缩容。即通过调整 len ,告诉外界 len 外的数据废弃了,当做是冗余空间。比如 sdstrim
函数
/* Remove the part of the string from left and from right composed just of
* contiguous characters found in 'cset', that is a null terminted C string.
*
* After the call, the modified sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call.
*
* Example:
*
* s = sdsnew("AA...AA.a.aa.aHelloWorld :::");
* s = sdstrim(s,"Aa. :");
* printf("%s\n", s);
*
* Output will be just "HelloWorld".
*/
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > sp && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep-sp)+1);
if (s != sp) memmove(s, sp, len);
s[len] = '\0';
sdssetlen(s,len);
return s;
}
SDS 的 trim 等操作的缩容属于惰性删除, 而下面的 sdsRemoveFreeSpace 就是真正释放空间的操作。物理缩容说白了就是要将 SDS 的空间从 alloc 缩容到 len 长度,即 alloc - len = 0
/* Reallocate the sds string so that it has no free space at the end. The
* contained string remains not altered, but next concatenation operations
* will require a reallocation.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
// 获得 sds 类型,和结构体长度
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
size_t len = sdslen(s);
size_t avail = sdsavail(s);
sh = (char*)s-oldhdrlen;
// 如果 avail 已经为 0 了,没有可用空间了,则无需再缩了
/* Return ASAP if there is no space left. */
if (avail == 0) return s;
/* Check what would be the minimum SDS header that is just good enough to
* fit this string. */
// 以 len 获得 type, 将空间从 alloc 降到 len 长度
type = sdsReqType(len);
hdrlen = sdsHdrSize(type);
/* If the type is the same, or at least a large enough type is still
* required, we just realloc(), letting the allocator to do the copy
* only if really needed. Otherwise if the change is huge, we manually
* reallocate the string to use the different header type. */
// 如果类型不变,则重新分配空间
if (oldtype==type || type > SDS_TYPE_8) {
newsh = s_realloc(sh, oldhdrlen+len+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
} else {
// 类型发生变化则重新分配内存
newsh = s_malloc(hdrlen+len+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, len);
return s;
}