redis源码阅读-一--sds简单动态字符串

环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation

参考书籍:《redis的设计与实现》
文章推荐:
redis源码阅读-一--sds简单动态字符串
redis源码阅读--二-链表
redis源码阅读--三-redis散列表的实现
redis源码浅析--四-redis跳跃表的实现
redis源码浅析--五-整数集合的实现
redis源码浅析--六-压缩列表
redis源码浅析--七-redisObject对象(下)(内存回收、共享)
redis源码浅析--八-数据库的实现
redis源码浅析--九-RDB持久化
redis源码浅析--十-AOF(append only file)持久化
redis源码浅析--十一.事件(上)文件事件
redis源码浅析--十一.事件(下)时间事件
redis源码浅析--十二.单机数据库的实现-客户端
redis源码浅析--十三.单机数据库的实现-服务端 - 时间事件
redis源码浅析--十三.单机数据库的实现-服务端 - redis服务器的初始化
redis源码浅析--十四.多机数据库的实现(一)--新老版本复制功能的区别与实现原理
redis源码浅析--十四.多机数据库的实现(二)--复制的实现SLAVEOF、PSYNY
redis源码浅析--十五.哨兵sentinel的设计与实现
redis源码浅析--十六.cluster集群的设计与实现
redis源码浅析--十七.发布与订阅的实现
redis源码浅析--十八.事务的实现
redis源码浅析--十九.排序的实现
redis源码浅析--二十.BIT MAP的实现
redis源码浅析--二十一.慢查询日志的实现
redis源码浅析--二十二.监视器的实现

目录

 

一.SDS是什么

二.SDS的定义

三.SDS相对于C字符串区别


一.SDS是什么

redis没有直接使用c语言中的传统字符串;SDS(simple dynamic string)是redis自己构建抽象类型,是对c传统字符串的一层封装;实现的源码主要位于src目录下sds.h和sds.c 中。接下来将简单介绍下redis为什么使用sds而不是c传统字符串;

 

二.SDS的定义

sds.h/sdshdr5 

// __attribute__ ((__packed__)) 告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。
/* 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 */ //低三位存储字符串类型,高5位存储字符串长度 
    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 */ //其中的最低3个bit用来表示header的类型,剩余5位未使用
    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属性是已经分配的总长度;
  • flage属性存的这个结构的类型,目前值可以通过sds.h中的宏定义得知如下;因为目前只有5中类型,源码注释了“只用了3位,5位是无用的”
    #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

     

  • buf属性是一个c字符串,用来存储真实的字符串;

 

需要注意的是sdshdr5结构体有所不同;sdshdr5只有两个属性:flage属性的低三位用户存储字符类型,高三位用户存储字符串的查长度;

 

三.SDS相对于C字符串区别

c字符串与sds之间的区别
c字符串sds
获取字符串长度的复杂度O(N)获取字符串长度的复杂度O(1)
API是不安全的,可能会造成缓冲区溢出不会造成缓冲区溢出
修改字符串N次必然需要执行N次内存重分配修改字符串N次最多需要执行N次内存重分配
只能保存文本数据可以保存文本或二进制数据
可以使用所有<string.h>中的库函数只可以使用一部分<string.h>中的库函数

接下来通过阅读sds的源码来看一下sds与c传统字符的区别;

1.常数级获得字符串长度

因为c字符串并不记录自身长度,所以获取字符串长度时要遍历整个字符串,直到字符串结束。所以时间复杂度是O(n);

而SDS中len属性记录了字符串长度,所以获取字符串长度时,直接返回属性len;时间复杂度是O(1);

我们可以看sds.h文件中的static inline size_t sdslen(const sds s)函数,就是根据SDS的类型,直接返回len属性:
 

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

2.避免缓冲区溢出

因为c字符串不记录自身长度,假设执行strcat执行字符串拼接时,超过目标字符串的分配内存大小,就会造成缓冲区溢出;

SDS的alloc属性记录的已经分配的内存的大小。当要对SDS进行修改时,会先判断已经分配的内存大小是否满足修改要求。
可以阅读sdscatlen函数来看sds是如何方式溢出的(其中sdscatlen调用的sdsMakeRoomFor是防止溢出的关键函数):
 


//追加一个字符串到🈯️sds
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
 * end of the specified sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s); //获取当前sds的长度

    s = sdsMakeRoomFor(s,len); //扩容sds的长度
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len); //copy 愿字符串到目标sds中
    sdssetlen(s, curlen+len); //设置新的长度
    s[curlen+len] = '\0'; //设置结束字符
    return s; 
}


/* 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); //获取sds剩余可用的空间大小
    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; //现有的空间能满足拼接的长度。 返回原有的sds

    //原有长度不够 扩容
    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); //根据新的字符串长度,判断需要初始化sds的仂

    /* 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); //获取新的sds类型
    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); //释放原有sds 空间
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len); //设置新的sds长度
    }
    sdssetalloc(s, newlen); //设置sds的allco(分配长度)的大小
    return s;
}

3.减少修改字符串时带来的内存分配次数
 (1)空间预分配
SDS空间与分配,当一个API对SDS进行修改需要扩容的时候,SDS不是仅分配扩容所必须需要的空间,还会为SDS分配额外未使用的空间,我们可阅读额上面贴出的sdsMakeRoomFor 函数,可得到SDS的扩容规则:

//SDS_MAX_PREALLOC 是宏定义 1024*1024 为1M 
//如果扩容后长度小于SDS_MAX_PREALLOC,将分配一倍的空余空 
if (newlen < SDS_MAX_PREALLOC) 间
        newlen *= 2;
else  //否则 分配 1M的额外空间
        newlen += SDS_MAX_PREALLOC;

(2)惰性空间释放
当SDS需要缩短字符串时,程序并不会立即使用内存重分配来回收多余的空间。而是只更新来len属性。
SDS也提供的相应的API,在需要时释放未使用的空间。
 


/* Reallocate the sds string so that it has no free space at the end. The
 * contained string remains not altered, but next concatenation operations
 * will require a reallocation.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    size_t len = sdslen(s);
    sh = (char*)s-oldhdrlen;

    /* Check what would be the minimum SDS header that is just good enough to
     * fit this string. */
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);

    /* If the type is the same, or at least a large enough type is still
     * required, we just realloc(), letting the allocator to do the copy
     * only if really needed. Otherwise if the change is huge, we manually
     * reallocate the string to use the different header type. */
    if (oldtype==type || type > SDS_TYPE_8) {
        newsh = s_realloc(sh, oldhdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
    } else {
        newsh = s_malloc(hdrlen+len+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, len);
    return s;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值