• Redis源码阅读一:简单动态字符串SDS


    源码阅读基于Redis5.0.9

    C字符串缺点

    redis 127.0.0.1:6379> SET dbname redis
    OK
    redis 127.0.0.1:6379> GET dbname
    "redis"
    

    从上面的例子可以看到,key为dbname的值是一个字符串“redis”
    Redis源码是用c写成,但并没有使用c的字符串。c的字符串有以下缺点:

    1. 没有储存字符串长度的变量,获取长度只能靠遍历字符串
    2. 扩容麻烦。没有相应保护,容易造成缓冲区溢出
    3. 更新字符串需要重新分配内存
    addr value
    0x0 s
    0x1 t
    0x2 r
    0x3 1
    0x4 ''
    0x5
    0x6
    0x7
    0x8 a
    0x9 b
    0xa ''

    解释下2,3点。上图是一段连续的内存,保存了字符串"str1"和“ab”。
    如果我们用strcat函数,拼接一个“append”在“str1”后面,就会对“ab”产生影响。造成内存的破坏。
    同样的道理,想要更新字符串,同时又不造成溢出,只能重新分配一段内存。
    普通的应用程序,上面的操作是可以接受的。但是redis作为数据库,经常增删改查,加上对速度有一定需求,所以没有使用C字符串。
    这里补充一个二进制安全的概念:C语言中''表示字符串结束。如果字符串本身含有'',那么读取的时候就会造成字符串截断,那么是非二进制安全。如果通过某些机制能保证读取字符串时不损害其中内容,则是二进制安全。

    SDS结构体

    SDS是Simple Dynamic String的缩写。我们先从redis3.0的实现看起。

    3.0版本的SDS

    /*
     * 指向 sdshdr 的 buf 成员
     */
    typedef char *sds;
    
    /*
     * 保存字符串对象的结构
     */
    struct sdshdr {
        
        // buf 中已占用空间的长度
        int len;
    
        // buf 中剩余可用空间的长度
        int free;
    
        // 数据空间
        char buf[];
    };
    

    buf是柔性数组,属于C99标准,具体可参考https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html
    sds指向buf,通过偏移可以很容易得到sdshdr的地址,即s-(sizeof(struct sdshdr)),进而可以得到len和free的值
    3.2版本前的SDS是这样设计的。不仅通过len成员使字符串读取不依赖于''终止,解决了二进制安全问题,而且sds指向的对象buf可以利用C的字符串函数处理。

    5.0版本的SDS

    到了4.0版本,SDS进行了改进。
    我们可以看到,3.0版本的SDS,不管buf实际存了几个字节,在64位机器上len和free各占用了4字节。实际可能并不需要占用8字节去记录buf信息,我们可以利用位存储这些信息,实现压缩。
    例如,buf长度为14(0b1110),那么我们用4个bit就能表示长度。一个char足够

    改进的SDS根据buf的可能最大长度,分成了下面几种类型

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

    sdshdr5的flags成员低三位用来表示类型,值可以看宏定义。高5位用来表示buf的长度。所以sdshdr5最大可以表示2^5-1=31位字符(最后一个字符必定是'')。
    对于超过31字符的buf,就参考了之前3.0SDS的设计,len定义一样,free改成了alloc表示最大容量。

    除了sdshdr5,剩下的定义只有长度的区别,成员是一样的。

    • buf[] 实际存储字符的数组
    • len 字符串长度
    • alloc 最大容量。等于sizeof(buf)-1,因为字符串最后一位固定是''。比如sdsnewlen("abc",3),len和alloc都是3,而buf的大小是4
    • flags 低3位是类型,高5位保留

    关于该结构体,还需要注意2点:

    1. __attribute__ ((packed))是为了让编译器以紧凑的方式分配内存,否则编译器可能会对结构体的成员进行对齐。对这里不太明白的可以看看struct大小的计算
    2. 结构体的最后定义了char buf[]; 这个字段只能作为结构体的最后一个成员。上文提到过,C语言中被称为柔性数组,只是作为一个标记,不占用内存空间。
      如果明白了以上2点,应该能算出sizeof(sdshdr32)=4+4+1=9Byte

    相关操作函数

    创建

    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        char type = sdsReqType(initlen);
        /* Empty strings are usually created in order to append. Use type 8
         * since type 5 is not good at this. */
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
        int hdrlen = sdsHdrSize(type);
        unsigned char *fp; /* flags pointer. */
    
        sh = s_malloc(hdrlen+initlen+1);
        if (init==SDS_NOINIT)
            init = NULL;
        else if (!init)
            memset(sh, 0, hdrlen+initlen+1);
        if (sh == NULL) return NULL;
        s = (char*)sh+hdrlen;  //s指向buf
        fp = ((unsigned char*)s)-1;  // buf[-1],即flag
        switch(type) {
            case SDS_TYPE_5: {
                *fp = type | (initlen << SDS_TYPE_BITS);  // 左移3bit置位
                break;
            }
            case SDS_TYPE_8: {
                SDS_HDR_VAR(8,s);  // struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8)));
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_16: {
                SDS_HDR_VAR(16,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_32: {
                SDS_HDR_VAR(32,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            case SDS_TYPE_64: {
                SDS_HDR_VAR(64,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
        }
        if (initlen && init)
            memcpy(s, init, initlen);
        s[initlen] = '';
        return s;
    }
    

    注意几点:

    1. SDS_TYPE_5类型的空串会被转成SDS_TYPE_8
    2. 实际分配内存时,大小是hdrlen+initlen+1,那个1是因为要以''结尾
    3. 返回的指针指向的是buf,而不是sdshdr

    释放

    void sdsfree(sds s) {
        if (s == NULL) return;
        s_free((char*)s-sdsHdrSize(s[-1]));
    }
    
    // 软删除,长度置0.方便追加不必再分配空间
    void sdsclear(sds s) {
        sdssetlen(s, 0);
        s[0] = '';
    }
    

    拼接

    sds sdscatlen(sds s, const void *t, size_t len) {
        size_t curlen = sdslen(s);
    
        s = sdsMakeRoomFor(s,len);
        if (s == NULL) return NULL;
        memcpy(s+curlen, t, len);
        sdssetlen(s, curlen+len);
        s[curlen+len] = '';
        return s;
    }
    
    sds sdscatsds(sds s, const sds t) {
        return sdscatlen(s, t, sdslen(t));
    }
    

    sdsMakeRoomFor确保s的buf能够追加进len长度的字符。如果buf剩余空间不够的话会进行扩容。
    扩容规则:原来字符长度加上追加字符长度如果小于1M,那么alloc翻倍;否则alloc+1M
    如果扩容后的type变了,那么需要一个新的sdshdr;否则更改下len和alloc即可

    sds sdsMakeRoomFor(sds s, size_t addlen) {
        void *sh, *newsh;
        size_t avail = sdsavail(s);  // alloc - len,即3.0sds的free
        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);  // 原来字符长度加上追加字符长度
        if (newlen < SDS_MAX_PREALLOC)  // 小于1M
            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;
    
        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;
    }
    
  • 相关阅读:
    Struts2拦截器
    Struts2 数据封装与值栈
    Struts2的环境搭配
    自学spring AOP
    小白学Maven第二篇配置Ecilpse
    小白学Maven第一篇配置
    web项目jsp出现The superclass javax.servlet.http.HttpServlet was not found on the Java Build Path错误
    软件测试复习(二)
    软件测试复习(一)
    白盒测试概述
  • 原文地址:https://www.cnblogs.com/pusidun/p/9088746.html
Copyright © 2020-2023  润新知