简单动态字符串 SDS

  1. 数据结构

  2. https://redis.io/topics/data-types-intro 介绍数据类型

  3. redis为了节省内存,针对不同长度的数据采用了完全不同的数据类型.

  4. 1
    2
    3
    4
    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为例子

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    typedef char* sds
    /*
     __attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法
    */
    struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 数据长度 */
    uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度*/
    unsigned char flags; /* 3 lsb of type, 5 unused bits,小端*/
    char buf[];//变长数据
    };
    
  5. 空间扩容

  6. 当当前有效长度大于等于新增长度时,会直接返回数据

  7. 更新之后,判断新旧类型是否一致

    1. 一致使用s_realloc 2. 否则使用malloc+free
  8. 增加步长

    1. 新增长度小于预分配的长度,会自动扩大一倍 2. 如果长度大于等于预分配的长度,每次加预分配长度
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
```
//创建新的字符串
    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        //根据长度获取sds的类型
        char type = sdsReqType(initlen);
        /* Empty strings are usually created in order to append. Use type 8
         * since type 5 is not good at this. */
        //sds_type_5这个类型是不会使用的啦.
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
        //根据类型得到头部的长度,还记得前面的类型声明吗?
        int hdrlen = sdsHdrSize(type);
        unsigned char *fp; /* flags pointer. */

        //分配空间: 头部大小+数据大小+'\0'(空终止符,为了c语言字符串兼容)
        sh = s_malloc(hdrlen+initlen+1);
        //如果没有初始化数据之间全部清空
        if (!init)
            memset(sh, 0, hdrlen+initlen+1);
        if (sh == NULL) return NULL;
        //s指向buf, 返回的sds就是这个s
        s = (char*)sh+hdrlen;
        fp = ((unsigned char*)s)-1;
        switch(type) {
            case SDS_TYPE_5: {
              //记得吗?对于SDS_TYPE_5类型,长度是放在flags字段里的
                *fp = type | (initlen << SDS_TYPE_BITS);
                break;
            }
            case SDS_TYPE_8: {
              //这里是新定义了一个相对应类型的sh
                SDS_HDR_VAR(8,s);
                //设置长度
                sh->len = initlen;
                //设置分配的大小(排查头部跟空终止符)
                sh->alloc = initlen;
                //设置类型
                *fp = type;
                break;
            }
            case SDS_TYPE_16: {
                SDS_HDR_VAR(16,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_32: {
                SDS_HDR_VAR(32,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_64: {
                SDS_HDR_VAR(64,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
        }
        //如果有初始化值,则执行memcpy复制数据
        if (initlen && init)
            memcpy(s, init, initlen);
        //设置终止符,这里并不会有两个'\0',因为前面只复制了initlen个字符
        s[initlen] = '\0';
        //返回sds,实际执行buf
        return s;
    }
  void sdsfree(sds s) {
      if (s == NULL) return;
      //得到真正的头部指向,然后释放
      s_free((char*)s-sdsHdrSize(s[-1]));
  }
  //字符串增长
  sds sdsMakeRoomFor(sds s, size_t addlen) {
      void *sh, *newsh;
      //获取剩余的空间,其实就是alloc-len
      size_t avail = sdsavail(s);
      size_t len, newlen;
      //保持老的类型,因为字符串空间加长后有可能类型变化
      char type, oldtype = s[-1] & SDS_TYPE_MASK;
      int hdrlen;

      /* Return ASAP if there is enough space left. */
      //如果空间足够直接返回
      if (avail >= addlen) return s;

      len = sdslen(s);
      sh = (char*)s-sdsHdrSize(oldtype);
      newlen = (len+addlen);
      //如果小于1024*1024直接翻倍空间
      if (newlen < SDS_MAX_PREALLOC)
          newlen *= 2;
      else
          newlen += SDS_MAX_PREALLOC;
      //根据长度获取sds的类型
      type = sdsReqType(newlen);

      /* Don't use type 5: the user is appending to the string and type 5 is
       * not able to remember empty space, so sdsMakeRoomFor() must be called
       * at every appending operation. */
      if (type == SDS_TYPE_5) type = SDS_TYPE_8;
      //获取头部长度
      hdrlen = sdsHdrSize(type);
      if (oldtype==type) {
        //如果类型不变
          newsh = s_realloc(sh, hdrlen+newlen+1);
          if (newsh == NULL) return NULL;
          s = (char*)newsh+hdrlen;
      } else {
          /* Since the header size changes, need to move the string forward,
           * and can't use realloc */
        //如果类型已经改变
          newsh = s_malloc(hdrlen+newlen+1);
          if (newsh == NULL) return NULL;
          //复制数据
          memcpy((char*)newsh+hdrlen, s, len+1);
          //释放老的数据
          s_free(sh);
          s = (char*)newsh+hdrlen;
          //设置类型s[-1]实际执行flags
          s[-1] = type;
          //设置新字符串的长度
          sdssetlen(s, len);
      }
      //设置新字符串分配空间大小
      sdssetalloc(s, newlen);
      return s;
  }
```
  1. 空间锁容

在进行trim操作的时候,采用的是惰性空间释放,即不会立即使用内存冲分配来回收缩短的字节,只是进行移动和标记,并修改数据长

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//缩小空间,主要根据len字段重新分配空间
  sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    //获取长度
    size_t len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    //根据长度获取sds的类型
    type = sdsReqType(len);
    //获取头部长度
    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
      newsh = s_realloc(sh, hdrlen+len+1);
      if (newsh == NULL) return NULL;
      s = (char*)newsh+hdrlen;
    } else {
      //类型改变
      newsh = s_malloc(hdrlen+len+1);
      if (newsh == NULL) return NULL;
      memcpy((char*)newsh+hdrlen, s, len+1);
      s_free(sh);
      s = (char*)newsh+hdrlen;
      s[-1] = type;
      sdssetlen(s, len);
    }
    //分配空间跟长度一样
    sdssetalloc(s, len);
    return s;
  }
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) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}
  1. 优点

    1. 常量获取字符串的长度

    2. 减少缓冲区溢出

    3. 减少字符串修改带来的内存频繁分配次数

    4. 二进制操作安全:可以保持文本数据,也可以保持任务格式的二进制数据

    5. sds是char*的别名,可以理解为分配的是一块连续内存(表头+数据),根据局部性原理可以提高访问速度。数据存储不使SDS_TYPE_5,因为使用该类型每次新数据时,都需要进行扩充利用C语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的,

      这里可以看到一个很神奇的操作

      1
      2
      3
      4
      5
      
      /* sdsalloc() = sdsavail() + sdslen() */
      static inline size_t sdsalloc(const sds s) {
          unsigned char flags = s[-1];
          return 0;
      }
      

      这里利用了c语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的

      这里关于s[-1]的操作是因为参考前面sdshdr8的定义,s所指向的是buf位置,此处的-1指向了前面一个flags