Redis源码之——SDS

码字不易,转载请附原链,搬砖繁忙回复不及时见谅,技术交流请加QQ群:909211071

概述:

SDS(simple Dynamic String)

Redis的字符串有2种存储方式,通过 redisObject 对象里的 encoding 字段决定,emstr对应常量为OBJ_ENCODING_EMBSTR,raw对应常量为OBJ_ENCODING_RAW:

  • 在长度特别短时使用emstr形式存储,而长度超过44字节时,使用raw形式存储。为什么是44字节呢:44+NULL(结尾)+SDS(19) = 64。embstr将RedisObject对象头结构和SDS对象连续存储在一起,使用malloc方法一次分配。
  • 在长度超过44字节时,用raw从初,需要使用两次malloc方法,RedisObject和SDS在内存地址上一般不连续。

下面是创建一个string类型时的方法源码:

/* Create a string object with EMBSTR encoding if it is smaller than
 * OBJ_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
 * used.
 *
 * The current limit of 44 is chosen so that the biggest string object
 * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}




/* Create a string object with encoding OBJ_ENCODING_RAW, that is a plain
 * string object where o->ptr points to a proper sds string. */
robj *createRawStringObject(const char *ptr, size_t len) {
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}




/* Create a string object with encoding OBJ_ENCODING_EMBSTR, that is
 * an object where the sds string is actually an unmodifiable string
 * allocated in the same chunk as the object itself. */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}


/* ===================== Creation and parsing of objects ==================== */
robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;

    /* Set the LRU to the current lruclock (minutes resolution), or
     * alternatively the LFU counter. */
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;
}

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[];
};
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[];
};

每个结构体成员的用途:

  • len:记录字符串的长度
  • alloc:排除掉头和字符串空终止符\0后,分配的大小
  • flags:标志位,前3位用来保存sds的类型,后5位未用到(8、16、32、64)
  • buf:字节数组,用于保存字符串,末尾遵从C语言字符串惯例,保存终止符\0,目的是可以使用部分C语言字符串函数

我们可以看到Redis内部定义了针对不同长度的字符串定义了不同的sdshdr结构体,目的是将len和alloc的两个字段占用内存进行优化,都是用的无符号整型。

struct __attribute__ ((__packed__)) 语法是用于取消字节对齐,目的也是为了优化内存占用。

flags位

通过标志位flags字段和7进行&操作,来判断当前字符串的sds类型(8、16、32、64),比如下面的获取字符串长度的方法:

#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)

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    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和C语言字符串的区别

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

  • 在C语言中,要获取某个字符串的长度,需要遍历整个字符串,对遇到的每个字符串进行计数,直到遇到\0为止,这个操作的复杂度为O(n)。
  • 在SDS中,额外用一个字段len保存了字符串的长度,获取长度的复杂度为O(1),典型的空间换时间。

2、防止缓冲区溢出

  • C语言中,假如我们用strcat拼接字符串之前,现有的字符串分配的空间无法容纳拼接的字符串大小,数据会溢出到后面紧邻的空间中。举个例子:字符串s1为redis,字符串s2为golang,在执行stcat(s1, 'abc')之前,忘了为s1重新分配内存,将会导致字符串s2变为acblang。
  • 而在SDS中通过空间分配策略避免了缓冲区溢出,当需要对SDS修改时,会首先检查空间是否满足需要的长度,如果不满足会自动扩容,然后再修改。

3、空间预分配

  • 在C语言中,假如我们要拼接字符串,则需要先计算出需要的空间,分配内存后再进行修改,而内存分配操作涉及到系统调用,从用户态切换到内核态,通过复杂的内存分配算法分配,再由内核态切换到用户态,整个过程非常耗时。
  • 而在SDS中,如果进行修改时,会先计算分配完后SDS的长度,如果SDS的长度小于1M,比如13字节,则会分配13byte+13byte+1byte(\0)个字节的空间;如果SDS的长度大于1M,比如3M,则会分配3M+1M+1byte(\0)的空间。

4、二进制安全

  • C语言中默认以\0标志字符串结尾,所以如果存储二进制数据会自动截断,造成数据不完整
  • SDS中用len计算字符串长度,所以二进制安全
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AirGo.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值