Redis 底层数据结构 -SDS学习打卡

98 阅读3分钟

2.基本数据结构

2.1 简单动态字符串(SDS)

2.1.1数据结构

redis为了节省内存,针对不同长度的数据采用不同的数据结构。如下共五种,但SDS_TYPE_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为例:

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

img

img

2.1.2 空间扩容

  • 当前有效长度>=新增长度,直接返回

  • 更新之后,判断新旧类型是否一致:

    • 一致使用remalloc,否则使用malloc+free

      • a.当前有效长度>=新增长度,直接返回
  • 增长步长:

    • 新增后长度小于预分配长度(1024*1024),扩大一倍;
  • 新增后长度大于等于预分配的长度,每次加预分配长度(减少不必要的内存)

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    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),扩大一倍;SDS_MAX_PREALLOC=1024*1024
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    //新增后长度大于等于预分配的长度,每次加预分配长度
    else
        newlen += SDS_MAX_PREALLOC;
​
    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;
   // 新老类型一致使用remalloc,否则使用malloc+freea.当前有效长度>=新增长度,直接返回
    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] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

2.1.3 空间缩容

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

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;
}
  • 真正的删除被放在后续操作中见tryObjectEncoding
if (o->encoding == OBJ_ENCODING_RAW &&
        sdsavail(s) > len/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }

2.1.4优点

  • 常量获取字符串长度(len)
  • 避免缓冲区溢出
  • 减少字符串修改带来的内存频繁重分配次数
  • 二进制操作安全:可以保持文本数据,也可以保持任意格式的二进制数据(如视频流数据)
  • 以’\0’结尾,使其兼容部分C字符串函数

2.1.5其他

  • sds是char*的别名,可以理解为分配的是一块连续内存(表头+数据),根据局部性原理可以提高访问速度。
  • 数据存储不使用SDS_TYPE_5,因为使用该类型每次新数据时,都需要进行扩充
  • 利用C语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的,因此在sds一列操作中,看到使用s[-1]这样的操作搞到惊讶,当然这里的s指向的是buf位置。
/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {
    //主要是看这句,s指向的是数据部分
    unsigned char flags = s[-1];
    return 0;
}

2.2链表