Redis(5.0.3)源码分析之sds对象


sds是redis中定义字符串对象,它比C中的字符串类型对象更为高效、安全。

  1. sds额外保存了字符串长度和内存分配大小等信息。获取长度就只用O(1)。
  2. sds对象中可能会包含多余空间,这样可以实现内存预分配和惰性删除,减少系统调用带来的开销。
  3. 由于sds记录了长度和剩余可用空间信息,所以strcat类操作也不会导致内存溢出问题。
  4. 能够复用C标准库里针对字符串的方法。

《Redis设计与实现》有比较详细介绍sds的使用场景和好处。但里面介绍的sds结构体在新版(5.0.x)中已经不一样了。

1
2
3
4
5
6
// 旧sds结构体
struct sdshdr {
  int len; // buf中已用字节长度
  int free; // buf中未用字节长度
  char buf[] // 存放实际字符串的地方
}

新版代码中提高了sds对内存的利用率,例如两个字节的字符串对应的sds对象,就没必要将len字段定义为int类型了,uint8足够。所以对于不同长度的字符串,实际结构体中len这些字段类型也不同。但sds却对上层使用方保持一致的接口,隐藏底层结构体差异性的细节。

基于5.0.3版本源码

 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
// 来看一下sds的定义
typedef char *sds; // 比较巧妙,sds直接被typedef为char*。那len这些信息存哪?

// 下面sdshdr5\sdshdr8\sdshdr16等等就是针对不同的字符串长度预先给出的sds定义
// __attribute__ ((__packed__)) 的作用是告诉GCC编译器,不要对此结构体做内存对齐。
// 同时看到free字段没了,改成alloc,并增加flags字段

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

但上层代码使用却是sds,是个char*,是怎么提取出len、alloc等字段信息呢?

其实sds是直接指向结构体里的buf数组。当取len等字段信息,只需要减去结构体长度,回退一下指针就行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 获取实际的结构体通过宏实现
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));

static inline size_t sdsavail(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
            return 0;
        }
        case SDS_TYPE_8: {
            // 这里做宏替换
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        // ...
    }
    return 0;
}

创建一个新字符串,怎么判断该选用哪个结构体?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// string_size就是目标字符串长度,比对一下,看用哪个长度的结构体
static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

现在可以看一下,是如何创建一个sds对象的

 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
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    // 判断该用哪种长度类型的结构体
    char type = sdsReqType(initlen);

    // 虽然有SDS_TYPE_5,但其实不会使用它
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 获取结构体长度
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    // 分配内存 字符串长度+结构体长度
    sh = s_malloc(hdrlen+initlen+1);
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;
    // 这里fp就是flags字段
    fp = ((unsigned char*)s)-1;
    // 接下来对结构体里的len、alloc、flags字段赋值吧
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            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;
        }
    }
    if (initlen && init)
    		// 字符串对象的内容需要初始化
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

内存复用的思想很常见,sds对象也不例外。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 将len字段设置为0,但内存空间不释放。方便下次直接复用
void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

// free方法才是真正释放内容的方法
void sdsfree(sds s) {
    if (s == NULL) return;
    // s[-1]就刚好指向了flags这个字段了
    s_free((char*)s-sdsHdrSize(s[-1]));
}