在src/sds.h中定义了Redis中的动态String类型,这意味着,使用者仅仅需要调用接口API就可以向String加入数据,而不需要关心扩容的问题。Redis使用 typedef char *sds;
来描述这个动态String,其在内存中的分布格式为一个StringHeader以及在StringHeader后面一段连续的动态内存,而sds
则是指向StringHeader后面的连续内存的第一个字节。其在内存中问分布情况可以入下图所示:
Strings的头部信息
sds
的头部信息主要包含了sds
被分配的缓存大小以及已经使用的缓存的大小,根据所需要的分配缓存的大小,在Redis中定义了五种sds
的头部信息:
sdshdr5
,定义了类型SDS_TYPE_5
sdshdr8
,定义了类型SDS_TYPE_8
sdshdr16
,定义了类型SDS_TYPE_16
sdshdr32
,定义了类型SDS_TYPE_32
sdshdr64
,定义了类型SDS_TYPE_64
上述的五种sdshdr
分别表示最大可以分配缓存的大小,其中sdshdr5
表示最大可以分配1 << 5
大小的缓存,而sdshdr8
表示最大可以分配1 << 8
大小的缓存。除了sdshdr5
之外,其余的头部信息都是按照如下格式(以sdshdr32
为例)进行定义的:
struct __attribute__ ((__packed__)) sdshdr32
{
uint32_t len; //已经使用缓存的长度
uint32_t alloc; //包含header以及结尾null结束符在内分配的缓存的总长度
unsigned char flags; //低三位保存header类型信息,SDS_TYPE_32
char buf[]; //动态分配的缓存
};
而sdshdr5
头部的格式则是按照如下的方式进行定义的:
struct __attribute__ ((__packed__)) sdshdr5
{
unsigned char flags; //低三位保存header类型信息,高五位用于表示已经使用缓存的长度
char buf[]; //动态分配的缓存
};
基于我们需要的String的长度,选择不同的sdshdr
,这样可以达到节约空间的目的。通过Redis之中关于sdshdr
数据类型的定义,我们可以发现,无论是哪种sdshdr
,sdshdr.buf
缓存字段之前,都是sdshdr.flags
标记字段,在Redis之中,我们实际使用的sds
变量,其实是指向sdshdr.buf
的指针,而整个SDS是一段连续分配的内存,那么,如果我们通过sds
向前偏移一个字节长度的话sds[-1]
,一定是这个SDS数据的sdshdr.flags
字段。通过位运算,我们便可以知道该SDS数据所使用sdshdr
的类型,进而通过指针偏移,便可以获取到整个SDS的头部信息,后续很对对于SDS的基础操作都是通过该方式实现的。
Strings的通用底层操作
在头文件之中定义了若干个宏以及静态函数用于实现对于sds
的基础操作。
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
给定一个sds
数据,使用SDS_HDR
来获取其对应的sdshdr
的头指针。其使用方法是通过SDS数据获取到对应的sdshdr.flags
,进而得到HeaderType,通过调用SDS_HDR
来获取
整个头部信息,例如:
unsigned char flags = s[-1];
switch (flags * SDS_TYPE_MASK)
{
...
case SDS_TYPE_8:
SDS_HDR(8, s)->len = new_len;
break;
...
}
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
给定一个sds
数据,声明一个sdshdr
指针变量sh
,并将这个sds对应的sdshdr
头指正赋值给这个sh
变量。
static inline size_t sdslen(const sds s);
给定一个sds
数据,获取其已使用缓存的长度,具体的实现方式为:
- 结合
sdshdr
的定义,以及sds
在内存中的分布结构,通过s[-1]
来获取StringHeader中的flags
数据。 - 根据
flags
计算出其对应的是什么类型的shshdr
。 - 调用宏
SDS_HDR
获取到对应的StringHeader的指针,进而获得len
字段数据。
static inline size_t sdsalloc(const sds s);
给定一个sds数据,获取其分配缓存sdshdr.buf
的总长度。
static inline size_t sdsavail(const sds s);
给定一个sds数据,获取其缓存可用的长度,可以理解为sdsavail(s) == sdsalloc(s) - sdslen(s)
。
static inline void sdssetlen(sds s, size_t newlen);
给定一个sds数据,以及一个新的长度newlen,将sds的header中的len字段设置为newlen。
static inline void sdsinclen(sds s, size_t inc);
给定一个sds数据,以及一个需要增加的长度inc,将sds的header中的len字段增加inc。需要注意的是,在sdsinclen
中,并不会检查增加的长度inc
是否是合法的,仅仅是将inc
累加到sdshdr.len
之中。这就需要调用者在调用sdsinclen
接口之前,自己检查长度是否合法。
static inline void sdssetalloc(const sds s, size_t newlen);
给定一个sds数据,以及一个新的长度newlen,将sds的header中的alloc字段设置为newlen。通过源码,我们可以发现,SDS_TYPE_5
类型的sds
数据与其他类型的sds
数据不论在sdshdr
结构,还是基础操作接口的处理,均有很大的差异。Redis在2015年7月15日的提交中引入了这个新的sds
类型,作者自己给出的提交信息是:
A new type, SDS_TYPE_5 is introduced having a one byte header with just the string length, without information about the available additional length at the end of the string.
结合后续其他操作接口对于SDS_TYPE_5
类型的处理,我们可以认为,这个类型的sds
数据,主要用于存储长度不超过32个字节,并且不会重新分配缓存的数据。对此,Redis的作者也给出了建议:
Don't use TYPE 5 if strings are going to be reallocated, since it sucks not having a free space left field.
构造与释放Strings的操作函数
static inline int sdsHdrSzie(char type);
static inline char sdsReqType(size_t string_size);
上述两个在src/sds.c头文件中定义的两个静态函数,分别用于返回一个特定HeaderType的头部结构体长度,以及根据一个string_size
的长度,返回合适的HeaderType。
sds sdsnewlen(const void *init, size_t initlen);
sds sdsempty(void);
sds sdsnew(const char* init);
sds sdsdup(const sds s);
void sdsfree(sds s);
其中sdsnewlen
函数,是这一系列函数的基础,其作用是,给定一段初始化内存的头指针init
,以及初始长度initlen
,构建一个sds
数据。这个函数会根据你需要初始化数据的长度initlen
通过sdsReqType
接口来选择所使用的HeaderType,8位,16位,32位还是64位,使用s_malloc
调用,为其分配长度为headerSize + initlen + 1
的缓存,之所以需要多分配出一个字节的缓存,是因为在Redis中的sds
总是以0
作为结束标记的,因为需要为这个结束标记多分配出一个字节的缓存。同时由于sds
本质上是二进制安全的,这也就意味着在数据的中间也有可能会出现0
,故此这也是我们为什么在头部信息结构体中需要sdshdr.len
字段的原因。同时初始话Header中的type,len,alloc字段,并将init所指向的数据调用memcpy
拷贝到sds的缓冲区中,同时以0
作为结束标记(null-termined)。后续的三个接口都是通过调用sdsnewlen来完成相关功能的:
sdsempty
函数用来创建一个空的sds数据。sdsnew
函数可以从一个null-terminated的C风格字符串中创建一个sds数据。注意这个接口不是二进制安全的,因为其内部是使用strlen
来计算传入数据长度的。sdsdup
函数可以通过一个给定的sds数据,复制出一个新的sds数据并返回
最后一个接口sdsfree
函数通过调用s_free
接口来释放一个给定的sds
数据,需要注意的是,所释放的内容包括sds
头指针,以及其前面的Header数据的整个缓存。
用于调整Strings长度信息的操作函数
void sdsupdatelen(sds s);
通过对内部数据调用strlen
来更新sds
的长度,这个接口在sds
缓存被手动改写的情况下很有用。通常来说,这个接口用于缩短sds
的sdshdr.len
字段,但是这个接口不会对sdshdr.buf
中的数据进行修改。
void sdsclear(sds s);
用于清空一个sds
数据的内容,与sdsupdatelen
接口类似,这个函数不会释放或者修改已经存在的缓存。仅仅是将sdshdr.len
长度字段清零,但是空间还在。
sds sdsMakeRoomFor(sds s, size_t addlen);
sdsMakeRoomFor
这个接口用于扩大一个给定sds
数据的可用缓存空间,可以确保用户在调用这个接口之后,可以向缓存之中续写addlen
个字节的内容,但是这个操作不会改变已经使用的缓存的大小,也就是不会改变sdslen
调用的结果。
其中几个细节点:
- 如果当前
sds
的可用空间也就是sdsavail
的大小大于addlen
,那么该函数什么操作也不会执行。 - 同时为了减少重复分配缓存所带来的系统开销,
sdsMakeRoomFor
接口总是会多分配出一些预留(最多1MB字节)的缓存:
newlen = (len+addlen)
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
- 如果当前操作没有引起Header的升级,例如从8位Header升级到16位,那么会调用
s_realloc
接口为其增加缓存容量。 - 扩容操作永远不会使用
SDS_TYPE_5
类型的Header,因为该类型的Header无法保存可用缓存的大小,这也就意味着,如果使用SDS_TYPE_5
类型的sds
,那么每次进行append操作的时候,都会调用sdsMakeRoomFor
来重新分配缓存。那么对于一个SDS_TYPE_5
类型的sds
,在调用过一次sdsMakeRoomFor
之后,至少会被升级的SDS_TYPE_8
类型的sds
。 - 如果引发了Header的升级,那么会调用
s_malloc
接口来分配一个新的sds
,将原始数据拷贝进去,返回新的sds
指针,这也就意味着,调用者无法保证作为参数传入的sds
指针在调用结束后是否依然有效,因此比如使用函数的返回值来执行后续操作s = sdsMakeRoomFor(s, newlen);
。
对于接口sdsMakeRoomFor
,Redis的作者给出的建议是:
Don't call sdsMakeRoomFor() when obviously not needed.
也就是说,当我们能确保sds
中有足够的多余缓存时,那么就不要调用该接口。
sds sdsRemoveFreeSpace(sds s);
sdsRemoveFreeSpace
这个接口的用途是收缩sds的缓存大小,通过释放多余的可用空间,使之刚好保存sdslen
大小的数据。
其中的细节点:
- 如果收缩导致Header的降级,那么调用
s_malloc
接口重新分配一个新的sds
,拷贝数据后,返回新的sds
。 - 如果收缩没有导致Header的降级,那么直接调用
s_realloc
接口调整缓存大小,实现缓存释放。
size_t sdsAllocSize(sds s);
sdsAllocSize
这个接口用与返回分配给指定sds
数据的内存的总大小。
其中包含:
sds
指针前的Header
的大小- 缓存中已使用数据的大小
- 可用空间的大小
- 结束符
0
的大小
这个接口与sdsalloc
的区别是,sdsalloc
返回的是sdshdr.buf
分配的缓存的大小。
void* sdsAllocPtr(sds s);
sdsAllocPtr
这个接口返回一个sds
数据直接被分配的头指针,也就是Header的指针。
void sdsIncrLen(sds s, ssize_t incr);
可以理解为可以给指定的sds
的sdshdr.len
增加incr
的长度,同时会导致sdsavail
的可用空间减少,该接口只负责处理sds
数据的长度,而不会改动其内容。基本应用场景:
- 调用
sdsMakeRoomFor
函数为sds扩容 - 向sds的缓存之中写入数据
- 调用
sdsIncrLen
函数,调整写入数据之后的sdslen长度
oldlen = sdslen(s);
s = sdsMakeRoomFor(s, BUFFER_SIZE);
nread = read(fd, s+oldlen, BUFFER_SIZE);
/* ... check for nread <= 0 and handle it ... */
sdsIncrLen(s, nread);
这个接口与sdsinclen
类似,但是该接口更多的是提供给用户调用,其内部增加了长度校验机制,这就需要我们在调用前通过sdsMakeRoomFor
接口来确保可用缓存空间,或者手动检查incr
的大小,不能超过sdsavail
的大小,否则会触发断言机制。
喜欢的同学可以扫描二维码,关注我的微信公众号,马基雅维利incoding