redis源码学习simple dynamic strings(简单动态字符串 sds)

Posted 看,未来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis源码学习simple dynamic strings(简单动态字符串 sds)相关的知识,希望对你有一定的参考价值。

阅读源码之前,先接几个问题,我觉得还蛮有意思的。

Q1:如何实现一个扩容方便且二进制安全(不会被\\0打断)的字符串呢?
Q2:SDS如何兼容C语言函数呢?
Q3:SDS为了节约内存都秀了什么操作呢?
Q4:SDS是如何扩容的?
Q5:SDS是如何减少拷贝次数的?


题外话:这种模式我还挺喜欢的,也写过一些源码分析类的博客,但是感觉看完之后就没了,收效甚微。看nginx的时候,除了惊叹于其鬼斧神工的架构设计,以及比较火的那几点问题之后,也没学到多少编程技法(我主要编程技法都是在STL源码里学的,当然萃取是没看明白),不过如果采用这种启发式学习的方式可能会好一些,也能让人更愿意看源码吧。


sds 结构分析

typedef char *sds;

/* 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 
    //该字段记录的字符串的长度,可以在常数时间获取
    //由于有长度记录变量len,在读写字符串的时候不依赖于‘\\0’,从而保证了二进制安全性。
    uint8_t len; /* used */	//可存储的最大字符串长度为 2^8=256
    uint8_t alloc; /* excluding the header and null terminator */
    //低三位表示字符串的类型,高五位只在sd5中使用,不过看着架势sd5是被打入冷宫了呀。
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    //柔性数组,保存一个空字符作为buf的结尾,不计入len、alloc,以此兼容 C 语言的 strcmp、strcpy 等函数。
    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[];
;

A String value can be at max 512 Megabytes in length,如果拿那个64来算,会只有这点吗?
但是一个字符串过大也没用。

使用柔性数组除了省内存,还有一个好处,柔型数组的内存和结构体是连续的,可以很方便的通过柔型数组的首地址偏移得到结构体的首地址。

接下来看一下是如何的节约内存的:

这是sdshdr5的,里面的 unsigned8 对应一个字节。后面的自行脑补。


基本操作

创建字符串

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;	//SDS_TYPE_5 强制转换为 SDS_TYPE_8
    int hdrlen = sdsHdrSize(type);	//计算不同头部的所需的那个 unsigned 长度
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);	//留了一个给‘\\0’
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;		//直接指向buf
    fp = ((unsigned char*)s)-1;	//buf首地址偏移
    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;

至于为什么要把sd5打入冷宫?可能是因为太短了吧,承受变长风险能力不够、


释放字符串

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) 
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));	//这里是直接释放内存了

不过这些优秀项目怎么能没有内存池呢,明着暗着都会有的。

/* Modify an sds string in-place to make it empty (zero length).
 * However all the existing buffer is not discarded but set as free space
 * so that next append operations will not require allocations up to the
 * number of bytes previously available. */
void sdsclear(sds s) 
    sdssetlen(s, 0);
    s[0] = '\\0';

该有的都会有的。


sdsMakeRoomFor 扩容

扩容流程图:

/* Enlarge the free space at the end of the sds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the sds string as returned
 * by sdslen(), but only the free buffer space we have. */
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;

	//此役过后,指向buf的指针被更新了
    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;

注意,分支上半部分是 realloc,下半部分是 malloc,注意区分二者区别。


小tip:__attribute__ ((__packed__))

在结构体声明当中,加上attribute ((packed))关键字,它可以做到让我们的结构体,按照紧凑排列的方式,占用内存。
简单的说,就是取消内存对齐。

这个 tip 哪里来的呢?翻到开头再看看。这个编程技法需要特别关注,稍不留神就错过了。

一般情况下,结构体会做内存对齐,以sd32为例,对齐前按4字节对齐,大小为12字节。取消对齐后,大小为9字节(buf不要面子的)。
而且,对齐后,可以直接通过 buf 的首地址向前偏移一位找到 flags ,如果不这样,各位可以自己思考一下要如何找到 flags,那就几乎成了一个 “鸡/蛋” 的死结了(不知道类型,怎么着偏移量?不知道偏移量,怎么找类型?)。


那各位自己解答开头的问题吧,溜了溜了。

以上是关于redis源码学习simple dynamic strings(简单动态字符串 sds)的主要内容,如果未能解决你的问题,请参考以下文章

Simple Dynamic Strings(SDS)源码解析和使用说明一

Redis源码解析01: 简单动态字符串SDS

《游戏学习》java代码实现《愤怒的小鸟》实战源码

《游戏学习》java代码实现《愤怒的小鸟》实战源码

SDS(Simple Dynamic String)

REDIS源码中一些值得学习的技术细节02