以下的总结,基于redis源码4.0.9版本
1、redis的底层存储数据结构
简单动态字符串(SDS)、链表、字典、跳跃表、整树集合、压缩列表等
2、redis的数据结构对象
字符串、列表、哈希、集合、有序集合等
- 简单动态字符串(sds)
SDS通过字符串的长短不同实现了5中不同的数据结构,其不同在于头部用来标识字符串长度的类型,主要用来减小结构体的大小,节省内存。
例如:长度小于32的字符串,则会使用sdshdr5结构体,sdshdr5结构体特殊在并没有len
和alloc
属性,其巧妙的使用了flags
的高5位用来标识字符串的长度。
sdshdr5 ----> <32
sdshdr8 ----> <256
sdshdr16 ----> < 2^16
sdshdr32 ----> < 2^32
sdshdr64 ----> < 2^64
基本数据结构如下:
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[];
};
len
代表字符串的真实长度
alloc
排除header和terminator外的分配长度
flags
类型标识了结构体的实际类型
buf[]
真实数据
sdshdr5
数据结构
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
字符串的扩容
/*
* 在必要情况下对SDS进行扩容
*
* 参数列表
* 1. s: 待扩容对SDS对字符串指针
* 2. addlen: 需要新加入字符串的长度
*
* 返回值
* 返回扩容后新的sds,如果没扩容则和入参sds地址相同
*/
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
// 首先计算出原SDS还剩多少可分配空间
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
// 已经够用的情况下直接返回
if (avail >= addlen) return s;
len = sdslen(s);
// 用sds(指向结构体尾部,字符串首部)减去结构体长度得到结构体首部指针
// 结构体类型是不确定的,所以是void *sh
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
// 如果新长度小于最大预分配长度则分配扩容为2倍
// 如果新长度大于最大预分配长度则仅追加SDS_MAX_PREALLOC长度
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 字符串的长度更改了,使用对头部类型可能也会变化
type = sdsReqType(newlen);
// 由于SDS_TYPE_5没有记录剩余空间(用多少分配多少),所以是不合适用来进行追加的
// 为了防止下次追加出现这种情况,所以直接分配SDS_TYPE_8类型
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
// 类型没变化则直接使用原起始地址重新分配下内存即可
newsh = s_realloc(sh, hdrlen+newlen+1);
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 */
// 头部类型有变化则重新开辟一块内存并将原先整个SDS拷贝一份过去
newsh = s_malloc(hdrlen+newlen+1);
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);
}
// 设置新对分配对总长度
sdssetalloc(s, newlen);
return s;
}
关键知识点:
1.SDS记录长度,O(1)获取整个长度
2.减少字符串修改时内存分配次数。内存预分配,分配一定长度的字符串,扩容时如果扩容需要的len小于SDS_MAX_PREALLOC,则扩大len的2倍,否则扩大SDS_MAX_PREALLOC(1M)(#define SDS_MAX_PREALLOC (1024*1024)
)。通过len和alloc实现惰性释放,字符串删除后,长度不变,便于下次使用。
3.二进制安全。不是靠空字符来判断字符串的结束的,而是通过len这个属性。
4.杜绝缓存去溢出。字符串合并前,会检查要当前字符串的长度,不足,扩容。规则如 2.