数据结构
Redis是用C语言实现的,那么在C语言里面,怎么实现一个既能动态扩容,又得二进制安全的字符串呢?
二进制安全: 在C语言中,'\0'是用来表示字符串结束的,但是如果字符串本身的内容就包含这个'\0'的话,原始字符串就会被截断,这就是非二进制安全。
Redis设计了一个struct,既能支持动态扩容,又能保证二进制安全:
struct sds{
int len; //buf中已经占用的字节数
int free; //buf中剩余可用的字节数
char buf[]; //数据空间
}
SDS的结构如下,在64位操作系统中,len和free都是4个字节。
buf[]是一个柔性数组(flexible array),在结构体中,flexible array只能放在末尾。包含flexible array的结构体,通过malloc函数为柔性数组动态分配内存。
这种结构的好处:
- 有len和free变量的存在,所以可以很容易的统计出字符串的长度;
- 由于有len变量的存在,所以读取字符串也不依赖于'\0',保证了二进制安全;
- buf[]柔性数组支持动态扩容;
但是这种结构也有不好的地方,len和free都固定地各占4个字节,而在实际使用场景中,字符串往往并不长。比如假设一个字符串只有两个字符,可是如果此时len和free都是4个字节都话,它们各自能描述都长度范围为0-2^32-1,可是字符串就两个字符,用这么大的类型去描述有点太浪费空间了。
所以在Redis 5.0中,改变了这种结构,增加了一个flag变量,长度为最小的1字节。这个flag变量的低三位用来表示类型。
这个类型就是len的大小,如果字符串是长度小于31的短字符串,那么可以用小于1字节的类型去描述。Redis 5.0里面,根据字符串的长短,设计了5种类型:
#define SDS_TYPE_5 0 //表示长度小于31的短字符串,用flag的低5位来表示len,也就是2^5-1=31, 0-31的范围。
#define SDS_TYPE_8 1 //表示len需要一个字节,也就是字符串长度已经大于31了,用前面的类型已经不够了。
#define SDS_TYPE_16 2 //表示len需要两个字节
#define SDS_TYPE_32 3 //表示len需要8个字节
#define SDS_TYPE_64 4 //表示len需要16个字节
这就是为什么要用flag的低三位来表示类型,因为低三位能表示的值范围是0-2^3-1,即0-7。而这5种类型的值是0-4。低三位的范围能覆盖这个0-4的值,还能有一定的扩展。
下图是SDS_TYPE是SDS_TYPE_5的结构:
所以在这种超短型字符串的结构中,只有flag变量和flexible array buf[]。
源代码结构:
struct sdshdr5{
unsigned char flags; //低三位存储类型,高5位存储长度
char buf[]; //存放实际内容
}
而长度大于31的字符串,则len和free单独存放。不过在Redis 5.0中,已经没有free了,是alloc,buf的总长度。SDS_TYPE是SDS_TYPE_8的结构图如下:
源代码结构:
stuct sdshdr8{
uint8_t len; //已使用长度,用1个字节存储
uint8_t alloc; //总长度,用1个字节存储
unsigned char flags; //低三位存储类型,高5位预留
char buf[]; //存放实际内容
}
SDS_TYPE_16, SDS_TYPE_32, SDS_TYPE_64的结构图和源代码,与SDS_TYPE_8的一致,只有len和alloc的长度的差别。
stuct sdshdr16{
uint16_t len; //已使用长度,用2个字节存储
uint16_t alloc; //总长度,用2个字节存储
unsigned char flags; //低三位存储类型,高5位预留
char buf[]; //存放实际内容
}
stuct sdshdr32{
uint32_t len; //已使用长度,用4个字节存储
uint32_t alloc; //总长度,用4个字节存储
unsigned char flags; //低三位存储类型,高5位预留
char buf[]; //存放实际内容
}
stuct sdshdr64{
uint64_t len; //已使用长度,用8个字节存储
uint64_t alloc; //总长度,用8个字节存储
unsigned char flags; //低三位存储类型,高5位预留
char buf[]; //存放实际内容
}
所以除了sdshdr5,其他几个的结构都是一样的。