数据结构 -- 简单动态字符串(Simple Dynamic String -- SDS)

简单动态字符串(Simple Dynamic String – SDS)

虽然Redis使用C语言编写,但是并没有直接采用C语言中的字符串表示方式(\0结尾),而是使用了自己实现的简单动态字符串(SDS)。SDSRedis的默认字符串表示,它可以存储二进制数据,并且可以动态地调整字符串的长度。

定义

可以在src/sds.h中找到SDS的定义(为了版面整洁这里就只展示部分定义):


typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
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[];
};

sds其实就是一个char *,这个指针其实就是指向SDS结构体实例的尾部即buf。值得注意的是sds依然保持了C字符串的特性(即以\0结尾),保持这一特性的好处是,sds可以重用部分C字符串函数。在SDS结构体的定义中,首先__attribute__ ((__packed__))告知编译器,不要因为内存对齐而在结构体中填充字节,以保证内存的紧凑,这一点为后续从sds获取SDS结构体实例的首地址提供的便利。

Redis定义不同SDS结构体是为了针对不同长度的字符串,使用合适的lenalloc属性类型,最大限度地节省内存。

头部信息

Redis使用一些宏定义来标识和获取SDS的头部信息

#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
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

因为SDS的头部信息和buf是一段连续的内存,因此通过sds[-1]即可得到flag,将 flagSDS_TYPE_MASK进行&操作即可得到SDS的类型。由sds再减去sizeof(SDS)就能找到SDS的起始地址。获取SDS的总空间,剩余空间,有效长度等操作都类似,因此这里只给出获取SDS的有效长度的函数,其余函数可以在src/sds.c中找到。

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];    // 因为SDS内存紧凑的,因此flag在s[-1]的位置
    // SDS_TYPE_MASK的二进制位为00000111,因此flags&SDS_TYPE_MASK的结果为flags的低三位,即SDS类型
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

构建

构建SDS的方式有多种,但最终都会调用_sdsnewlen函数。由于篇幅原因,函数做出了部分删减,但不影响对源代码的理解。

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);    // 获取SDS类型
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);  // 获取SDS头部长度
    unsigned char *fp; /* flags pointer. */
    size_t usable;

    assert(initlen + hdrlen + 1 > initlen); // 防止数据溢出
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable); // 分配内存
    if (sh == NULL) return NULL;    // 内存分配失败
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;   // buf首地址
    fp = ((unsigned char*)s)-1; // buf首地址-1即为flag首地址
    usable = usable-hdrlen-1;   // 可用空间
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    // 根据SDS类型,初始化SDS
    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 = usable;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);   // 将数据拷贝到buf中
    s[initlen] = '\0';  // 保持了`C`字符串的特性(即以`\0`结尾)
    return s;
}

扩容

SDS的字符串进行修改过程中,会经常出现对字符串的增长、缩短等操作。如果每次增长、缩短都重新分配或释放内存,那必然会带来性能损失。因此SDS在使用了空间预分配惰性空间释放,来避免对内存频繁的申请和释放操作。SDS扩容的函数为_sdsMakeRoomFor,由于篇幅原因,函数做出了部分删减,但不影响对源代码的理解。

  • 空间预分配
    • SDSAPI对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。此策略可以减少连续执行字符串增长操作所需的内存重分配次数。
  • 惰性空间释放
    • SDSAPI对一个SDS进行缩短时,程序并不立即回收多出来的字节,而是通过alloclen的差值,将这些字节数量保存起来,等待将来使用
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);     // 获取SDS剩余空间
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;   // 获取SDS类型
    int hdrlen;
    size_t usable;
    if (avail >= addlen) return s;  // 剩余空间足够,直接返回
    len = sdslen(s);    // 获取SDS有效长度 -- 已经使用的长度
    sh = (char*)s-sdsHdrSize(oldtype);  // 获取SDS首地址
    reqlen = newlen = (len+addlen); // 新的SDS长度 -- 不含头部和'\0'
    assert(newlen > len);   // 防止数据溢出
    if (greedy == 1) {  // greedy为1的时候 分配比需要空间更多的空间 防止下次修改再次分配
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
    }
    type = sdsReqType(newlen);  // 更加长度获取SDS类型 -- 更新后的SDS类型
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    hdrlen = sdsHdrSize(type);  // 获取新类型SDS头部长度
    assert(hdrlen + newlen + 1 > reqlen);  // 防止数据溢出
    if (oldtype==type) {
        // 更新前后SDS头部不变,使用realloc直接进行扩容
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        // 更新前后SDS头部发生变化,需要使用malloc重新分配内存,然后将数据拷贝到新的内存中
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);  // 拷贝buf数据
        s_free(sh); // 释放老的数据 -- sh为SDS首地址,因此次释放的是整个SDS = 头部 + buf + '\0'
        s = (char*)newsh+hdrlen;    // 更新buf首地址
        s[-1] = type;   // 更新flag
        sdssetlen(s, len);  // 设置新的SDS的len
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable); // 设置新的SDS的alloc
    return s;
}

二进制安全

C语言的字符串中的字符必须符合某种编码(ASCII),并且除了字符串末尾的空字符,其他位置不能包含空字符,否则,会出现数据被截断的情况。
sdshdr
如上图所示,如果使用C字符串所用的函数来识别,只能读取到hello,后面的redis会被忽略,这个限制使得C字符串只能保存文本数据,而不能保存图片、视频、压缩文件等二进制数据。而SDSAPI都会以二进制的方式来处理字符串,并且SDS是以len来判断字符是否结束,因此SDS是二进制安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值