什么是SDS?
SDS名称叫做(simple dynamic string)翻译过来就叫简单的动态字串。顾名思义,它主要就是用来存字串数据的。类似于java的String对象。
它主要的作用就是来解决传统C语言中字符串的一些限制,比如内存缓冲区溢出和内存泄漏问题,同时还针对性的提供了一些String相关的性能优化。
为什么会有SDS存在,而不是直接使用Char[]数组?
字串长度问题?
我们都知道。 Redis 底层是C语言编写的。但是在C语言中是没有类似于Java里面的String类型,所以在C语言中实现字串显示,只能是靠自己的char[]来实现。
那么基于这种存储方式,如果我们想要获取 「Redis」的长度,就需要从头开始遍历,直到遇到 ‘\0’ 为止(\0是C语言中的一个结束符号)。这样就会有一个问题,redis本身就是以快为核心的,而通过这种方式来获取长度,每次都遍历的话,那它也就跟快不沾边了。
所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。
也就是说。SDS动态字符串,肯定是有单独维护长度的属性的。这样获取长度的时间复杂度就从O(N)到了O(1)。
二进制安全问题?
C语言获取一个字符的长度,是通过遍历char[]数组直到遇到’\0’为止,这样来判断一个字符的长度的,但是对于很多的图片数据、音视频数据等二进制文件,很多都会存在 ‘\0’ 。对于这类 数据就会存在数据安全的问题。如下
对于sds来说。无论字串数据是什么样的,它不关心。他对于所有的数据都会有单独的长度字段来存放数据的长度。所以就解决了这个问题。但实际上对比C语言来说,sds的结构体在空间方面占用量肯定就比c语言多了很多。
SDS的结构体是什么样的?
我们下载一下redis的源码包。这个官方都有提供。在源码包下可以看到有很多的库。其中我们在src目录下就可以看到有关于sds的包,大家可以看一下sds.h这个文件 。下面代码我将关键结构体也已经复制出来了。
typedef char *hisds;
/* 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[];
};
#define HI_SDS_TYPE_5 0
#define HI_SDS_TYPE_8 1
#define HI_SDS_TYPE_16 2
#define HI_SDS_TYPE_32 3
#define HI_SDS_TYPE_64 4
#define HI_SDS_TYPE_MASK 7
#define HI_SDS_TYPE_BITS 3
#define HI_SDS_HDR_VAR(T,s) struct hisdshdr##T *sh = (struct hisdshdr##T *)((s)-(sizeof(struct hisdshdr##T)));
#define HI_SDS_HDR(T,s) ((struct hisdshdr##T *)((s)-(sizeof(struct hisdshdr##T))))
#define HI_SDS_TYPE_5_LEN(f) ((f)>>HI_SDS_TYPE_BITS)
上面是原码,下面是其核心的结构体如下图
一个实际的数据在内存中是如何存储的呢? 我们以name为例子
存储的字串为name,对应的sds中存的结构体就包含了下面的这4部分。
说明一下下面和字段的意思:
len : 表示 buf数组已用空间的长度,占 4 个字节,不包括 ‘\0’;
alloc:表示 buf数组 的实际分配长度,占 4 个字节,不包括 ‘\0’;
flags:标记当前字节数组是 sdshdr8/16/32/64 中的哪一种,占 1 个字节;
buf:表示字节数组,保存实际数据。Redis 会在数组最后加一个’\0’表示结束,这也是与C语言相同的,需要额外占用 1 个字节的开销;
在看一个存储值为redis的
什么情况下不适合使用SDS?
第一:对于一个key的数据量来说,如果特别大,则不建议使用SDS。一般建议小于10K
第二:对于大量的同一业务数据来说,不建议使用,建议使用hash/set等。我们了解sds的结构后可以明显知道,一个key就会多一些额外的空间来存储长度之类的。批量的同一业务数据则会产生很多额外的空间浪费。
动态扩容
动态扩容是一个结构体最复杂地方。因为涉及到内存的分配。
我们来详细分析一下sds的动态扩容机制。例如我们给一个key设置一个内容为“hi”,那么它的存储结构为:
当我们将hi重新赋值为“hi,Amy”,那sds内存是如何执行的呢?
首先redis会申请新内存空间,然后对字串大小进行判断:
如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;
如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。(称为内存预分配)
1M是可以存储的数据量很大了。所以当大于1M时,说明文件存储的量可能会很大,所以就会预分配1M的数据量。
我们看一下扩容源码:大家可以参考一下
sds sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
size_t avail = sdsavail(s); // 获取当前字符串可用的额外空间大小
size_t len, newlen, reqlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK; // 获取当前字符串的类型
int hdrlen;
size_t usable;
/* 如果当前可用空间已经足够,则直接返回 */
if (avail >= addlen) return s;
len = sdslen(s); // 获取当前字符串的长度
sh = (char*)s-sdsHdrSize(oldtype); // 获取当前字符串的头指针
reqlen = newlen = (len+addlen); // 计算需要的总长度
assert(newlen > len); /* 检查 size_t 是否溢出 */
if (greedy == 1) { // 如果采用贪婪策略
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2; // 如果需要的长度小于最大预分配长度,则将长度翻倍
else
newlen += SDS_MAX_PREALLOC; // 否则增加最大预分配长度
}
type = sdsReqType(newlen); // 计算新字符串所需的类型
/* 不使用类型 5:因为用户正在追加字符串,而类型 5 无法记住空闲空间,
* 因此每次追加操作都需要调用 sdsMakeRoomFor()。*/
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type); // 获取新字符串的头长度
assert(hdrlen + newlen + 1 > reqlen); /* 检查 size_t 是否溢出 */
if (oldtype==type) { // 如果新旧类型相同
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable); // 重新分配内存
if (newsh == NULL) return NULL; // 如果分配失败,则返回 NULL
s = (char*)newsh+hdrlen; // 更新字符串指针
} else {
/* 由于头大小发生变化,需要将字符串向前移动,
* 不能使用 realloc */
newsh = s_malloc_usable(hdrlen+newlen+1, &usable); // 分配新内存
if (newsh == NULL) return NULL; // 如果分配失败,则返回 NULL
memcpy((char*)newsh+hdrlen, s, len+1); // 复制原字符串内容
s_free(sh); // 释放原内存
s = (char*)newsh+hdrlen; // 更新字符串指针
s[-1] = type; // 更新字符串类型
sdssetlen(s, len); // 更新字符串长度
}
usable = usable-hdrlen-1; // 计算可用空间大小
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type); // 如果可用空间大于类型最大大小,则限制为最大大小
sdssetalloc(s, usable); // 更新字符串分配的大小
return s; // 返回更新后的字符串
}
总结
文章介绍了SDS设计的初衷及为什么要这样设计,也介绍了String在哪些情况下不太适合。还有扩容的机制。
可以说 SDS 是redis的一种很核心也很优秀的设计结构,它基于C语言确也优化了 C语言对于字符串的一些问题。
redis是最优秀的缓存技术之一,离不开它本身的这种设计结构。