Redis源码学习(1)──字符串

redis 版本:5.0

本文代码在Redis源码中的位置:redis/src/sds.c、redis/src/sds.h

源码整体结构

src:核心实现代码,用 C 语言编写

tests:单元测试代码,用 Tcl 实现

deps:所有依赖库

字符串存储结构

Redis 将字符串的实现称为 sds(simple dynamic string)。为了提高存储空间的利用率,Redis 对不同长度的字符串,采用不同的数据结构。

以下是长度小于32的字符串的存储结构:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags;
    char buf[];
};

其中,flags:高5位指示字符串真实长度,低3位指示数据类型;buf:柔性数组,指向字符串存储地址。

:柔性数组是在 C99 及以上标准才支持,其目的是为了在结构体中能够“动态”地设定数组的长度。但需要注意的是,对结构体进行 sizeof 时,柔性数组的大小不被计算在内。因此在对结构体分配大小时,需要注意加上柔性数组的大小。柔性数组必须被声明在结构体的最后,其起始地址与上一字段的末尾地址相连。

参考:https://en.wikipedia.org/wiki/Flexible_array_member

其他长度的字符串的存储结构与其类似,结构分别如下:

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

其中,len 表示字符串实际长度;alloc 表示申请的字符数组的长度。

可以看出,Redis 最长能存储长度大约为 \(1.84* 10^{19}\)\(2^{64}-1\)) 的字符串。

:在声明结构体时,struct __attribute__ ((__packed__)) 语法用来告诉编译器采用紧凑的方式存储数据,即1字节对齐,而不是默认的所有变量大小的最小公倍数做字节对齐。

sdshdr32 为例,若采用1字节对齐,lenallocflags 一共占9个字节(4+4+1),而默认会使用4字节对齐,这样 flags 也会占用4个字节,一共占用12个字节。

采用 packed 的好处是明显的,一来可以减少数据的大小,提高空间利用率;二来这为地址计算带来了方便:无论哪种类型(除 sdshdr5 外),使用 buff[-1] 即能获取到 flag

字符串基本操作

创建

sdsnewlen 方法负责创建字符串,代码如下:

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp;

    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;
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            *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;
        }
        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] = '\0';
    return s;
}

以上代码比较简单,但有几点需要说明:

  • 从第5行代码可以看出,对于空字符串,Redis 将其做为 sdshdr8 类型而不是 sdshdr5 类型。其给出的原因是:用户很有可能会在空字符串后追加新的字符串,因此用一个存储长度适中的结构。
  • sh 指针指向对应结构体的起始地址,其长度为结构体大小 + 待存储字符串的大小 + 1。最后加1是为了在末尾追加一个 \0 符。
  • s 指针即为以上提到的柔性数组起始地址。fp 指针地址正好在 s 指针指向地址的前一字节,指向 flags
  • 第19行代码中,initlen << SDS_TYPE_BITSinitlen 左移3位,正好印证之前提到的 flags 使用高5位存储字符串长度。之后或上 type ,即把低3位置为 type 的值(因为之前左移3位后,低3位值均为0,这样或运算的值就是 type)。
  • SDS_HDR_VAR 宏定义为 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 。其中的 ## 表示字符连接的意思。以23行代码为例,该行代码在经编译器预处理后,变为 struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8))),语义为sh = s - sizeof(sdshdr8)暂时还没弄明白这里为啥要重算一下 sh。按理说第15行代码 s = (char*)sh+hdrlen; 中,s 正是由 sh +sizeof(sdshdr8)得到。
  • 注意,最终返回给外部的是 s 指针而不是结构体指针。

其中的sdsReqType 方法用于根据待存储字符串长度选择合适的存储类型,代码如下:

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

LONG_MAX 表示 long 类型整数最大值,与编译器有关。通常64位编译器值为 \(2^{63} - 1\),32位编译器为 \(2^{32}-1\)LLONG_MAX 表示 long long 类型整数最大值,通常32位和64位编译器值均为 \(2^{63} - 1\)

参考:

:static inline 关键字是在建议编译器,以类似于宏定义的方式对待该函数,即在编译阶段,直接将该函数的相关指令插入到调用该函数的地方。这样做可减少函数调用的开销。

参考:https://zhuanlan.zhihu.com/p/132726037

sdsHdrSize 函数用于计算对应存储类型的大小,代码如下:

static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

其中,SDS_TYPE_MASK 的值为7,二进制即为:0000 0111type 和它做与运算,正好得出自己的后3位的值,即类型值。

删除

删除字符串的方法有两种,一种是真正释放了内存,代码如下:

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

其中 s[-1] 之前也提到过,就是 flags 的值。而 s-sdsHdrSize(s[-1]) 则得到了 sdshdr 结构体的起始地址。

另一种则只是将长度置为0,并没有真正释放内存,这么做的目的当然是为了下次存储字符串时,无需重新申请内存,直接再用即可。

void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

其中,sdssetlen 函数用来设置字符串长度,代码如下:

static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}

追加(cat)

追加操作用于在一个字符串后面添加另一字符串,其代码如下:

sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

连接后,s 将指向连接后的字符串。

sdscatlen 函数逻辑也很简单:扩容(若需要) -> 复制追加内容 -> 修改长度。代码如下:

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] = '\0';
    return s;
}

其中 sdsMakeRoomFor 为扩容函数,根据剩余可用空间大小和待追加的字符串长度决定是否扩容。它保证了其返回的指针指向的内存区域,一定能容纳追加的字符串(若内部的内存申请成功的话)。代码如下:

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);
    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;

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

其基本逻辑为:

  • 若剩余容量足够容纳待追加的字符串,则无需扩容(第9行)。
  • 否则,算出追加后的字符串的长度 newlen(第13行)。第13行~第17行代码还对 newlen 做了调整。暂时不知道为啥要这么做
  • 之后根据 newlen 决定使用什么类型进行存储。若类型还和之前一样,则继续沿用之前的内存结构,但需要使用 s_realloc 扩大之前的内存空间(第28行);否则则需要重新申请一块新的内存(第34行),将之前的数据复制到新内存中(第36行)。

连接(join)

连接操作是将一个字符串数组的所有元素串联在一起,若给定了分隔符,各元素之间还需插入分隔符。例如,若字符串数组 s = ["a", "b", "c"] ,分隔符 sep = ",",则 join(s, sep) 的结果为:a,b,c。其代码如下:

sds sdsjoin(char **argv, int argc, char *sep) {
    sds join = sdsempty();
    int j;

    for (j = 0; j < argc; j++) {
        join = sdscat(join, argv[j]);
        if (j != argc-1) join = sdscat(join,sep);
    }
    return join;
}

代码很简单,就不多做说明了。其中,sdsempty() 是构造一个空串,即:sdsnewlen("",0)

分割参数

sdssplitargs 函数用于将一条命令按参数分割成一个字符串数组,代码如下:

sds *sdssplitargs(const char *line, int *argc) {
    const char *p = line;
    char *current = NULL;
    char **vector = NULL;

    *argc = 0;
    while(1) {
        /* skip blanks */
        while(*p && isspace(*p)) p++;
        if (*p) {
            /* get a token */
            int inq=0;  /* set to 1 if we are in "quotes" */
            int insq=0; /* set to 1 if we are in 'single quotes' */
            int done=0;

            if (current == NULL) current = sdsempty();
            while(!done) {
                if (inq) {
                    if (*p == '\\' && *(p+1) == 'x' &&
                                             is_hex_digit(*(p+2)) &&
                                             is_hex_digit(*(p+3)))
                    {
                        unsigned char byte;

                        byte = (hex_digit_to_int(*(p+2))*16)+
                                hex_digit_to_int(*(p+3));
                        current = sdscatlen(current,(char*)&byte,1);
                        p += 3;
                    } else if (*p == '\\' && *(p+1)) {
                        char c;

                        p++;
                        switch(*p) {
                        case 'n': c = '\n'; break;
                        case 'r': c = '\r'; break;
                        case 't': c = '\t'; break;
                        case 'b': c = '\b'; break;
                        case 'a': c = '\a'; break;
                        default: c = *p; break;
                        }
                        current = sdscatlen(current,&c,1);
                    } else if (*p == '"') {
                        /* closing quote must be followed by a space or
                         * nothing at all. */
                        if (*(p+1) && !isspace(*(p+1))) goto err;
                        done=1;
                    } else if (!*p) {
                        /* unterminated quotes */
                        goto err;
                    } else {
                        current = sdscatlen(current,p,1);
                    }
                } else if (insq) {
                    if (*p == '\\' && *(p+1) == '\'') {
                        p++;
                        current = sdscatlen(current,"'",1);
                    } else if (*p == '\'') {
                        /* closing quote must be followed by a space or
                         * nothing at all. */
                        if (*(p+1) && !isspace(*(p+1))) goto err;
                        done=1;
                    } else if (!*p) {
                        /* unterminated quotes */
                        goto err;
                    } else {
                        current = sdscatlen(current,p,1);
                    }
                } else {
                    switch(*p) {
                    case ' ':
                    case '\n':
                    case '\r':
                    case '\t':
                    case '\0':
                        done=1;
                        break;
                    case '"':
                        inq=1;
                        break;
                    case '\'':
                        insq=1;
                        break;
                    default:
                        current = sdscatlen(current,p,1);
                        break;
                    }
                }
                if (*p) p++;
            }
            /* add the token to the vector */
            vector = s_realloc(vector,((*argc)+1)*sizeof(char*));
            vector[*argc] = current;
            (*argc)++;
            current = NULL;
        } else {
            /* Even on empty input string return something not NULL. */
            if (vector == NULL) vector = s_malloc(sizeof(void*));
            return vector;
        }
    }

err:
    while((*argc)--)
        sdsfree(vector[*argc]);
    s_free(vector);
    if (current) sdsfree(current);
    *argc = 0;
    return NULL;
}

以上代码总体上是在对字符串 line 逐个字符进行遍历,指针 p 做为游标指向当前访问的字符。

主体上,代码由两层 while 循环构成。外层 while 循环(第7行)条件总是成立,因此该函数要么出错结束(第102行~末尾),返回 NULL,要么从第98行成功结束,此时的条件是 p 指针正好指向字符串末尾的 \0,返回分割出的参数数组 vector。而内层 while 循环执行结束后(第91行),会识别出一个参数,并会将该参数添加到 vector 数组中(第92行)。

再来看内层 while 循环是如何识别一个参数的。这段代码(第17行到89行)总体结构是 if...else if ... else ...。进入前两分支的条件是处于双引号模式或单引号模式下,否则进入 else 分支。每轮循环结束,游标 p 向后挪动一位(第88行)。

双、单引号模式由 inqinsq 变量指示,前者表示当前访问的字符在单引号内(即之前已访问了起始单引号,但还未访问到闭合单引号),后者则表示是在双引号内。进入这两个模式的时机当然是遇到了双引号(第77行)或单引号(第80行)。

在非双、单引号模式下,若遇到一般字符,都会将该字符追加到 current 字符串中(current 在遍历开始是空串);若遇到 空格'\n''\r''\t''\0',内层循环都将结束,并将 current 做为识别到的一个参数。

在双、单引号模式下,若是遇到 "\n""\r""\t""\b""\a"(注意,这些都是两个字符而不是一个),会将其做为单字符 '\n''\r''\t''\b''\a' 加入 current 中(第41行);若字符串遍历完成都没遇到闭合的双、单引号,则会报错(第49行和第64行);闭合的双、单引号后面不是空格(若字符串后面还有其他字符),也会报错(第44行和第60行)。

另外,在双引号模式下,会将格式类似于 "\x41"的子串,做为16进制数,转成字符类型后追加到 current 中(第19行~第28行)。该例中,"\x41" 将做为字母 A

以上便是 Redis 字符串操作最主要的函数,还有一些比如字符串大小写转换,比较等函数实现均非常简单,这里不再赘述,有兴趣可以去看看源码。

参考

posted @ 2021-01-24 23:55  学数学的程序猿  阅读(198)  评论(0编辑  收藏  举报