本文以 redis 3.2.8 版本来介绍 redis 源码。
字符串是 Redis 最基本的数据结构,首先键都是字符串类型的,而且其他几种数据结构也都是在字符串类型的基础之上构建的,因此,我认为从字符串入手来探究 Redis 的数据结构是相对合理的。
redis 没有直接使用 C 语言中传统的字符串表示,而是自己实现了一套名为简单动态字符串(simple dynamic string, SDS)的抽象类型,将其作为 redis 的默认字符串使用。实际上 redis 并没有完全抛弃 C 字符串,只是在这之上进行了进一步封装,使其获得更好的特性,比如二进制安全、动态扩展内存等,这也使得 SDS 可以兼容 C 字符串的 API。
redis 源码中关于 sds 的部分,主要在 sds.h
和 sds.c
这两个文件中。
SDS 的定义
首先在 sds.h
中找到了 sds 的定义
typedef char *sds;
这跟 C 字符串是一样的,但是真的这么简单吗?往下看,会有新的发现:
// 暂时未用到
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 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
到此,大概明白了,sds 采用一段连续的内存空间来存储动态字符串,即 header + str 的形式。__attribute__ ((packed))
是为了让编译器以紧凑模式来分配内存。不对 struct 中的字段进行内存对齐,以此保证 header 和 sds 的数据部分紧紧的相邻,否则,不能按照固定的偏移来获取 flags 字段。
当定义了 sds* s
后,使用 s[-1]
就可以获得 flag 的值,这样就知道了这个 sds 属于哪种类型。
为了满足存储不同长度字符串的需求(为了节省内存),在宏定义中定义了五种 header(0~4)。
下面解释一下 header 中各个字段的含义:
- len: 字符串真正的长度(不包含空终止字符)
- alloc: 字符串的最大容量,不包含 header 和最后的 '0',初始时与 len 值一致
- flags: 低三位表示 header 类型
- buf: 柔性数组,表示一个长度动态的字符串
柔性数组,只能定义在一个结构体的最后一个字段上。它在这里只是起到一个标记的作用,表示在 flags字段后面是一个字符数组,或者说,它指明了紧跟在 flags 字段后面的这个字符数组在结构体中的偏移位置。
程序在为 header 分配内存的时候,buf[] 并不占用内存空间。如果计算 sizeof(struct sdshdr16)
的值,那么结果是5个字节,其中没有 buf 字段.
注:关于柔性数组的介绍,可以参考这篇博客 结构体中使用柔性数组。
下面画了一个内存简易图可以帮助理解:
SDS 宏定义
// sds 类型
#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
// 类型掩码,与 flag 相与,可以得到低三位
#define SDS_TYPE_MASK 7
// 类型占用的比特位数
#define SDS_TYPE_BITS 3
// 获得 sds header 头指针
#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))))
// SDS_TYPE_5 类型的 sds,低三位表示 sds type, 高五位表示 sds len
#define SDS_TYPE_5_LEN(f) ((f) >> SDS_TYPE_BITS)
SDS 函数
创建 sds
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen); // 根据 initlen 判断需要以哪种类型的 sds 来存储 init
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type); // 所选择的 sds 类型的 header 长度
unsigned char *fp; /* flags pointer. */
sh = s_malloc(hdrlen+initlen+1); // 分配 sds 需要的内存:header + str + 1,以\0 结尾
if (!init)
memset(sh, 0, hdrlen+initlen+1); // 初始化 sds
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen; // s 指向 buf 首元素
fp = ((unsigned char*)s)-1; // fp 指向 flag
switch(type) {// 设置 sds 的 header 各参数
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS); // sds5, 第三位为 type,高五位是 len
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen; // len 与 alloc 起始值相同
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;
}
}
if (initlen && init)
memcpy(s, init, initlen); // copy
s[initlen] = '\0';// 结束
return s;
}
释放 sds
#define s_free zfree
void sdsfree(sds s) {
if (s == NULL)
return;
s_free((char *)s - sdsHdrSize(s[-1]));
}
扩展 sds 容量
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); // 当前 sds 剩余空间
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) // 需要增加的空间当前的 sds 足以满足,不需要调整 sds
return s;
len = sdslen(s);
sh = (char *)s - sdsHdrSize(oldtype); // header 头指针
newlen = (len + addlen); // 新的 sds 需要的长度
/* sds规定:如果扩展后的字符串总长度小于 1M 则新字符串长度加倍
* 否则,新长度为扩展后的总长度加上1M
* 这样做的目的是减少Redis内存分配的次数,同时尽量节省空间 */
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
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;
hdrlen = sdsHdrSize(type);
if (oldtype == type) {// 新旧 sds 的类型一致,调用realloc函数扩充内存
newsh = s_realloc(sh, hdrlen + newlen + 1); // realloc对malloc申请的内存进行大小的调整
if (newsh == NULL)
return NULL;
s = (char *)newsh + hdrlen; // s 指向 buf
}
else {
/* 如果类型调整了,header的大小就需要调整
这时就需要移动buf[]部分,所以不能使用realloc */
newsh = s_malloc(hdrlen + newlen + 1);
if (newsh == NULL)
return NULL;
memcpy((char *)newsh + hdrlen, s, len + 1); // 拷贝
s_free(sh); // 释放之前的 sh 占用的内存
s = (char *)newsh + hdrlen;
s[-1] = type; // 设置 flag
sdssetlen(s, len); // 设置 sds 的 len 字段
}
sdssetalloc(s, newlen); // 设置 sds 的 alloc 字段
return s;
}
另外,redis 提供 sds sdsRemoveFreeSpace(sds s);
函数来回收 sds 空余空间。
redis 还提供了很多其他的函数,来方便 sds 的操作,这个可以自行查看 源码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。