1、目标
本文的主要目标是下载Redis最新源码并分析SDS和IntSet数据类型的底层源码
Redis的github官网是:https://github.com/redis/redis
点击Release最新的版本7.2.5
点击Source下载Redis7.2.5版本的源码,并用VSCODE打开,主要源码内容在src目录下
查看源码用VSCODE,VSCODE返回前一个位置的快捷键是alt+左箭头
2、SDS简单动态字符串
SDS数据类型是二进制安全的,获取字符串长度的时间复杂度是O(1),它是动态扩容的,可以减少内存分配的次数,动态扩容可以避免缓冲区溢出
简单动态字符串SDS是二进制安全的,二进制安全指的是不会将数据中的任何字节视为特殊字符或字符串结束符,Redis的SDS数据类型不会将\0视为字符串结束符,因此SDS数据类型可以存储任意数据包括\0
那SDS数据类型怎么知道这个数据什么时候读取结束? SDS有一个len属性表示存储的字符串实际长度,根据这个len属性可以知道这个数据什么时候读取结束,而不是根据\0这个字符串结束符保证了数据的二进制安全,并且获取字符串长度的时间复杂度是O(1)
SDS动态扩容是修改字符串的时候不会缓存区溢出,可以动态扩容,当修改字符串的时候会先判断内存空间是否足够如果不足就会申请内存空间,如果新字符串的长度小于1M就申请内存空间是2倍,如果新字符串的长度大于等于1M就申请内存空间多1M,可以减少内存分配次数
缓冲区溢出指的是修改字符串的时候新字符串长度过长会溢出内存空间,导致相邻的字符串被修改了
2.1 SDS数据类型
SDS数据类型和方法都定义在sds.c和sds.h文件中,其中sdshdr8结构体定义在sds.h文件中
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[];
};
sdshdr8数据类型是SDS数据的底层结构,它是一个结构体,它包括len表示SDS数据实际使用的字节长度,alloc表示SDS数据内存分配的字节长度,一般alloc都超过len很多这样可以减少内存分配的次数,flags表示SDS数据的类型有5位的、8位的、16位、32位等类型,buf是字符数组用来保存实际的字符串
sds.h文件中声明的主要方法是上图这些,重点分析一下sdsnew方法和sdscat方法
2.2 sdsnew方法
/* 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);
}
sdsnew方法会调用sdsnewlen方法,可以创建一个SDS数据
sds sdsnewlen(const void *init, size_t initlen) {
return _sdsnewlen(init, initlen, 0);
}
sdsnewlen方法会创建一个SDS数据,会调用_sdsnewlen方法
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;
}
_sdsnewlen方法会先确定SDS数据的类型,然后计算SDS数据的字节长度,接着申请内存空间,然后根据类型给sdshdr8结构体的len设置实际长度,给alloc设置申请的内存空间大小,接着用memcpy方法复制字符串数据,最后结束加上\0表示字符串结束并返回
2.3 sdscat方法
/* Append the specified null terminated C string to the sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sdscat方法可以将字符数组t添加到SDS字符串s后面
sdscat方法会调用sdscatlen方法,会调用sdsMakeRoomFor方法判断是否需要申请内存空间,如果不需要就返回null,如果需要就调用memcpy方法复制数据并设置字符串结束标志\0,\0的作用是Redis的SDS类型可以重用一部分string.h的函数
其中,memcpy(s+curlen, t, len) 的含义是将源地址 t 开始的 len 个字节的数据复制到目标地址 s + curlen 开始的内存位置
2.3.1 sdscatlen调用sdslen方法
计算SDS数据的字节长度会调用sdslen方法,根据宏定义调用sdshdr8结构体的len属性并返回
其中,sdshdr8结构体前面已经介绍过了
2.3.2 sdscatlen调用sdsMakeRoomFor方法
/* Enlarge the free space at the end of the sds string more than needed,
* This is useful to avoid repeated re-allocations when repeatedly appending to the sds. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
return _sdsMakeRoomFor(s, addlen, 1);
}
sdsMakeRoomFor方法会调用_sdsMakeRoomFor方法,它的作用是扩充空闲空间,在多次修改SDS数据时可以避免多次的重新分配内存空间的操作,因为内存分配是耗时的,减少内存分配次数可以让Redis的SDS数据的修改变得很快
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;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
if (greedy == 1) {
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);
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh == NULL) return 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;
}
addlen是需要额外添加的长度,greedy默认是1,先判断当前可用空间avail是否足够,如果足够则直接返回原字符串s就不会内存分配了,如果不足会先计算新字符串的长度newlen,然后判断新字符串的类型进行内存复制,最后设置新字符串SDS数据的alloc属性是新字符串的长度表示可用空间大小并返回SDS数据
计算新字符串的长度newlen:新字符串的长度newlen的默认计算策略是如果新字符串的长度小于1M就会将newlen设置成原来的2倍,如果大于等于就会将newlen加1M,这样可以减少内存分配的次数
内存复制:如果旧字符串和新字符串的类型相同会调用s_realloc_usable方法申请内存空间,如果类型不同会调用s_malloc_usable方法申请内存空间并调用memcpy方法将旧字符串s开始的len+1个字节复制到新字符串的开始位置
2.3.3 sdscatlen调用memcpy方法
memcpy(s+curlen, t, len) 的含义是将源地址 t 开始的 len 个字节的数据复制到目标地址 s + curlen 开始的内存位置
2.3.4 sdscatlen调用sdssetlen方法
设置新字符串SDS数据的len属性是newlen,最后设置新字符串的结尾是\0并返回
\0的作用是Redis的SDS类型可以重用一部分string.h的函数
3、IntSet整数集合
IntSet整数集合只能存放整数,并且是升序的、唯一的,因此查询会二分查找到指定整数值,插入数据如果超过范围会升级编码方式,升级的作用是尽量节省内存空间,如果插入的数据都是2字节就一直采用这个编码方式
3.1 IntSet整数集合的数据类型
IntSet整数集合在intset.c和intset.h文件中,其中intset结构体在intset.h文件中
整数数组intset是一个结构体,intset整数集合存放数据是升序的、唯一的,因此查找数组元素用二分查找,添加元素如果数据类型超过类型可以表示的范围就升级
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t)) //2字节
#define INTSET_ENC_INT32 (sizeof(int32_t)) //4字节
#define INTSET_ENC_INT64 (sizeof(int64_t)) //8字节
编码方式encoding有三种,包括2字节、4字节、8字节
intset整数集合的方法如图所示,主要分析intsetAdd方法和intsetFind方法
3.2 intsetAdd方法
/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 1;
/* Upgrade encoding if necessary. If we need to upgrade, we know that
* this value should be either appended (if > 0) or prepended (if < 0),
* because it lies outside the range of existing values. */
if (valenc > intrev32ifbe(is->encoding)) {
/* This always succeeds, so we don't need to curry *success. */
return intsetUpgradeAndAdd(is,value);
} else {
/* Abort if the value is already present in the set.
* This call will populate "pos" with the right position to insert
* the value when it cannot be found. */
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
is = intsetResize(is,intrev32ifbe(is->length)+1);
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
_intsetSet(is,pos,value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
intsetAdd方法先获取value的编码方式,然后判断value如果大于当前编码的范围就调用intsetUpgradeAndAdd方法升级编码方式并插入数据,否则会调用intsetSearch方法查询value是否在is这个intset整数集合中,如果存在就插入失败因为intset整数集合存放的数据是唯一的,如果不存在就查询得到要插入的位置并重新申请内存空间并调用intsetMoveTail方法将一部分数据移动到尾部,为插入value腾出空间,最后调用_intsetSet方法在pos下标位置插入数据并返回
3.2.1 intsetAdd调用intsetUpgradeAndAdd方法
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding);
uint8_t newenc = _intsetValueEncoding(value);
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
intsetUpgradeAndAdd方法会升级编码方式并插入数据,先根据value获取新的编码方式,然后设置is的encoding属性是新的编码方式并申请内存空间,然后从后向前倒序将数组元素拷贝到尾部,这样可以避免覆盖数据,接着调用_intsetSet方法插入数据,最后修改is的length长度并返回
3.2.2 intsetAdd调用intsetMoveTail方法
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
void *src, *dst;
uint32_t bytes = intrev32ifbe(is->length)-from;
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
src = (int64_t*)is->contents+from;
dst = (int64_t*)is->contents+to;
bytes *= sizeof(int64_t);
} else if (encoding == INTSET_ENC_INT32) {
src = (int32_t*)is->contents+from;
dst = (int32_t*)is->contents+to;
bytes *= sizeof(int32_t);
} else {
src = (int16_t*)is->contents+from;
dst = (int16_t*)is->contents+to;
bytes *= sizeof(int16_t);
}
memmove(dst,src,bytes);
}
intsetMoveTail方法会调用memmove方法,memmove方法可以将长度为 bytes 的数据块从 src 复制到 dst
3.2.3 intsetAdd调用_intsetSet方法
static void _intsetSet(intset *is, int pos, int64_t value) {
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value;
memrev64ifbe(((int64_t*)is->contents)+pos);
} else if (encoding == INTSET_ENC_INT32) {
((int32_t*)is->contents)[pos] = value;
memrev32ifbe(((int32_t*)is->contents)+pos);
} else {
((int16_t*)is->contents)[pos] = value;
memrev16ifbe(((int16_t*)is->contents)+pos);
}
}
_intsetSet方法会将is整数集合的pos下标位置的元素设置成value
3.3 intsetFind方法
/* Determine whether a value belongs to this set */
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value);
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
intsetFind方法会先获取value的encoding编码类型,然后调用intsetSearch方法二分查询value所在的数组下标
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
_intsetValueEncoding方法会根据value查询intset整数数组的编码方式
/* Search for the position of "value". Return 1 when the value was found and
* sets "pos" to the position of the value within the intset. Return 0 when
* the value is not present in the intset and sets "pos" to the position
* where "value" can be inserted. */
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
/* The value can never be found when the set is empty */
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
/* Check for the case where we know we cannot find the value,
* but do know the insert position. */
if (value > _intsetGet(is,max)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
intsetSearch方法会查询 is 这个intset整数数组,二分查找value值,如果存在就返回1并将所在下标存放到pos,如果不存在就返回0并将第一个大于这个value的数组下标存放到pos,因此调用这个方法会根据返回值是0还是1判断是否查找到这个value,还会根据pos判断value所在数组下标