Redis初识-简单动态字符串| 8月更文挑战

173 阅读7分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

1. 数据结构

1.1 二进制安全

  • 非二进制安全:C 语言中,用 \0 表示字符串的结束,如果字符串本身就有 \0 字符,字符串会被截断
  • 二进制安全:通过实现某种机制,保证了读写字符串不损害其内容

1.2 sdshdr5 短字符串

// sds.h
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags;
    char buf[];
};
  • flags:无符号字符型,大小1字节(8 bit),低3位表示字符串类型(字符长度为1字节、2字节、4字节、8字节、小于1字节),高5位表示字符串长度(0~31)
  • buf:柔性数组,存放实际的字符内容

sdshdr5结构

使用柔性数组保存字符的原因:

  • 柔性数组的地址和结构体是连续的,查找内存更快
  • 可以通过柔性数组的首地址偏移得到结构体的首地址,进而方便获取其余属性字段

1.3 其他字符串

sdshdr8、sdshdr16、sdshdr32、sdshdr64 类型的字符串结构类似

// sds.h
#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

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已使用长度,1字节 */
    uint8_t alloc; /* 总长度,1字节 */
    unsigned char flags; /* 低3位表示类型,高5位闲置 */
    char buf[]; /* 柔型数组,存放实际的字符内容 */
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* 已使用长度,2字节 */
    uint16_t alloc; /* 总长度,2字节 */
    unsigned char flags; /* 低3位表示类型,高5位闲置 */
    char buf[]; /* 柔型数组,存放实际的字符内容 */
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* 已使用长度,4字节 */
    uint32_t alloc; /* 总长度,4字节 */
    unsigned char flags; /* 低3位表示类型,高5位闲置 */
    char buf[]; /* 柔型数组,存放实际的字符内容 */
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 已使用长度,8字节 */
    uint64_t alloc; /* 总长度,8字节 */
    unsigned char flags; /* 低3位表示类型,高5位闲置 */
    char buf[]; /* 柔型数组,存放实际的字符内容 */
};
  • len:表示 buf 中字符的已使用长度
  • alloc:表示 buf 中能够保存字符的总长度
  • flags:表示当前结构体的类型,低3位表示类型,高5位闲置未使用
  • buf:柔性数组,存放实际的字符内容

sdshdr8 字符串结构图:

sdshdr8结构图

1.4 attribute ((packed))

一般情况下,结构体会根据所有变量的最小公倍数做字节对齐,当用 packed 修饰后,结构体变为按1字节对齐

以 sdshdr32 类型的字符串为例,修饰前按4字节对齐大小为12字节,修饰后按1字节对齐为9字节

packed修饰前后示意

packed 修饰的好处:

  • 节省内存
  • SDS 返回给上层的,不是结构体的首地址,而是指向字符串内容 buf 的指针。SDS 创建后,通过 (char*)sh+hdrlen 得到 buf 指针地址(sh 为指向结构体首地址的指针,hdrlen 是结构体的长度)。packed 修饰后方便通过 buf[-1] 找到 flags

2. 源码浅析

2.1 创建字符串

  • // sds.c
    sds sdsnewlen(const void *init, size_t initlen) {
        void *sh;
        sds s;
        // 根据字符串初始长度获取类型
        char type = sdsReqType(initlen);
        // SDS_TYPE_5 需要转换为 SDS_TYPE_8
        if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
        // 获取当前类型的头部长度(结构体大小)
        int hdrlen = sdsHdrSize(type);
        unsigned char *fp;
    
        // 为 sds 分配内存
        // +1是为了结束符 ‘\0’
        // sh 为存放 sds 内存空间的首地址
        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 指向 buf
        s = (char*)sh+hdrlen;
        // fp 指向 flags
        fp = ((unsigned char*)s)-1;
        // 根据类型为 sds 结构体属性字段
        switch(type) {
            case SDS_TYPE_5: {
                // flags 赋值,低3位表示类型,高3位表示字符长度
                *fp = type | (initlen << SDS_TYPE_BITS);
                break;
            }
            case SDS_TYPE_8: {
                // 获取结构体的指针
                SDS_HDR_VAR(8,s);
                sh->len = initlen;
                sh->alloc = initlen;
                *fp = type;
                break;
            }
            // ......
        }
        if (initlen && init)
            // 填充字符内容到 buf
            memcpy(s, init, initlen);
        // 添加结束符
        s[initlen] = '\0';
        // 返回指向 buf 的指针
        return s;
    }
    

创建空字符串时,SDS_TYPE_5 类型会转化为 SDS_TYPE_8 类型。创建空字符串后,可能存在字符内容频繁变更而引发扩容,所以直接创建为 SDS_TYPE_8 类型的字符串

2.2 释放字符串

直接释放内存

  • // sds.c
    void sdsfree(sds s) {
        if (s == NULL) return;
        // 根据 sds 首地址释放
        s_free((char*)s-sdsHdrSize(s[-1]));
    }
    

不直接释放内存,重置字符已使用长度为0,新的数据直接覆盖写入,无需重新申请内存

  • // sds.c
    void sdsclear(sds s) {
        // 重置已使用长度为0
        sdssetlen(s, 0);
        // 设置结束符
        s[0] = '\0';
    }
    

2.3 拼接字符串

根据 sds 当前字符内容已使用长度,有需要时进行扩容,然后直接拼接字符串,重置 sds 已使用长度,添加结束符

  • // sds.c
    sds sdscatsds(sds s, const sds t) {
        return sdscatlen(s, t, sdslen(t));
    }
    
  • // sds.c
    sds sdscatlen(sds s, const void *t, size_t len) {
        // 获取字符串当前长度
        size_t curlen = sdslen(s);
    
        // 有需要时对 sds 进行扩容
        s = sdsMakeRoomFor(s,len);
        if (s == NULL) return NULL;
        // 直接拼接字符串
        memcpy(s+curlen, t, len);
        // 重置已使用长度
        sdssetlen(s, curlen+len);
        // 添加结束符 
        s[curlen+len] = '\0';
        return s;
    }
    
  • // sds.c
    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;
    
        // 如果未使用字符长度充足,无需扩容直接返回
        if (avail >= addlen) return s;
    
        len = sdslen(s);
        // 获取 sds 首地址
        sh = (char*)s-sdsHdrSize(oldtype);
        // 计算字符串新长度
        newlen = (len+addlen);
        if (newlen < SDS_MAX_PREALLOC)
            // 新字符串所需内存空间小于1M时,按新长度的2倍扩容
            newlen *= 2;
        else
            // 新字符串所需内存空间大于等于1M时,按新字符串内存空间加上1M进行扩容
            newlen += SDS_MAX_PREALLOC;
    
        // 获取新字符串类型
        type = sdsReqType(newlen);
    
        // SDS_TYPE_5 需要转换为 SDS_TYPE_8
        if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    
        hdrlen = sdsHdrSize(type);
        if (oldtype==type) {
            // 类型没有变化时,扩大 buf 即可
            newsh = s_realloc(sh, hdrlen+newlen+1);
            if (newsh == NULL) return NULL;
            // 获取指向 buf 的新指针
            s = (char*)newsh+hdrlen;
        } else {
            // 如果类型发生变化,则重新分配内存
            newsh = s_malloc(hdrlen+newlen+1);
            if (newsh == NULL) return NULL;
            // 原字符内容复制到新内存空间
            memcpy((char*)newsh+hdrlen, s, len+1);
            // 释放旧指针
            s_free(sh);
            // 获取指向 buf 的新指针
            s = (char*)newsh+hdrlen;
            // 更新 flags 标识的类型
            s[-1] = type;
            // 更新 len
            sdssetlen(s, len);
        }
        // 更新 alloc
        sdssetalloc(s, newlen);
        return s;
    }
    

3. 其他 API

  • // sds.h
    // 根据给定字符串创建 SDS
    sds sdsnewlen(const void *init, size_t initlen);
    // 创建空字符串,长度为0,内容为“”
    sds sdsempty(void);
    // 复制 SDS
    sds sdsdup(const sds s);
    // SDS 扩容到指定长度,并用'0'填充新增内容
    sds sdsgrowzero(sds s, size_t len);
    // 将指定长度的字符串复制到 SDS
    sds sdscpylen(sds s, const char *t, size_t len);
    // SDS 两端清除指定字符
    sds sdstrim(sds s, const char *cset);
    // 刷新 SDS 属性值
    void sdsupdatelen(sds s);
    // 根据指定分隔符对 SDS 进行切分
    sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);
    /* Low level functions exposed to the user API */
    sds sdsMakeRoomFor(sds s, size_t addlen);
    // SDS 缩容
    sds sdsRemoveFreeSpace(sds s);
    // 返回 SDS 占用内存大小
    size_t sdsAllocSize(sds s);
    

4. 总结

  • SDS 如何兼容 C 语言字符串?

    SDS 对象中 buf 是一个柔性数组,上层调用时,SDS 直接返回 buf 。由于 buf 是直接指向字符内容的指针,所以兼容 C 语言函数

  • 如何保证二进制安全?

    读取字符串内容时,SDS 会通过 len 来限制读取长度,而非 \0

  • sdshdr5 的特殊之处?

    sdshdr5 只能存储长度为0~31的字符串,一般情况下短字符串存储更加普遍,所以 Redis 进一步压缩了 sdshdr5 的数据结构,将类型和长度放入同一个属性字段中,用 flags 的低3位保存类型,高5位保存长度。创建空字符串时,sdshdr5 会被 sdshdr8 取代

  • SDS 如何扩容?

    调用 sdsMakeRoomFor 函数,检查后根据具体情况进行扩容

    • 新字符串内存空间小于1M时,字符串内存空间按新内存两倍扩容
    • 新字符串内存空间大于等于1M时,字符串内存空间按新内存加上1M进行扩容

学自《Redis 5设计与源码分析》