redis源码分析与思考(一)——sds

本文深入探讨了Redis中的SDS(Simple Dynamic String),一种改进版的C字符串。SDS通过额外的元数据存储,解决了传统C字符串的一些缺陷,如无法快速获取长度、易发生缓冲区溢出等问题。文章详细介绍了SDS的内部结构、主要操作函数及其优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  在阅读黄健宏的书《Redis设计与实现》的时候,深刻的意识到仅仅看别人的作品是远远不够,自己更应该去阅读源码,形成自己的思考,这样才算真正的学进去了。

  现如今,Nosql的概念大行其道,redis作为其中的佼佼者被广大的开发者爱好着,而且Redis的源码仅仅只有三万多行,作为一名喜爱开源技术以及新技术的人来说,Redis源码无疑是值得每个开发者阅读的。

  在Redis中,即使源码是由c写成的,但是却没有使用c字符串,而是封装了一个sds字符串,在redis源码中对应着sds.c与sds.h两个文件,其sds字符串结构体定义如下:

struct sdshdr {
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 数据空间
    char buf[];
};

  len与free来记录所存数据(c字符串)的长度也即是占据buf空间的大小与buf空间剩余的多少。

  现列出sds的函数调用:

//创建一个新的指定initlen大小buf的sdshdr字符串
sds sdsnewlen(const void *init, size_t initlen);
//创建一个不指定buf空间大小的sdshdr字符串,调用sdsnewlen
sds sdsnew(const char *init);
//创建了保存“”的sdshdr
sds sdsempty(void);
//返回buf空间已用长度
size_t sdslen(const sds s);
//复制并返回一个sdshdr
sds sdsdup(const sds s);
//销毁一个sdshdr
void sdsfree(sds s);
//返回buf空间空余的长度
size_t sdsavail(const sds s);
//增长sds至指定长度,并初始化为0
sds sdsgrowzero(sds s, size_t len);
//将额外字符串与原buf拼接起来
sds sdscatlen(sds s, const void *t, size_t len);
//调用sdscatlen
sds sdscat(sds s, const char *t);
//与sdscat功能一致,传递参数不一样
sds sdscatsds(sds s, const sds t);
//复制指定长度的字符串
sds sdscpylen(sds s, const char *t, size_t len);
//调用sdscpylen
sds sdscpy(sds s, const char *t);
/**
*输出函数
**/
sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif

sds sdscatfmt(sds s, char const *fmt, ...);
//消除指定的字符
sds sdstrim(sds s, const char *cset);
//截取指定开始结尾的字符串
void sdsrange(sds s, int start, int end);
//没有被使用
void sdsupdatelen(sds s);
//清空sdshdr
void sdsclear(sds s);
//比较长度大小
int sdscmp(const sds s1, const sds s2);
//使用分隔符进行分割,
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
//
void sdsfreesplitres(sds *tokens, int count);
//转化为小写
void sdstolower(sds s);
//转化为大写
void sdstoupper(sds s);
//用long long类型来创造一个sds
sds sdsfromlonglong(long long value);
//将长度为 len 的字符串 p 以带引号(quoted)的格式追加到给定 sds 的末尾
sds sdscatrepr(sds s, const char *p, size_t len);
//
sds *sdssplitargs(const char *line, int *argc);
//替换指定的字符
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
//
sds sdsjoin(char **argv, int argc, char *sep);

/* 暴露给用户的低等级的api */
//增加sdshdr的buf的长度,成倍增加
sds sdsMakeRoomFor(sds s, size_t addlen);
//根据incr来增加sds空间
void sdsIncrLen(sds s, int incr);
//移除sdshdr多余的空间
sds sdsRemoveFreeSpace(sds s);
//返回给定 sdshdr 分配的内存字节数
size_t sdsAllocSize(sds s);

  挑选其中一些重要的点来谈谈自己的理解,首先,在sds.h中有两个静态的内联函数,也就是sdslen与sdsavail,先看其源代码(ps:下文若无说明sds即为sdshdr):

typedef char *sds;

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}

  这两个函数本身的含义十分的简单,一个是获取sds字符串的长度,另一个则是返回sds中未使用的空间,但是其中巧妙的利用连续地址实现了sds与普通字符串的转换,也就是这句代码:

struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));

  根据c的内存分配可知,结构体内的数据空间是连续的,利用字符串s的首地址减去一个结构体的占用内存的大小,也就是把s的首地址往前移了sizeof(struct sdshdr)的大小,并且对其实行强制转换,使其成为一个无类型的指针,再将sds的指针指向s的首地址,完成转换。

  其实sizeof(struct sdshdr)的大小就是sizeof(int len)+sizeof(int free),即八个字节,因为buf数据尚未赋值与初始化,所以它所占内存为零,如下代码所示:

#include <iostream>
struct test{
    int b;
    char c[];
};
int main() {
    std::cout << "Hello, World!" << std::endl;
    //arrayList arrayList1<int>(10);
     struct test c;
    std::cout<< sizeof(c);
    return 0;
}

  运行结果为:

  可见只包含了一个int的大小,所以s首地址只向前移动了八个字节,s字符串往前移动的八个字节加上其原来本身的字符串的数据,恰好的构成了一个sds结构体内存的分配规则,从而可以完成转换。

  如图所示:

  再来看看sds是如何创建一个新的sds的:

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
   //如果init不为空
    if (init) {
   //申请不初始化的malloc空间
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
   //申请初始化为零的calloc空间
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
   //申请内存失败,返回空
    if (sh == NULL) return NULL;
  //将长度赋值给len
    sh->len = initlen;
    sh->free = 0;
    //如果initlen与init同时不为假,进行copy
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
  //将字符串末尾设为'\0'
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}

    而之所以申请内存要加1,是因为字符串结尾处需要'\0'来结尾,'\0'恰好是一个比特,而zmalloc与zcalloc是redis自己封装了malloc与calloc两个函数,代码如下:

#define PREFIX_SIZE (sizeof(size_t))

void *zmalloc(size_t size) {
    //申请内存
    void *ptr = malloc(size+PREFIX_SIZE);
  //异常处理
    if (!ptr) zmalloc_oom_handler(size);
  //内存统计
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
}

  申请内存的时候加上PREFIX_SIZE,是由于redis划分出来一部分内存用来存储该个结构体的总体占用内存大小,具体在这不谈,后面会用一篇来谈谈redis对内存的管理。

  那么,sds内存不够的时候怎么扩充自己的内存的呢?利用好了free这个属性,对空间进行预分配,当需要增加的内存大小小于free时,便不扩充,使用原有的内存,当其大于时,便扩充。

#define SDS_MAX_PREALLOC (1024*1024)//默认最大分配值

sds sdsMakeRoomFor(sds s, size_t addlen) {

    struct sdshdr *sh, *newsh;
    // 获取 s 目前的空余空间长度
    size_t free = sdsavail(s);
    size_t len, newlen;

    // s 目前的空余空间已经足够,无须扩展
    if (free >= addlen) return s;

    // 获取 s 目前已占用空间的长度
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));

    // s 最少需要的长度
    newlen = (len+addlen);

    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
   
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);

    // 内存不足,分配失败,返回
    if (newsh == NULL) return NULL;

    // 更新 sds 的空余长度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

  而sds也提供了回收内存的函数

sds sdsRemoveFreeSpace(sds s) {
    struct sdshdr *sh;
    sh = (void*) (s-(sizeof(struct sdshdr)));
    sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
    sh->free = 0;
    return sh->buf;
}

  该函数是用于当机器内存不够时,对sds进行内存回收,但不影响字符串的存储,只是将空余的空间给回收了,也即是重新分配了内存空间。

  sds相比较普通的字符串有如下的优点:

  1、杜绝了缓冲区的溢出,实现了动态的存储,惰性释放内存空间

  2、获取其长度的时间复杂度为O(1),动态的分配其内存空间

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值