简单动态字符串 《Redis 5 设计与源码分析》

1 简单动态字符串(Simple Dynamic Strings, SDS) 是Redis的基本数据结构之一,用于存储字符串和整型数据。

2 sds的结构体,大体有这4个变量:

  • len:表示buf中已占用字节数
  • alloc:表示buf中已分配字节数,即buf分配的总长度
  • flags:标识当前结构体的类型,flags用来标示是属于5种中哪个字符串类型结构体
  • buf:柔性数组,真正存储字符串的数据空间

如上所示,redis提供了5种字符串类型结构体(1字节,2字节,4字节,8字节和小于1字节)。变量

我猜这就是为什么称为简单动态字符串。它会根据内容采用对应的字符串类型来存储。比如用如下结构来存储长度小于32的短字符串

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};

注:3 lsb of type, and 5 msb of string length 表示 低3位存储类型,高5位存储长度

3 源码中的__attribute__ ((__packed__))的作用:一般情况下,结构体会按其所有变量大小的最小公倍数做字节对齐。而用packed修饰后,结构体则变成按1字节对齐

4 sds返回给上层的,不是结构体首地址,而是指向内容的buf指针。因为此时按1字节对齐,故sds创建成功后,无论是sdshdr8,sdshdr16还是sdshdr32,都能通过(char*)sh + hdrlen得到buf指针地址(其中hdrlen是结构体长度,通过sizeof计算得到)。

所以修饰后,无论sdshdr8,sdshdr16还是sdshdr32,都能通过buf[-1]找到flags

5 创建sds:Redis 通过sdsnewlen函数创建sds。sdsnewlen是hi_sdsnewlen函数的别名。在函数中会根据字符串长度选择合适的类型。

hisds hi_sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    hisds s;
    char type = hi_sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == HI_SDS_TYPE_5 && initlen == 0) type = HI_SDS_TYPE_8;
    int hdrlen = hi_sdsHdrSize(type);
    unsigned char *fp; /* 指向flags的指针 */

    sh = hi_s_malloc(hdrlen+initlen+1); // +1是为了结束符'\0'
    if (sh == NULL) return NULL;
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;   // s是指向buf的指针
    fp = ((unsigned char*)s)-1; // 
    switch(type) {
        case HI_SDS_TYPE_5: {
            *fp = type | (initlen << HI_SDS_TYPE_BITS);
            break;
        }
        case HI_SDS_TYPE_8: {
            HI_SDS_HDR_VAR(8,s);
    
    ...

    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

6 sdshdr5类型基本被舍弃了,因为这种可能会频繁更新而引起扩容,所以直接就创建为sdshdr8

7 释放sds:sdsfree函数,直接释放内存的函数,通过对s的偏移,可定位到sds结构体的首部,然后调用s_free释放内存

/* Free an hisds string. No operation is performed if 's' is NULL. */
void hi_sdsfree(hisds s) {
    if (s == NULL) return;
    hi_s_free((char*)s-hi_sdsHdrSize(s[-1]));
}

为了优化性能,sds提供了不直接释放内存,而是通过重置统计值来达到清空目的的方法 - sdsclear

void hi_sdsclear(hisds s) {
    hi_sdssetlen(s, 0);
    s[0] = '\0';
}

8 拼接字符串: sdscatsds函数,里头真正干活的是hi_sdscatlen函数

hisds hi_sdscatsds(hisds s, const hisds t) {
    return hi_sdscatlen(s, t, hi_sdslen(t));
}

hisds hi_sdscatlen(hisds s, const void *t, size_t len) {
    size_t curlen = hi_sdslen(s);

    s = hi_sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    hi_sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

9 扩容函数hi_sdsMakeRoomFor,有如下策略,可以从代码里看出

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;


    // 1)若sds中剩余空闲长度avail大于新增内容的长度addlen,直接返回,无需扩容
    if (avail >= addlen) return s;

    len = hi_sdslen(s);
    sh = (char*)s-hi_sdsHdrSize(oldtype);
    newlen = (len+addlen);

    // 2)若新增后总长度len+addlen < 1MB,按新长度的2倍扩容;
    //    如果len+addlen > 1MB,新长度再加上1MB扩容
    if (newlen < HI_SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += HI_SDS_MAX_PREALLOC;

    // 3)根据新长度重新选取存储类型,并分配空间
    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;
}

10 其他API

11 sds有两点注意

  • sds暴露给上层的是指向柔性数组buf的指针
  • 读操作的复杂度多为O(1),直接读取成员变量;写操作则可能会触发扩容
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值