redis源码学习之sds简单动态字符串

我就是我 2022-07-13 14:51 358阅读 0赞

参考用书,《Redis设计与实现》黄建宏著。

虽然没有实际在项目中用过redis,不过很多人推荐学习redis的代码。

Redis使用C语言编写,里面有很多跟C语言相似的内容,也兼容C语言的函数,代码量不大。

简单动态字符串

用来保存key-value,在redis底层key和value用SDS保存。
SDS是一个结构体,它的定义如下:

  1. /*
  2. * 保存字符串对象的结构
  3. */
  4. struct sdshdr {
  5. // buf 中已占用空间的长度
  6. int len;
  7. // buf 中剩余可用空间的长度
  8. int free;
  9. // 数据空间
  10. char buf[];
  11. };

实际上,redis中还进行了一次类型重定义:

  1. typedef char * sds;

这样,用sds指向buf[],事实上,有时候我们只关注buf里面的内容,而并不关注,已占用的空间和未使用的空间,不过,如果我们要获取这两个成员变量时怎么办?
redis的做法也很巧妙,直接计算地址。

  1. static inline size_t sdslen(const sds s) {
  2. struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
  3. return sh->len;
  4. }
  5. static inline size_t sdsavail(const sds s) {
  6. struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
  7. return sh->free;
  8. }

SDS的特性

1.SDS的字符串都是null-terminated。

也就是说,它的结尾都是'\0',这一点肯C语言是一样的,因此C语言的很多汗是它是兼容的。比如,从SDS中的申请空间API中可以看到:

  1. sds sdsnewlen(const void *init, size_t initlen) {
  2. struct sdshdr *sh;
  3. if (init) {
  4. // zmalloc 不初始化所分配的内存
  5. sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
  6. } else {
  7. //
  8. // zcalloc 将分配的内存全部初始化为 0
  9. sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
  10. }//分配空间时要考虑结构体的大小+数据长度+1的'\0'
  11. //...
  12. //...
  13. // 以 \0 结尾
  14. sh->buf[initlen] = '\0';
  15. // 返回 buf 部分,而不是整个 sdshdr
  16. return (char*)sh->buf;
  17. }

但是SDS是二进制安全的,也就是说’\0’可以出现在字串中间。

2.惰性空间释放

书中提到这种情况下并没有直接释放空间,结构体中的len成员设置为0,free设置为len,看了源码之后确实如此:

  1. void sdsclear(sds s) {
  2. // 取出 sdshdr
  3. struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
  4. // 重新计算属性
  5. sh->free += sh->len;
  6. sh->len = 0;
  7. // 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
  8. sh->buf[0] = '\0';
  9. }

3.空间分配策略

这部分有点类似STL中的vector重新申请空间,比如说在一些函数调用例如sdscat,源空间不够追加时,需要扩容。redis设计了sds sdsMakeRoomFor(sds s, size_t addlen),很形象。
策略很简单:
1.如果追加的内容小于sds中剩余的空间,即addlen<free,则直接返回无需扩容。
2.如果追加后的内容长度newlen小于1MB,则再分配newlen大小的空间:newlen*=2
3.如果追加后的长度大于1MB,则再分配1MB的空间。newlen+=1024*1024

关于这个1MB,在redis中有一个宏

  1. #define SDS_MAX_PREALLOC 1024*1024
  2. //-------------------------------------------//
  3. sds sdsMakeRoomFor(sds s, size_t addlen) {
  4. struct sdshdr *sh, *newsh;
  5. // 获取 s 目前的空余空间长度
  6. size_t free = sdsavail(s);
  7. size_t len, newlen;
  8. // s 目前的空余空间已经足够,无须再进行扩展,直接返回
  9. if (free >= addlen) return s;
  10. // 获取 s 目前已占用空间的长度
  11. len = sdslen(s);
  12. sh = (void*) (s-(sizeof(struct sdshdr)));
  13. // s 最少需要的长度
  14. newlen = (len+addlen);
  15. // 根据新长度,为 s 分配新空间所需的大小
  16. //#define SDS_MAX_PREALLOC 1024*1024
  17. if (newlen < SDS_MAX_PREALLOC)
  18. // 如果新长度小于 SDS_MAX_PREALLOC
  19. // 那么为它分配两倍于所需长度的空间
  20. newlen *= 2;
  21. else
  22. // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
  23. newlen += SDS_MAX_PREALLOC;
  24. // T = O(N)
  25. newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
  26. // 内存不足,分配失败,返回
  27. if (newsh == NULL) return NULL;
  28. // 更新 sds 的空余长度
  29. newsh->free = newlen - len;
  30. // 返回 sds
  31. return newsh->buf;
  32. }

总结

SDS这部分代码比较简单,还需要考虑的问题就是:
redis对malloc、free等这些空间管理函数进行了一层封装。

发表评论

表情:
评论列表 (有 0 条评论,358人围观)

还没有评论,来说两句吧...

相关阅读