介绍
我们知道Redis是一个键值对数据库,当你执行如下命令时
set testKey testValue;
rpush fruits banana apple;
其中的键就是用sds(Simple Dynamic String,简单动态字符串)来实现的,redis中string类的值也是用SDS实现的(如上面的testValue)。我们来看一下SDS的底层数据结构是啥?
3.0版本及以前
struct sdshdr {
// buf数组中已使用字符的数量
unsigned int len;
// buf数组中未使用字符的数量
unsigned int free;
// 字符数组,用来保存字符串
char buf[];
};
如下是一个sds的示例
free:free为0,表示未使用的空间
len:这个sds保存了5字节长的字符串
buf:char类型的数组,前5个字节是字符串,后一个字节是\0,表示结尾,\0不计入总长度
当要存的字符串变大或者变小的时候,会造成频繁的内存分,进而影响性能。所以Redis通过空间预分配和惰性空间释放策略来避免内存的频繁分配
空间预分配
当sds的内容变大时,程序不仅会为sds分配修改所需要的空间,还会为sds分配额外的未使用的空间。
- 对sds进行修改之后,sds的长度(即len属性的值)小于1MB,程序分配和len属性同样大小的未使用空看,即sds len属性的值和free属性的值相同
- 对sds进行修改之后,sds的长度(即len属性的值)大于等于1MB,程序会分配1MB的未使用空间
惰性空间释放
当sds内容变小时,程序并不会释放缩短后剩余的空间,只是修改free属性,将未使用字符数量记录下来,等以后使用
3.0版本以后
上面的结构有改进的空间吗?如果让你来写sds,你会怎么写呢?
不同长度的字符串是否有必要占用相同大小的头部?
当字符串很小的时候,我们还得额外的使用8个字节(len和free各占4个字节),感觉有点太浪费了。
我们是否可以根据字符串的长度,来决定len和free占用的字节数呢?比如短字符串len和free的长度为1字节就够了。长字符串,用2字节或4字节,更长的字符串,用8字节。
那我们如何区分这3种情况呢?
很简单,再加一个一字节的字段标明类型就行了
所以在redis 3.2中,有如下5种类型
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
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[];
};
- len:buf数组中已使用字符的数量
- alloc:buf数组中已分配字符的数量
- flags:标识当前结构体的类型,低3位用作标识位,高5位预留(3个字节能保存的种类数为2的3次方即8,sds总共的种类数为5,所以用低三位就能用标识种类)
- buf:字符数组,用来保存字符串
可以看到sdshdr5比较特殊,并没有len和alloc两个字段,那它是如何存已使用和已分配的数量呢?
sdshdr5中,flags占用一个字节,低3位表示type,高5位表示长度,能表示的区间长度为(0~31,即25-1)
sdshdr5并没有存在alloc的值,因此它不会进行空间预分配和惰性空间空间释放,长度变动每次都重新申请内存
当字符串长度大于31时,flags的后5位就存不下了,所以我们就将len和free单独存放