这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
1. 数据结构
1.1 二进制安全
- 非二进制安全:C 语言中,用
\0表示字符串的结束,如果字符串本身就有\0字符,字符串会被截断 - 二进制安全:通过实现某种机制,保证了读写字符串不损害其内容
1.2 sdshdr5 短字符串
// sds.h
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};
- flags:无符号字符型,大小1字节(8 bit),低3位表示字符串类型(字符长度为1字节、2字节、4字节、8字节、小于1字节),高5位表示字符串长度(0~31)
- buf:柔性数组,存放实际的字符内容
使用柔性数组保存字符的原因:
- 柔性数组的地址和结构体是连续的,查找内存更快
- 可以通过柔性数组的首地址偏移得到结构体的首地址,进而方便获取其余属性字段
1.3 其他字符串
sdshdr8、sdshdr16、sdshdr32、sdshdr64 类型的字符串结构类似
// sds.h
#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
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用长度,1字节 */
uint8_t alloc; /* 总长度,1字节 */
unsigned char flags; /* 低3位表示类型,高5位闲置 */
char buf[]; /* 柔型数组,存放实际的字符内容 */
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 已使用长度,2字节 */
uint16_t alloc; /* 总长度,2字节 */
unsigned char flags; /* 低3位表示类型,高5位闲置 */
char buf[]; /* 柔型数组,存放实际的字符内容 */
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 已使用长度,4字节 */
uint32_t alloc; /* 总长度,4字节 */
unsigned char flags; /* 低3位表示类型,高5位闲置 */
char buf[]; /* 柔型数组,存放实际的字符内容 */
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 已使用长度,8字节 */
uint64_t alloc; /* 总长度,8字节 */
unsigned char flags; /* 低3位表示类型,高5位闲置 */
char buf[]; /* 柔型数组,存放实际的字符内容 */
};
- len:表示 buf 中字符的已使用长度
- alloc:表示 buf 中能够保存字符的总长度
- flags:表示当前结构体的类型,低3位表示类型,高5位闲置未使用
- buf:柔性数组,存放实际的字符内容
sdshdr8 字符串结构图:
1.4 attribute ((packed))
一般情况下,结构体会根据所有变量的最小公倍数做字节对齐,当用 packed 修饰后,结构体变为按1字节对齐
以 sdshdr32 类型的字符串为例,修饰前按4字节对齐大小为12字节,修饰后按1字节对齐为9字节
packed 修饰的好处:
- 节省内存
- SDS 返回给上层的,不是结构体的首地址,而是指向字符串内容 buf 的指针。SDS 创建后,通过
(char*)sh+hdrlen得到 buf 指针地址(sh 为指向结构体首地址的指针,hdrlen 是结构体的长度)。packed 修饰后方便通过buf[-1]找到 flags
2. 源码浅析
2.1 创建字符串
-
// sds.c sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; // 根据字符串初始长度获取类型 char type = sdsReqType(initlen); // SDS_TYPE_5 需要转换为 SDS_TYPE_8 if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; // 获取当前类型的头部长度(结构体大小) int hdrlen = sdsHdrSize(type); unsigned char *fp; // 为 sds 分配内存 // +1是为了结束符 ‘\0’ // sh 为存放 sds 内存空间的首地址 sh = s_malloc(hdrlen+initlen+1); if (init==SDS_NOINIT) init = NULL; else if (!init) // 填充内存 memset(sh, 0, hdrlen+initlen+1); if (sh == NULL) return NULL; // s 指向 buf s = (char*)sh+hdrlen; // fp 指向 flags fp = ((unsigned char*)s)-1; // 根据类型为 sds 结构体属性字段 switch(type) { case SDS_TYPE_5: { // flags 赋值,低3位表示类型,高3位表示字符长度 *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; } // ...... } if (initlen && init) // 填充字符内容到 buf memcpy(s, init, initlen); // 添加结束符 s[initlen] = '\0'; // 返回指向 buf 的指针 return s; }
创建空字符串时,SDS_TYPE_5 类型会转化为 SDS_TYPE_8 类型。创建空字符串后,可能存在字符内容频繁变更而引发扩容,所以直接创建为 SDS_TYPE_8 类型的字符串
2.2 释放字符串
直接释放内存
-
// sds.c void sdsfree(sds s) { if (s == NULL) return; // 根据 sds 首地址释放 s_free((char*)s-sdsHdrSize(s[-1])); }
不直接释放内存,重置字符已使用长度为0,新的数据直接覆盖写入,无需重新申请内存
-
// sds.c void sdsclear(sds s) { // 重置已使用长度为0 sdssetlen(s, 0); // 设置结束符 s[0] = '\0'; }
2.3 拼接字符串
根据 sds 当前字符内容已使用长度,有需要时进行扩容,然后直接拼接字符串,重置 sds 已使用长度,添加结束符
-
// sds.c sds sdscatsds(sds s, const sds t) { return sdscatlen(s, t, sdslen(t)); } -
// sds.c sds sdscatlen(sds s, const void *t, size_t len) { // 获取字符串当前长度 size_t curlen = sdslen(s); // 有需要时对 sds 进行扩容 s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; // 直接拼接字符串 memcpy(s+curlen, t, len); // 重置已使用长度 sdssetlen(s, curlen+len); // 添加结束符 s[curlen+len] = '\0'; return s; } -
// sds.c sds sdsMakeRoomFor(sds s, size_t addlen) { void *sh, *newsh; // 获取未使用字符长度 size_t avail = sdsavail(s); size_t len, newlen; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; // 如果未使用字符长度充足,无需扩容直接返回 if (avail >= addlen) return s; len = sdslen(s); // 获取 sds 首地址 sh = (char*)s-sdsHdrSize(oldtype); // 计算字符串新长度 newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) // 新字符串所需内存空间小于1M时,按新长度的2倍扩容 newlen *= 2; else // 新字符串所需内存空间大于等于1M时,按新字符串内存空间加上1M进行扩容 newlen += SDS_MAX_PREALLOC; // 获取新字符串类型 type = sdsReqType(newlen); // SDS_TYPE_5 需要转换为 SDS_TYPE_8 if (type == SDS_TYPE_5) type = SDS_TYPE_8; hdrlen = sdsHdrSize(type); if (oldtype==type) { // 类型没有变化时,扩大 buf 即可 newsh = s_realloc(sh, hdrlen+newlen+1); if (newsh == NULL) return NULL; // 获取指向 buf 的新指针 s = (char*)newsh+hdrlen; } else { // 如果类型发生变化,则重新分配内存 newsh = s_malloc(hdrlen+newlen+1); if (newsh == NULL) return NULL; // 原字符内容复制到新内存空间 memcpy((char*)newsh+hdrlen, s, len+1); // 释放旧指针 s_free(sh); // 获取指向 buf 的新指针 s = (char*)newsh+hdrlen; // 更新 flags 标识的类型 s[-1] = type; // 更新 len sdssetlen(s, len); } // 更新 alloc sdssetalloc(s, newlen); return s; }
3. 其他 API
-
// sds.h // 根据给定字符串创建 SDS sds sdsnewlen(const void *init, size_t initlen); // 创建空字符串,长度为0,内容为“” sds sdsempty(void); // 复制 SDS sds sdsdup(const sds s); // SDS 扩容到指定长度,并用'0'填充新增内容 sds sdsgrowzero(sds s, size_t len); // 将指定长度的字符串复制到 SDS sds sdscpylen(sds s, const char *t, size_t len); // SDS 两端清除指定字符 sds sdstrim(sds s, const char *cset); // 刷新 SDS 属性值 void sdsupdatelen(sds s); // 根据指定分隔符对 SDS 进行切分 sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count); /* Low level functions exposed to the user API */ sds sdsMakeRoomFor(sds s, size_t addlen); // SDS 缩容 sds sdsRemoveFreeSpace(sds s); // 返回 SDS 占用内存大小 size_t sdsAllocSize(sds s);
4. 总结
-
SDS 如何兼容 C 语言字符串?
SDS 对象中 buf 是一个柔性数组,上层调用时,SDS 直接返回 buf 。由于 buf 是直接指向字符内容的指针,所以兼容 C 语言函数
-
如何保证二进制安全?
读取字符串内容时,SDS 会通过 len 来限制读取长度,而非
\0 -
sdshdr5 的特殊之处?
sdshdr5 只能存储长度为0~31的字符串,一般情况下短字符串存储更加普遍,所以 Redis 进一步压缩了 sdshdr5 的数据结构,将类型和长度放入同一个属性字段中,用 flags 的低3位保存类型,高5位保存长度。创建空字符串时,sdshdr5 会被 sdshdr8 取代
-
SDS 如何扩容?
调用
sdsMakeRoomFor函数,检查后根据具体情况进行扩容- 新字符串内存空间小于1M时,字符串内存空间按新内存两倍扩容
- 新字符串内存空间大于等于1M时,字符串内存空间按新内存加上1M进行扩容
学自《Redis 5设计与源码分析》