转载

Redis中SDS简单动态字符串

0×00 SDS:简单动态字符串

SDS是Redis中实现的一种数据结构,用来存储字符串,代码实现如下:

// 文件路径:src/sds.h struct sdshdr {     // 记录buf数组中已使用字节的数量     // 等于SDS所保存字符串的长度     int len;      // 记录buf数组中未使用字节的数量     int free;      // 字节数组,用于保存字符串     char buf[]; };
// 文件路径:src/sds.h structsdshdr{  // 记录buf数组中已使用字节的数量  // 等于SDS所保存字符串的长度  intlen;  // 记录buf数组中未使用字节的数量  intfree;  // 字节数组,用于保存字符串  charbuf[]; }; 

0×01 SDS与C字符串的区别

1、常数复杂度获取字符串长度

常规C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,知道遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)。

而SDS使用结构体实现,结构体中的len属性直接记录了该SDS结构体中buf数组中已使用的长度,因此获取字符串长度时,只需要获取len属性的值,这个操作的复杂度为O(1)。

SDS结构体的实现确保了获取字符串长度的工作不会成为Redis的性能瓶颈。

2、杜绝缓冲区溢出

因为C字符串不记录自身的长度,所以当进行字符串复制的时候,如果分配内存不够,便有可能产生缓冲区溢出。

而在Redis中,当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。

Redis中 strcat 的实现代码:

sds sdscat(sds s, const char *t) {     return sdscatlen(s, t, strlen(t)); }
sds sdscat(sdss,constchar*t){     returnsdscatlen(s,t,strlen(t)); }

Redis中 sdscatlen 的实现代码:

sds sdscatlen(sds s, const void *t, size_t len) {      struct sdshdr *sh;      // 原有字符串长度 直接获取len属性     size_t curlen = sdslen(s);      // 扩展 sds 空间     // T = O(N)     s = sdsMakeRoomFor(s,len);      // 内存不足?直接返回     if (s == NULL) return NULL;      // 复制 t 中的内容到字符串后部     // T = O(N)     sh = (void*) (s-(sizeof(struct sdshdr)));     memcpy(s+curlen, t, len);      // 更新属性     sh->len = curlen+len;     sh->free = sh->free-len;      // 添加新结尾符号     s[curlen+len] = '/0';      // 返回新 sds     return s; }
sds sdscatlen(sdss,constvoid*t,size_tlen){  structsdshdr*sh;  // 原有字符串长度 直接获取len属性  size_tcurlen=sdslen(s);  // 扩展 sds 空间  // T = O(N)  s=sdsMakeRoomFor(s,len);  // 内存不足?直接返回  if(s==NULL)returnNULL;  // 复制 t 中的内容到字符串后部  // T = O(N)  sh=(void*)(s-(sizeof(structsdshdr)));  memcpy(s+curlen,t,len);  // 更新属性  sh->len=curlen+len;  sh->free=sh->free-len;  // 添加新结尾符号  s[curlen+len]='/0';  // 返回新 sds  returns; } 

3、减少修改字符串时带来的内存重分配次数

常规C字符串,再执行拼接操作或者截断操作时,通常会对数组进行内存重分配,而内存重分配操作涉及复杂的算法,并且可能执行系统调用,所以它通常是一个比较耗时的操作。

Redis作为数据库,会对数据进行频繁的修改,并且对速度要求极为严苛,所以每次修改字符串长度都需要进行内存重分配会对性能造成极大的影响。

因此,SDS实现了空间预分配和惰性空间释放两种优化策略。

1) 空间预分配

当SDS的API对一个SDS进行修改时,程序不仅会为SDS分配必须要的空间,还会为SDS分配额外的未使用空间。

额外分配未使用空间数量的规则:

当SDS的len属性值小于1MB,程序分配和len属性同样大小的未使用空间。

当SDS的len属性值大于1MB,程序将多分配1M的未使用空间。

通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。

2) 惰性空间释放

当对SDS进行字符串缩短操作时,SDS的API不会立即使用内存重分配回收多出来的字节,而是使用free属性将这些字节的数量记录起来,等待将来使用。

当然,SDS也提供了相应的API,可以用来真正释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。

0×02 总结

C字符串和SDS之间的区别

C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API是不安全的,可能造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需啊哟执行N次内存重分配
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

0×03 致谢

感谢huangz的《Redis设计与实现》,本文纯属读书笔记,资源均来源于书中。

原文作者:我才是二亮

原文链接: http://www.2liang.me/archives/269

转载必须在正文中标注并保留原文链接、作者等信息。

正文到此结束
Loading...