Redis 源码简洁剖析 02 - SDS 字符串
Categories: 编程
C 语言的字符串函数
C 语言 string 函数,在 C 语言中可以使用 char* 字符数组实现字符串,C 语言标准库 string.h 中也定义了多种字符串操作函数。
字符串使用广泛,需要满足:
- 高效的字符串操作,比如追加、拷贝、比较、获取长度
- 能保存任意的二进制数据,比如图片
- 尽可能省内存
💡 为什么 Redis 不直接使用 C 语言的字符串?
- C 语言 char* 以
'\0'标识字符串的结束,则中间含有'\0'的字符串无法被正确表示;也正因为此,没有办法保存图像等二进制数据。 - C 语言 char* 效率问题:
- 获取 ` 字符串长度 ` 的时间复杂度是
O(n) - 追加字符串的时间复杂度也是
O(n) - 可能由于可用空间不足,无法追加
- 获取 ` 字符串长度 ` 的时间复杂度是
下面代码展示了 C 语言中 ‘\0’ 结束字符对字符串的影响。
#include "stdio.h"
#include "string.h"
int main(void) {
char *a = "red\0is";
char *b = "redis\0";
printf("%lu\n", strlen(a));
printf("%lu\n", strlen(b));
}
输出结果是 3 和 5。
SDS 定义
SDS(简单动态字符串) 是 simple dynamic string 的简称,Redis 使用 SDS 作为字符串的数据结构。Redis 中所有的键(key)底层都是 SDS 实现的。
比如:
redis> SET msg "hello world"
OK
redis> RPUSH fruits "apple" "banana" "cherry"
(integer) 3
Redis SDS 源码主要在 sds.h 和 sds.c 中。其中可以发现 Redis 给 char* 起了别名:
typedef char *sds;
SDS 内部结构
SDS 结构中有一个元数据 flags,表示的是 SDS 类型(最低 3 位)。事实上,SDS 一共设计了 5 种类型,分别是
- sdshdr5
- sdshdr8
- sdshdr16
- sdshdr32
- sdshdr64
💡 这几个的区别就在于字符数组现有长度 len 和分配空间长度 alloc 的类型,为了节省内存。
sds struct
/* 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 */
// buf 是柔性数组,必须是结构体的最后一个成员,不制定大小就不计算空间
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
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__)) 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[];
};
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
// ## 是 C 预处理器的连接符,如果 T=8,则为 sdshdr8
// 找到 header 地址,sh 即为 header 地址
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
// 返回 struct sdshdr##T 类型的指针(header 地址)
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
SDS 的主要操作 API

字符串初始化
整体和 Java 的 StringBuilder 很像了 O_o
/* 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);
}
首先是判断输入的 init 字符串的长度,接着调用 sdsnewlen 分配内存空间并赋值。
sds sdsnewlen(const void *init, size_t initlen) {
return _sdsnewlen(init, initlen, 0);
}
核心函数_sdsnewlen 如下,主要就是先确保空间是否足够、分配空间,然后再调用 memcpy 将 *init 复制到对应的内存空间。
/* 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 _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
char type = sdsReqType(initlen);
/* 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);
unsigned char *fp; /* flags pointer. */
size_t usable;
assert(initlen + hdrlen + 1> initlen); /* Catch size_t overflow */
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
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;
usable = usable-hdrlen-1;
if (usable> sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
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 = usable;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
Redis 源码简洁剖析系列
- Redis 7.0.md
- Redis 源码简洁剖析 01 - 环境配置. md
- Redis 源码简洁剖析 02 - SDS 字符串. md
- Redis 源码简洁剖析 03 - Dict Hash 基础. md
- Redis 源码简洁剖析 04 - Sorted Set 有序集合. md
- Redis 源码简洁剖析 05 - ziplist 压缩列表. md
- Redis 源码简洁剖析 06 - quicklist 和 listpack.md
- Redis 源码简洁剖析 07 - main 函数启动. md
- Redis 源码简洁剖析 08 - epoll.md
- Redis 源码简洁剖析 09 - Reactor 模型. md
- Redis 源码简洁剖析 10 - aeEventLoop 及事件. md
- Redis 源码简洁剖析 11 - 主 IO 线程及 Redis 6.0 多 IO 线程. md
- Redis 源码简洁剖析 12 - 一条命令的处理过程. md
- Redis 源码简洁剖析 13 - RDB 文件. md
- Redis 源码简洁剖析 14 - Redis 持久化. md
- Redis 源码简洁剖析 15 - AOF.md
- Redis 源码简洁剖析 16 - 客户端. md
- Redis 源码简洁剖析 17 - 服务器. md
- Redis 源码简洁剖析 18 - 复制、哨兵 Sentinel.md
Java 编程思想 - 最全思维导图 - GitHub 下载链接,需要的小伙伴可以自取~
原创不易,希望大家转载时请先联系我,并标注原文链接。
我的公众号
coding 笔记、读书笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,大家关注 ^_^
我的博客地址:博客主页。
