C语言的字符串是字符串长度+’\0’,这种形式使得获取数组长度的时间复杂度为 O ( n ) O(n) O(n)。而当想扩容的时候需要预先分配空间,一旦忘记分配,就会导致数组越界
简单动态字符串
Redis使用的是简单动态字符串(SDS),其定义如下:
struct sdshdr {
// 用于记录数组长度
int len;
// 用于记录数组种未使用的字节数
int free;
// 用于保存字符串列表
char buf[];
}
内存分配
其中Redis对于字符串修改导致的内存分配有一定的优化:
(1) 空间预分配
当修改的SDS长度len使用内存小于1MB的时候,会分配内存使得分配后SDS的len==free。如SDS的使用长度为14,分配后SDS的数组大小为14+14+1=29(一字节保存空字符)
当修改后SDS的len大于1MB,则会额外分配1MB的内存
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空余空间已经足够,无须再进行扩展,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
/******** 这个公式很常见,是通过数组的地址取出整个SDS的地址 *****/
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
/******************** 空间预分配逻辑 ******************/
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新长度小于 SDS_MAX_PREALLOC
// 那么为它分配两倍于所需长度的空间
newlen *= 2;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
/******************** 空间预分配逻辑 ******************/
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
(2)惰性释放
当字符串空间被缩短时,不释放里面的空间,而是保留这部分内存,修改free的值。下次当字符串长度增加时,就不需要再次分配了。
例如下面的方法,仅仅是将数组清空,但是并没有释放内存
void sdsclear(sds s) {
// 取出 sdshdr
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
// 重新计算属性
sh->free += sh->len;
sh->len = 0;
// 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
sh->buf[0] = '\0';
}
如果想要释放内存,可以使用如下方法
sds sdsRemoveFreeSpace(sds s) {
struct sdshdr *sh;
sh = (void*) (s-(sizeof(struct sdshdr)));
// 进行内存重分配,让 buf 的长度仅仅足够保存字符串内容
// T = O(N)
sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
// 空余空间为 0
sh->free = 0;
return sh->buf;
}
二进制安全
另外Redis的字符串是二进制安全的。即对于存进来的数据,不做任务解析,修改或者假设。比如’\0’对于C语言的字符串是有特殊意义的,表示字符串结束的位置。这样的规则使得其无法存储音频,压缩文件等的二进制数据,只能用于存储文本数据。
Redis使用数组不是存储的是一系列二进制数据,而不是用于保存字符串。数据被写入时是怎么样,它被读取的时候就是怎么样的。