redis中字符串的数据结构 SDS

redis中字符串的数据结构 SDS

前言

字符串

C语言中的字符串和redis中的字符串对比:

C中char*的劣势:

操作效率低:获取长度需遍历,O(N)复杂度

空间分配上:手动检查和分配字符串空间,增加代码开发的工作量。

限制性:图片等数据还无法用字符串保存。

二进制不安全:无法存储包含 \0 的数据。

简单动态字符串(Simple Dynamic String,SDS)

对比redis中设计的好处:

提升字符串的操作效率,并且可以用来保存二进制数据。

SDS 的优势:

  • 操作效率高:获取长度无需遍历,O(1)复杂度
  • 二进制安全:因单独记录长度字段,所以可存储包含 \0 的数据
  • 兼容 C 字符串函数,可直接使用字符串 API

为什么C的会要手动检查和分配字符串空间???

举例:字符串追加函数 strcat


  char *strcat(char *dest, const char *src) {
     //将目标字符串复制给tmp变量
     char *tmp = dest;
     //用一个while循环遍历目标字符串,直到遇到“\0”跳出循环,指向目标字符串的末尾
     while(*dest)
        dest++;
     //将源字符串中的每个字符逐一赋值到目标字符串中,直到遇到结束字符
     while((*dest++ = *src++) != '\0' )
     return tmp;
  }

首先,需要先遍历一次;

其次,确保目标字符串有足够的可用的空间。否则,还得手动的动态分配空间。

所以,这样不符合Redis 对字符串高效操作。

那为什么会有限制呢????

举例:strlen字符串长度测量

strlen是通过’\0’检测长度的。


  #include <stdio.h>
  #include <string.h>
  int main()
  {
     char *a = "red\0is";\\3
     char *b = "redis\0";\\5
     printf("%lu\n", strlen(a));
     printf("%lu\n", strlen(b));
     return 0;
  }

不符合 Redis 希望能保存任意二进制数据的需求.

SDS数据结构

包含:一个字符数组 buf[];

字符数组现有长度 len;

分配给字符数组的空间长度 alloc;

以及 SDS 类型 flags这四部分内容。

redis中sds的定义:

typedef char *sds;

从上可以看出,SDS 本质还是字符数组,只是在字符数组基础上增加了额外的元数据。

同时,在创建新的字符串时,Redis 会调用 SDS 创建函数 sdsnewlen。sdsnewlen 函数会新建 sds 类型变量(也就是 char* 类型变量),并新建 SDS 结构体,把 SDS 结构体中的数组 buf[] 赋给 sds 类型变量。最后,sdsnewlen 函数会把要创建的字符串拷贝给 sds 变量。下面的代码就显示了 sdsnewlen 函数的这个操作逻辑,你可以看下。


sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;  //指向SDS结构体的指针
    sds s;     //sds类型变量,即char*字符数组

    ...
    sh = s_malloc(hdrlen+initlen+1);   //新建SDS结构,并分配内存空间
    ...
    s = (char*)sh+hdrlen;              //sds类型变量指向SDS结构体中的buf数组,sh指向SDS结构体起始位置,hdrlen是SDS结构体中元数据的长度
    ...
    if (initlen && init)
        memcpy(s, init, initlen);    //将要传入的字符串拷贝给sds变量s
    s[initlen] = '\0';               //变量s末尾增加\0,表示字符串结束
    return s;

看redis中 的 追加字符串操作:

sds.c文件中sdscatlen函数操作


sds sdscatlen(sds s, const void *t, size_t len) {
    //获取目标字符串s的当前长度
    size_t curlen = sdslen(s);
    //根据要追加的长度len和目标字符串s的现有长度,判断是否要增加新的空间
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    //将源字符串t中len长度的数据拷贝到目标字符串结尾
    memcpy(s+curlen, t, len);
    //设置目标字符串的最新长度:拷贝前长度curlen加上拷贝长度
    sdssetlen(s, curlen+len);
    //拷贝后,在目标字符串结尾加上\0
    s[curlen+len] = '\0';
    return s;
}

通过分析这个函数的源码,我们可以看到 sdscatlen 的实现较为简单,

  1. 其执行过程分为三步:首先,获取目标字符串的当前长度,并调用 sdsMakeRoomFor 函数,根据当前长度和要追加的长度,判断是否要给目标字符串新增空间。这一步主要是保证,目标字符串有足够的空间接收追加的字符串。

  2. 其次,在保证了目标字符串的空间足够后,将源字符串中指定长度 len 的数据追加到目标字符串。

  3. 最后,设置目标字符串的最新长度。

在这里插入图片描述

对比C语言优点:

SDS 通过记录字符数组的使用长度和分配空间大小,避免了对字符串的遍历操作,降低了操作开销,进一步就可以帮助诸多字符串操作更加高效地完成,比如创建、追加、复制、比较等。

空间检查和扩容封装在了 sdsMakeRoomFor 函数中。

封装思想:

直接让函数内部检查是否需要分配内存,避免开发人员忘记检查和分配内存而造成 的 内存溢出。

另外 Redis 在操作 SDS 时,为了避免频繁操作字符串时,每次「申请、释放」内存的开销,还做了这些优化:
- 内存预分配:SDS 扩容,会多申请一些内存(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容)
- 多余内存不释放:SDS 缩容,不释放多余的内存,下次使用可直接复用这些内存

这种策略,是以多占一些内存的方式,换取「追加」操作的速度。

这个内存预分配策略,详细逻辑可以看 sds.c 的 sdsMakeRoomFor 函数。

SDS数据类型:

SDS 一共设计了 5 种类型,分别是 sdshdr5(不再使用)、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

主要区别就在于:它们数据结构中的字符数组现有长度 len 和分配空间长度 alloc,这两个元数据的数据类型不同。

紧凑型字符串结构的编程技巧

以sdshdr8为例:


struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 字符数组现有长度*/
    uint8_t alloc; /* 字符数组的已分配空间,不包括结构体和\0结束字符*/
    unsigned char flags; /* SDS类型*/
    char buf[]; /*字符数组*/
};

uint8_t 是 8 位无符号整型,会占用 1 字节的内存空间。当字符串类型是 sdshdr8 时,它能表示的字符数组长度(包括数组最后一位\0)不会超过 256 字节(2 的 8 次方等于 256)。

以此类推:sdshdr16、sdshdr32、sdshdr64 三种类型来说,它们的 len 和 alloc 数据类型分别是 uint16_t、uint32_t、uint64_t,即它们能表示的字符数组长度,分别不超过 2 的 16 次方、32 次方和 64 次方。这两个元数据各自占用的内存空间在 sdshdr16、sdshdr32、sdshdr64 类型中,则分别是 2 字节、4 字节和 8 字节。

SDS 之所以设计不同的结构头(即不同类型),是为了能灵活保存不同大小的字符串,从而有效节省内存空间。

为了避免操作系统自动对齐,在定义数据结构时使用:


struct __attribute__ ((__packed__)) sdshdr8

作用就是告诉编译器,在编译 sdshdr8 结构时,不要使用字节对齐的方式,而是采用紧凑的方式分配内存。

课后题:SDS 字符串在 Redis 内部模块实现中也被广泛使用,你能在 Redis server 和客户端的实现中,找到使用 SDS 字符串的地方么?

1、Redis 中所有 key 的类型就是 SDS(详见 db.c 的 dbAdd 函数)

2、Redis Server 在读取 Client 发来的请求时,会先读到一个缓冲区中,这个缓冲区也是 SDS(详见 server.h 中 struct client 的 querybuf 字段)

课后题:SDS 字符串在 Redis 内部模块实现中也被广泛使用,你能在 Redis server 和客户端的实现中,找到使用 SDS 字符串的地方么?

1、Redis 中所有 key 的类型就是 SDS(详见 db.c 的 dbAdd 函数)

2、Redis Server 在读取 Client 发来的请求时,会先读到一个缓冲区中,这个缓冲区也是 SDS(详见 server.h 中 struct client 的 querybuf 字段)

3、写操作追加到 AOF 时,也会先写到 AOF 缓冲区,这个缓冲区也是 SDS (详见 server.h 中 struct client 的 aof_buf 字段)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值