Redis数据结构之SDS

Redis 没有直接使用 C 语言传统的字符串表示,而是自己构建了一种名为简单动态字符串( simple dynamic string ,SDS) 的抽象 类型,并将 SDS 用作 Redis 的默认字符串表示。

SDS结构

总共有五类SDS结构,根据设置的字符串串长度,选择对应的结构体。

结构体中,len表示字符串的长度,alloc 表示字符串最大的长度,flags 低三位保存sds的类型,buf 占位符。

/* 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__)) hisdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr8 {
    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__)) hisdshdr16 {
    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__)) hisdshdr32 {
    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__)) hisdshdr64 {
    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与传统C字符串区别

1.获取字符串长度

C字符串: C 字符串并不记录自身的长度信息,所 以为了获取一个 C 字符串的长度,程序必须遍历整个字符串,直到遇到代表字符串结尾的空字符为止,这个操作的 复杂度为O(n).

SDS:SDS在 len 属性中记录了本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。

2.缓冲区溢出

C字符串: C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。例如strcat将一个字符串拼接到另一个字符串末尾,但是没有给目标字符串分配足够的空间,从而导致冲区溢出。

SDS:SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求,如果不满足的话. API 会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。

3.SDS空间预分配和惰性空间释放

        空间预分配用于优化 SDS 的字符串增长操作:当 SDS 的 API 对一个 SDS进行修改, 并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用空间。

        如果对 SDS 进行修改之后,SOS 的长度(也即是 len 属性的值)将小于1M, 那么程序分配和 len 属性同样大小的未使用空间。比如,如果进行修改之后, SDS 的 len 将变成13字 节,那么程序也会分配13字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13+13+1=27 字节(额外的一字节用于保存空字符)。

        如果对 SDS 进行修改之后, SDS 的长度将大于等于 1M,那么程序会分配 1MB 的未使用空间。举个例子,如果进行修改之后, SOS 的 len 将变成 30M,那么程序会分 配 1M的未使用空间, SDS 的 buf 数组的实际长度将为 30 M+ 1M+1byte

        惰性空间释放用于优化 SDS 的字符串缩短操作,当 SOS 的 API 需要缩短 SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是等待将来使用。

4.二进制安全

C字符串: 除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读人的空字符将被误认为是字符串结尾,这些限制使得 C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二 进制数据。

SDS:SDS 的 API 都是二进制安全的,所有 SOSAPI 都会以处理二进制的方式来处理 SDS存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。

5.兼容C字符串函数

        虽然 SDS 的 API 都是二进制安全的,但它们一样遵循 C 字符串以空字符结尾的惯例: 这些 API 总会将 SDS 保存的数据的末尾设置为空字符,并且总会在为 buf 数组分配空间时 多分配一个字节来容纳这个空字符, 这样SDS可以重用一部分C中的函数。

源码部分

扩容代码

/* Enlarge the free space at the end of the hisds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the hisds string as returned
 * by hi_sdslen(), but only the free buffer space we have. */
hisds hi_sdsMakeRoomFor(hisds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = hi_sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & HI_SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = hi_sdslen(s);
    sh = (char*)s-hi_sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < HI_SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += HI_SDS_MAX_PREALLOC;

    type = hi_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 hi_sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == HI_SDS_TYPE_5) type = HI_SDS_TYPE_8;

    hdrlen = hi_sdsHdrSize(type);
    if (oldtype==type) {
        newsh = hi_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 = hi_s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        hi_s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        hi_sdssetlen(s, len);
    }
    hi_sdssetalloc(s, newlen);
    return s;
}

缩减容量

static inline void hi_sdssetlen(hisds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&HI_SDS_TYPE_MASK) {
        case HI_SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = (unsigned char)(HI_SDS_TYPE_5 | (newlen << HI_SDS_TYPE_BITS));
            }
            break;
        case HI_SDS_TYPE_8:
            HI_SDS_HDR(8,s)->len = (uint8_t)newlen;
            break;
        case HI_SDS_TYPE_16:
            HI_SDS_HDR(16,s)->len = (uint16_t)newlen;
            break;
        case HI_SDS_TYPE_32:
            HI_SDS_HDR(32,s)->len = (uint32_t)newlen;
            break;
        case HI_SDS_TYPE_64:
            HI_SDS_HDR(64,s)->len = (uint64_t)newlen;
            break;
    }
}
void hi_sdsclear(hisds s) {
    hi_sdssetlen(s, 0);
    s[0] = '\0';
}

SDS结构

 

hisds s
s 指向内容起始地址
s[-1] 可以获取到type地址
s-sizeof(hisdshdr) 可以获取到sds内存起始地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值