redis底层数据结构
文章目录
一、SDS是什么?
SDS,即Simple Dynamic String,简单动态字符串,是redis为了克服C语言原生字符串的弊端所设计的一种新的数据结构。
二、什么要用SDS?
C语言字符串存在的问题:
- 获取字符串长度需要通过运算,时间复杂度O(N)。
- 非二进制安全。某二进制字符串里面可能也存在’\0’,会导致读取提前结束。
- 不可修改。char *str = “hello”,这是一个字面量,存放在常量区。
SDS的优势:
- 获取字符串长度的时间复杂度为O(1),因为sds的len记录了sds中数据的长度。
- 支持动态扩容。
- 减少了内存分配次数——内存预分配。
- 二进制安全,因为sds不是以’\0’为字符串结束标志,而是通过len来判断结束。
三、SDS数据结构
1. sds header
// 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[]; /* 真实的数据 */
};
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[];
};
redis根据所存储的数据类型,将sds划分为四种,分别为sdshdr8、sdshdr16、sdshdr32、sdshdr64。虽然64位类型下最大字符串长度为2^63-1,但redis依然限制了字符串最大长度为512MB。通过命令config get proto-max-bulk-len
可以查询到值为536870912。
在每一种sdshdr里,都包含len、alloc、flags和buf,其中len + alloc + flags被称为header。
- len:buf中已保存的字符串字节数,不包含结束标识。
- alloc:buf申请的总的字节数,不包含header和结束标识。alloc分配的内存会超过buf的长度
- flags:不同的sds的头类型,用来控制sds的头大小。
- buf:字符数组,存放真实数据。
根据以上代码可知四种sds的header大小分别为:
- sdshdr8:1 + 1 + 1 = 3byte
- sdshdr16:2 + 2 +1 = 5byte
- sdshdr32:4 + 4 + 1 = 9byte
- sdshdr64:8 + 8 + 1 = 17byte
__attribute__((__packed__)):是 GCC/Clang 编译器的一个扩展,它的作用是告诉编译器对结构体进行紧凑对齐,即禁止对齐填充。默认情况下,编译器可能会在结构体字段之间插入填充字节,以使字段按照一定的内存对齐规则排列(例如,某些数据类型需要 4 字节对齐)。使用 __packed__ 属性后,结构体的成员将紧密排列在内存中,不会有额外的填充字节。
flags的值为四个宏定义:
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
2. sds与sdshdr的转化
sds的定义:
typedef char *sds;
sds本质是一个字符数组。但sds.c源码中操作的都是sds,并不是sdshdr。为了将其转化成指定的sdshdr,redis使用了如下的宏定义:
#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_HDR_VAR为例):
- SDS_HDR_VAR(T,s):声明一个宏,接受两个参数T和s。T代表sdshdr类型,s代表sds指针。
- struct sdshdr##T *sh:通过
##
操作符拼接出一个新的结构体类型。例如,T为8时,sh就是sdshdr8。 - (void*)((s)-(sizeof(struct sdshdr##T))):
- void*:强制类型转换。
- (s):传递给宏的 sds 类型指针,它指向字符串的实际数据部分,也就是sdshdr中的buf。
- sizeof(struct sdshdr##T):sdshdr的大小。
- (s)-(sizeof(struct sdshdr##T)):用指向的buf的地址 减去 sdshdr占用的字节数,可以将指针移到sdshdr结构体的开头。
用图来解释:
在sdshdr结构体内存布局中,上面的内存地址小,下面的内存地址大,用s的地址 - 上面header的大小就能得到sdshdr的地址。
由于buf是个灵活数组,因此sizeof(sdshdr)并不会计算buf的内存大小。
灵活数组:Flexible Array Member,简称 FAM,是指在结构体中声明的没有固定大小的数组。它通常是结构体的最后一个成员,并且没有大小([]),它允许结构体的大小在运行时根据实际需要动态分配更多的内存空间
注意:s[-1]获取到的是flag。
3. 内存预分配
当redis给sds追加一段字符串时,首先会申请新的内存空间:
- 如果添加字符串后的新字符串小于1M,则新空间为扩展后字符串长度的两倍;
- 如果添加字符串后的新字符串大于1M,则新空间为扩展后字符串长度+1M。
源码如下:
// sds.c
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh; //sh:旧的sdshdr头指针,newsh:新的sdshdr头指针
size_t avail = sdsavail(s); //获取字符串当前可用的空间。
size_t len, newlen, reqlen; //len是当前字符串的长度,newlen是当前字符串长度加上要增加的长度addlen,reqlen是字符串扩展后所需的总长度
char type, oldtype = s[-1] & SDS_TYPE_MASK; //type是新字符串的类型,oldtype是旧字符串的类型。SDS_TYPE_MASK是7
int hdrlen; //sdshdr的长度
size_t usable;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s; //如果剩余空间足够容纳要添加的字符串大小,直接返回字符串s,无需扩容
len = sdslen(s); // 获取sds的长度,也就是sdshdr中buf的大小
sh = (char*)s-sdsHdrSize(oldtype); //将指针移动到sdshdr头部,原理跟第2节讲的一样
reqlen = newlen = (len+addlen); //新字符串所需的空间
assert(newlen > len); /* Catch size_t overflow */
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC) //如果新字符串的长度 没超过 sds预分配的最大空间1M
newlen *= 2;//将当前新字符串长度扩大一倍
else
newlen += SDS_MAX_PREALLOC;//如果新字符串的长度 超过 1M,则将其大小 +1M
}
type = sdsReqType(newlen); //获取扩容后的sdshdr类型
/* 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); //扩容后的sdshdr结构体大小
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) { //如果扩容前后sdshdr的类型相同,给sdshdr头部所指的区域分配内存
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen; //指向新的sdshdr中的buf位置
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
/* 因为sdshdr类型发生了变化,无法直接使用realloc,因此需要重新分配内存,s_malloc_usable,
* 然后将原来的字符串内容拷贝到新分配的内存中*/
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);//由于len都不包含buf的结束字符,因此需要+1,将其算进去
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);//将s中len+1大小的内容拷贝到新内存区域
s_free(sh);//释放掉旧sdshdr中buf所在的内存
s = (char*)newsh+hdrlen;//指向新的sdshdr中的buf位置
s[-1] = type;//设置新类型
sdssetlen(s, len);//设置新长度,注意,这个函数的功能只是分配内存,并没有将新添加的字符串放进去,因此这里的长度还是len
}
usable = usable-hdrlen-1; //usable是可用内存的大小。如果分配的内存空间超过了类型的最大大小(sdsTypeMaxSize(type)),则将 usable 设置为最大可用大小。此处减去sdshdr和buf结尾字符串的内存,剩余的就是给buf分配的内存大小。
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
sdssetalloc(s, usable); //给新sds设置分配的内存大小
return s;
}
四、 一些函数和宏定义(后续再补充)
1. 宏定义
#define SDS_TYPE_MASK 7 //sdshdr类型掩码,
#define SDS_TYPE_BITS 3 //sdshdr类型位数因为sdshdr只有4种类型,用3个bit位即可表示
#define SDS_MAX_PREALLOC (1024*1024) //sds字符串最大预分配空间,1MB
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) //根据给定的 buf 指针(s)和类型(T),返回指向整个 sdshdr 结构体头部的指针
2. sdslen
// sds.h
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
作用:根据传入的sds,获取到sdshdr指针,然后返回sdshdr的长度。
3. sdsHdrSize
// sds.c
static inline int sdsHdrSize(char type) {
switch(type&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return sizeof(struct sdshdr5);
case SDS_TYPE_8:
return sizeof(struct sdshdr8);
case SDS_TYPE_16:
return sizeof(struct sdshdr16);
case SDS_TYPE_32:
return sizeof(struct sdshdr32);
case SDS_TYPE_64:
return sizeof(struct sdshdr64);
}
return 0;
}
作用:根据传入的type,返回对应的sdshdr的内存占用大小。这里的type就是sdshdr中的flag。
4. sdsReqType
// sds.c
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5)
return SDS_TYPE_5;
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}
作用:根据传入的字符串大小,返回sdshdr的类型。
5. sdssetlen
// sds.h
static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
}
}
作用:给sds设置新的长度