一、数据结构
redis 为了节省内存,针对不同长度的数据结构采用不同的数据结构。如下共五种,但SDS_TYPE_5 并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放:
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
以 type = 1 为例:
typedef char* sds
// 告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是 GCC 特有的写法
struct __attribute__((__packed__)) sdshdr8 {
uint8_t len; /* 数据长度 */
uint8_t alloc; /* 去掉头和 null 结束符,有效长度+数据长度 */
unsigned char flags;
// 变长数据
char buf[];
}
二、空间扩容
- 当前有效长度 >= 新增长度,直接返回;
- 更新之后,判断新旧类型是否一致:
- 一致使用 remalloc,否则使用 malloc + free
- 当有效长度 >= 新增长度,直接返回
- 一致使用 remalloc,否则使用 malloc + free
- 增长步长:
- 新增后长度小于预分配长度(1024 * 1024),扩大一倍;
- 新增后长度大于等于预分配的长度,每次加预分配长度(减少不必要的内存)
sds sdsMakeRoomFor(sds s, size_t addlen){
// 当前有效长度 >= 新增长度,直接返回
if(avail > addlen) return s;
len = sdslen(s);
newLen = len + addLen;
// 新增后长度小于预分配长度,扩大一倍; SDS_MAX_PREALLOC = 1024 * 1024
if (newLen < SDS_MAX_PREALLOC)
newLen *= 2;
// 新增后长度大于预分配长度,每次加预分配长度(考虑到内存的占用,可能会 OOM)
else
newLen += SDS_MAX_PREALLOC;
type = sdsReqType(newLen);
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
// 新老类型一直使用 remalloc, 否则使用 malloc + free。 当前有效长度 >= 新增长度,直接返回
if(oldType == type)
newsh = s_realloc(sh,hdrlen + newlen + 1);
else
// 新老类型不一致需要分配新的内存
newsh = s_malloc(hdrlen + newlen + 1);
// 释放老的数据
s_free(sh);
sdssetalloc(s, newlen);
return s
}
三、空间缩容
在 trim 操作时,采用的是惰性空间释放:不会立即使用内存重分配来回收缩短的字节,只是进行移动和标记,并修改数据长度。
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s + sdslen(s) - 1;
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > sp && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep - sp) + 1);
if(s != sp) remmove(s, sp, len);
s[len] = '\0';
sdssetlen(s, len);
return s;
}
四、优点 ✅
- 常量获取字符串长度(len) ➡️ sds结构体中定义了字符串长度。
- 避免了缓冲区溢出 ➡️ 在扩容时,当 len > SDS_MAX_PREALLOC 时,不是乘以 len 的 2倍,而是加上一个 SDS_MAX_PREALLOC 的大小。
- 减少字符串修改带来的内存频繁重分配次数 ➡️ 扩容时判断 SDS 类型,若相同则 realloc,若不相同则 malloc 重新分配内存。
- 二进制操作安全:可以保持文本数据,也可以保持任意格式的二进制数据(如视频流数据)
- 以 ‘\0’ 结尾,使其兼容部分 C 字符串函数。
五、其他
- sds 是 char* 的别名,可以理解为分配的是一块连续内存(表头+数据),根据局部性原理可以提高访问速度。
- 数据存储不使用 SDS_TYPE_5 因为使用该类型每次新数据时,都需要进行扩充
- 利用 C 语言内存布局,在 SDS 结构体中使用一个 0 长度的数组,既可以达到变长,又能保证内存也是连续的。