【每日一博】Redis 中的字符串实现 sds

简介:

在C中子字符串的实现都是用 char *来实现的,用起来很不方便,而且容易出现内存泄露,并且效率不高,在Redis内部,字符串采用了 sds 的方式进行了封装,似的字符串在Redis中可以方便、高效的使用,Redis字符串的实现如要依赖一下两个数据类型和结构(以下代码可以在 src/sds.h中找到):

typedef char *sds;
sds 存放了字符串的具体值

struct sdshdr {
    int len;   //字符串对象已经使用的内存数量
    int free;  //字符串对象剩余的内存数量
    char buf[]; //字符串对象的具体值(其实就是sds字符串)
};

sdshdr 实现了字符串对象

这样设计的好处有很多,比如使得Redis在获取字符串长度的时候可以达到o(1)的复杂度,在进行追加等字符串操作的时候,可以减少内存分配(提高性能),sdshdr的结构使得根据sds字符串获取对应的sds对象的时候可以非常方便的获取。

创建字符串 init 为需要初始化的字符串值。initlen表示为初始化字符串的长度,该函数创建一个sds字符串对象并返回sds字符串(以下代码可以在 src/sds.c中找到):

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh; //创建一个空的字符串对象

    //如果init为空的时候需要对分配的内存进行初始化
    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }   
    if (sh == NULL) return NULL;
    //设置字符串对象的已占用长度
    sh->len = initlen;
    sh->free = 0;
    //如果init不为空将其复制到字符串对象中
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    //在结尾加入终止符(c语言字符串以\0为结尾)
    sh->buf[initlen] = '\0';
    //返回字符串对象中的sds值
    return (char*)sh->buf;
}

例如你创建了一个sds字符串 为 hello 那么你的代码应该如下:

sds str = sdsnewlen("hello", 5);

这时候,Reids会创建一个sdshdr对象,长度为:

sizeof(struct sdshdr) + 5 + 1

Redis在释放字符串也会分方便,因为是对整个结构进行的分配所以只需要对sds字符串的对象进行释放就可以将字符串值和字符串对象的内存都释放掉,如下:

void sdsfree(sds s) {
    if (s == NULL) return;
    zfree(s-sizeof(struct sdshdr));
}

释放上例中的sds字符串只需要简单的调用:

sdsfree(str);
  1. 根据sds字符串获取对应的字符串对象

如1中你知道了如何创建一个字符串对象并返回它的sds字符串,根据sdshdr的存储结构,你可以方便的通过返回的sds字符串得到字符串对象,如下代码:(下面代码中s表示sds字符串,定义为 sds s;)

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

由1中创建的hello的字符串对象在内存中的分配大概如下图:

image

上例中的 s 为 sdshdr结构中的buf元素,上例代码中的 s - sizeof(struct sdshdr) 会将指向buff 的指针,移动到len上,这样通过一个简单的运算就可以获取到sds字符串的对象,并对其进行字符串操作(不知道为啥redis宁可每次手动写,也没有对此进行一个宏定义的封装)。

  1. 计算字符串长度

计算长度的方式就非常简单了只需要根据sds字符串获取到sds对象,然后获取其len属性即可,具有o(1)的效率,而不需要去遍历字符列表,如下获取方法(以下代码可以在 src/sds.h中找到):

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}
  1. 追加字符串

Redis的追加字符串由于其设计方式可以非常高效,进行追加,直接看代码(以下代码可以在 src/sds.c中找到):

sds sdscatlen(sds s, const void *t, size_t len) {
    struct sdshdr *sh;      //定义一个字符串对象
    size_t curlen = sdslen(s);   //获取当前sds字符串的长度 可以参考第3条

    s = sdsMakeRoomFor(s,len);   //对sds字符串扩展,申请len长度的内存(会根据free决定是否申请,见下文)
    if (s == NULL) return NULL;
    sh = (void*) (s-(sizeof(struct sdshdr)));   //根据新申请空间后的sds字符串获取对应的对象
    memcpy(s+curlen, t, len);    //将新的字符串追加到结尾
    sh->len = curlen+len;     //更新已占用空间
    sh->free = sh->free-len;  //更新剩余空间
    s[curlen+len] = '\0';     //设置字符串结尾
    return s;     //返回修改后的sds字符串
}

对sds字符串内存进行扩展的函数如下:(以下代码可以在 src/sds.c中找到):

sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;   //初始化两个字符串对象
    size_t free = sdsavail(s);   //字符串剩余内存,定义在src/sds.h中,获取方法与 sdslen()相同
    size_t len, newlen;

    if (free >= addlen) return s;
    len = sdslen(s);   //获取当前字符串长度
    sh = (void*) (s-(sizeof(struct sdshdr)));   //获取当前的字符串对象
    newlen = (len+addlen);    //计算扩展后的字符串长度
    //一下为重点:申请字符串会计算,新的长度是否会超过SDS_MAX_PREALLOC(定义在src/sds.h中,默认为1M)
    //如果超过则申请SDS_MAX_PREALLOC大小的内存,否则申请2*扩展后字符串的长度
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);  //重新分配内存
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;  //更新剩余空间
    return newsh->buf;
}

如上代码所述,系统在扩展内存的时候,会申请新字符串长度的两倍,这样后续在进行追加操作的时候就不进行内存分配处理了,节省了很多内存分配的消耗,当然这样可能会对内存造成一些浪费,Redis的一些配置可以改变这种行为,可以通过字符串函数 sdsRemoveFreeSpace() 对多申请的那部分内存进行释放。

相关文章
|
11月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
352 32
|
存储 缓存 NoSQL
redis数据结构-字符串
redis数据结构-字符串
171 1
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
204 4
|
NoSQL 安全 Java
Redis6入门到实战------ 三、常用五大数据类型(字符串 String)
这篇文章深入探讨了Redis中的String数据类型,包括键操作的命令、String类型的命令使用,以及String在Redis中的内部数据结构实现。
Redis6入门到实战------ 三、常用五大数据类型(字符串 String)
|
存储 缓存 NoSQL
3)深度解密 Redis 的字符串
3)深度解密 Redis 的字符串
183 1
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
C# 开发者 UED
WPF开发者必备秘籍:深度解析文件对话框使用技巧,打开与保存文件原来如此简单!
【8月更文挑战第31天】在WPF应用开发中,文件操作是常见需求。本文详细介绍了如何利用`Microsoft.Win32`命名空间下的`OpenFileDialog`和`SaveFileDialog`类来正确实现文件打开与保存功能。通过示例代码展示了如何设置文件过滤器、初始目录等属性,并使用对话框进行文件读写操作。正确使用文件对话框能显著提升用户体验,使应用更友好易用。
694 0
|
存储 NoSQL Redis
【Redis 探秘】SDS 简单动态字符串:揭秘 Redis 高效字符串处理的秘密武器!
【8月更文挑战第24天】Redis采用的简单动态字符串(SDS)是一种专为优化内存存储和字符串操作而设计的数据结构。相较于C语言的标准字符串,SDS改进了字符串长度计算、内存重分配及字符串比较等问题。其特性包括预分配冗余空间减少未来的内存重分配、显式存储长度以加快获取速度等。这些改进使Redis能更高效地管理字符串数据。例如,在Redis中,SDS被广泛应用于键值对的存储,显著提升了字符串操作的性能。了解SDS不仅对于深入理解Redis的工作原理至关重要,也是开发者技能树中的重要一环。
263 0
|
存储 JSON NoSQL
揭秘Redis字符串String的隐藏技能!从基础到进阶,让你的数据存储操作秒变高大上!
【8月更文挑战第24天】Redis中的字符串类型作为其基石,不仅能够存储从简单文本到复杂格式如JSON的各种数据,还能通过多样化的命令实现包括但不限于自增自减、设置过期时间等高级功能,极大提升了其实用性和灵活性。例如,使用`SET`命令可添加或更新键值对,`GET`获取值,`DEL`删除键;同时,`INCR`和`DECR`支持对整数值的原子性增减操作,非常适合实现计数器等功能;通过`EXPIRE`命令设置过期时间,则适用于需要限时存储的应用场景。尽管名为“字符串”,但实际上还可存储图片、音频文件的Base64编码等形式的数据,为开发者提供了强大而灵活的工具。
201 0
|
NoSQL Java Redis
Redis字符串数据类型之INCR命令,通常用于统计网站访问量,文章访问量,实现分布式锁
这篇文章详细解释了Redis的INCR命令,它用于将键的值增加1,通常用于统计网站访问量、文章访问量,以及实现分布式锁,同时提供了Java代码示例和分布式锁的实现思路。
832 0