redis源码学习01:字符串sds

爷,独闯天下 提交于 2020-04-20 19:06:37

前言

本文是redis源码关于字符串处理的学习笔记,欢迎指正。 redis版本是5.0.5,redis的功能、用途及性能我就不做赘述了。

正文

进入正题,redis提供了自己的字符串存储及相关操作,源码文件在sds.h和sds.c里。 在学习代码的过程中发现redis使用了一个比较巧妙的设计,redis里存储字符串不是简单的使用C语言里的char *来存储,而是利用C语言指针可以加减运算的特性来封装字符串结构体。从而能够在常用的字符串处理函数里自动扩容;而且这个设计保证你在使用redis的字符串存函数同时也能使用全部的libc里的所有关于字符串的函数。 下面就来说说这个设计,首先看下一个宏定义:

typedef char *sds;

redis里给char *取了个别名sds,所以常用的跟字符串操作相关的函数也都是以sds开头如:

void sdssetlen(sds s, size_t newlen);
​size_t sdslen(const sds s);

接下来看下存储字符串的结构体:

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

可以看到定义了5个不同的结构体,我们知道redis数据都是缓存在内存中的,所以分别定义不同的结构体来存放不同长度的字符串,尽可能的减少内存的占用。 sdshdr5结构体基本不用,就先不介绍了,其它4个结构里都包含4个字段:

  • len:字符串长度,不同结构体支持最大长度不一样
  • alloc:分配的内存大小
  • flags:低3位存储类型,即结构类型
  • buf:用于存储字符串的内存起始地址,buf都是动态分配的

每个结构体通过__attribute__来告诉编译器不要进行字节对齐。不采用字节对齐一可以节省内存,另一个用途是可以通过指针的减法操作获取flags,能获取到flags后一切都好办了,flags里低3位是存放结构体类型的,在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
#define SDS_TYPE_MASK 7     // 获取类型的掩码
#define SDS_TYPE_BITS 3     // 类型位数

当需要存储字符串时,先根据字符串长度选择一种结构体,然后申请一块空间,大小包括结构体大小及需要存储的字符串的大小,部分代码如下:

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);	// 申请的空间包含\0结束符
    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为存储字符串的起始位置
    /*
     * 其它代码
     * ......
     *
     * /

    return s;
}

redis会多申请一个字节用于存储\0结束符,而且自动加上这个结束符,这样libc里所有的字符串处理函数都能适用。函数里申请的空间包括结构体部分,但是返回值却是真正存放字符串的地址。那么redis是怎么将s和结构体关联起来的呢,先看sds.h里的两个宏:

// 根据字符串起始位置定义一个sdshdr**的结构体指针
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
// 将字符串起始位置的指针转化为sdshdr**的结构体指针
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

因为申请的时候是连续的地址,所以这两个宏直接将地址减去结构体的长度就能获取到结构体的起始地址。 使用这两个宏需要知道结构体的类型,但是只有一个字符串的起始地址,那怎么知道是哪一种结构体存储的呢。 这时候结构体不采用内存对齐的方式就派上用场了,以获取字符串长度的函数为例:

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

因为结构体没有内存对齐,而且flags字段是char类型,所以直接s[-1]就能获取到flag,只要取到flag其它就都好办了。其它比如获取容量等操作都是通过这种方式处理的,这里就不再一一介绍了。

收尾

本文主要是介绍存储字符串结构的技巧,其它代码都相对简单。sds.h和sds.c里常用的储如字符串拼接、格式化等操作都是自动扩容的,扩容大小是指数增长的。有时候也有可能进行缩容,不管是扩容还是缩容都会导致原来的指针失效。所以使用的时候要小心指针失效的情况,大部分函数都得这么用:s = sdstrim(s,"Aa. :"); 就先到这吧,欢迎指正~

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!