1、redis自定义了新的字符串机制SDS
Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串SDS的抽象类型
2、简单动态字符串SDS
SDS定义:
struct sdshdr { // buf 中已占用空间的长度,不包括末尾‘\0’ int len; // buf 中剩余可用空间的长度 int free; // 数据空间,一个char类型的数组 char buf[]; };
3、SDS与C字符串的区别
1)获取字符串长度的复杂度
在C字符串中,C字符串是以一个数组来保存的(如上图所示),因此需要获取字符串的长度,需要遍历整个数组,时间复杂度为O(N);
在SDS中,由于SDS结构体中保存了一个len变量,直接读取该变量即可知道字符串的长度,时间复杂度为O(1)
2)缓冲区溢出
由于C字符串在保存之前需要为这个字符串数组分配大小,如果一直往这个数组插入元素,则会造成缓冲区溢出,当然如果要避免缓冲区溢出,则需要先检查数组是否被填满,如果是,则重新手动分配一个更大的空间,然后将数据拷贝过去,再释放原数组;
SDS中,用户只需要往里面添加字符即可,看似不会再有缓冲区溢出,其实只是将缓冲区溢出检查,重新分配内存,拷贝数据,释放原缓冲区这些操作交给幕后去执行了,而不用用户去执行这一部分工作(有点类似于STL中vector的扩容机制)
3)减小修改字符串时带来的内存重分配次数
C字符串的缺陷:
执行增长字符串的操作时,程序需要先通过内存重分配来扩展底层数组的空间大小,否则可能会产生缓冲区溢出;
执行缩短字符串的操作时,程序需要通过内存重分配来释放字符串不再使用的那部分空间,否则可能会产生内存泄漏
SDS的空间预分配,
以修改后len是否大于1M,有两种分配方式:
如果修改后len长度将小于 1 M, 这时分配给free的大小和len一样, 例如修改过后为13字节, 那么给free也是13字节 . buf实际长度变成了 13 byte+ 13byte + 1byte = 27byte;
如果修改后len长度将大于等于1 M, 这时分配给free的长度为 1 M, 例如修改过后为30M, 那么给free是1M . buf实际长度变成了 30M + 1M + 1 byte;
在修改时, 首先检查空间是不是够, 如果足够, 直接使用, 否则执行内存重分配.
SDS惰性空间释放:
程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free变量将这些字节的数量记录起来,病等待将来使用;
SDS也提供了相应的API,让我们可以在有需要的时候,真正释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存泄漏
4)二进制安全
C字符串只能保存文本数据,因为C字符串遇到‘\0’即认为结束;SDS则是通过len变量来判断字符串结束位置的,因此SDS可以保存文本或二进制数据
5)SDS仅可以使用部分C字符串函数
SDS在字符串末尾添加‘\0’,主要是为了能够让那些保存文本数据的SDS可以重用一部分C字符串处理函数,而不用重写
4、扩容部分源码(具体的扩容机制):
/* Enlarge the free space at the end of the sds string so that the caller * is sure that after calling this function can overwrite up to addlen * bytes after the end of the string, plus one more byte for nul term. * * Note: this does not change the *length* of the sds string as returned * by sdslen(), but only the free buffer space we have. */ /* * 对 sds 中 buf 的长度进行扩展,确保在函数执行之后, * buf 至少会有 addlen + 1 长度的空余空间 * (额外的 1 字节是为 \0 准备的) * * 返回值 * sds :扩展成功返回扩展后的 sds * 扩展失败返回 NULL * * 复杂度 * T = O(N) */ 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); 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; }参考书籍《redis设计与实现》