Redis源码阅读—数据结构之简单动态字符串 sds.c/sds.h

sds.c/sds.h


一、 SDS的定义

SDS(Simple Dynamic String,简单动态字符串),是redis底层使用的字符串表示,取代了C语言中默认的char *类型。

与C语言的默认字符串相比,sds既可以高效地实现追加和长度计算,同时也是二进制安全的,并会在字符串末尾自动添加 '\0'。

采用默认的空字符结尾,使得sds可以兼容部分C字符串函数。

所以,在Redis中,C字符串只会作为字符串字面量,用在一些不需要对字符串值进行修改的地方。

在sds.h/sdshdr中,对SDS进行了定义:

typedef char *sds;     //类型别名,用于指向 sdshdr 中的 buf 属性

struct sdshdr {
    unsigned int len;  //已使用的字节数
    unsigned int free; //可用的字节数
    char buf[];        //数据空间,实际保存字符串数据的地方
};

对于 sdshdr 来说,sizeof(struct sdshdr)= 2*sizeof(unsigned int),因为 char buf[] 等价于 char buf[0], 它仅对编译器有效,并不实际占用存储。

所以,一个sds字符串申请的实际内存为:sizeof(sdshdr)+len+free+1,其中的“1”实际上是sds自动添加的字符'\0'。

另外,sds 指向 sdshdr 中的 buf 属性,要是想获取 sdshdr 结构体指针,可以通过以下代码来获取:

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

二、 SDS API

函数 作用 时间复杂度
sdslen 返回 SDS 实际保存的字符串的字节数 O(1) ,直接读取 SDS 的 len 属性
sdsavail 返回 SDS 可用空间的字节数 O(1)
sdsnew 创建并返回一个包含给定C字符串的 SDS ,新 SDS 不预留任何空间,即 free 属性值为0 O(N),N为给定C字符串的长度
sdsempty 创建并返回一个不包含任何东西的 SDS O(1),直接读取 SDS 的 free 属性
sdsdup 创建一个给定 SDS 的副本 O(N),N为给定 SDS 的长度
sdsfree 释放给定的 SDS O(N),N为给定 SDS 的长度
sdsclear 清空 SDS 保存的字符串内容,即将'\0'置于 sdshdr 的 buf[0] 处,惰性地删除 buf 中的内容 O(1)
sdsMakeRoomFor 扩展 SDS 中 buf 的长度 O(N)
sdsRemoveFreeSpace 回收 SDS 中的空闲空间 O(N)
sdsIncrLen 调整 SDS 字符串:增加字符串长度,缩减空余空间,并将 '\0' 放到新字符串的末端。用于在调用 sdsMakeRoomFor() 函数对字符串进行扩展,用户在字符串末端写入某些内容之后,正确更新 free 和 len 属性 O(1)
sdsAllocSize 返回给定 SDS 分配的内存字节数,包括 SDS 头部开销,已用字节数(len),可用字节数(free)和一字节的空字符 O(1)
sdsgrowzero 用 0 字节将 SDS 扩充至指定长度 O(N),N为扩展新增的字节数
sdscatlen 将长度为 len 的给定C字符串追加到 SDS 字符串的末尾 O(N)
sdscat 将给定C字符串追加到 SDS 字符串的末尾 O(N)
sdscatsds 将给定 SDS 字符串追加到 SDS 字符串的末尾 O(N)
sdscpylen 将给定C字符串的前 len 位复制到 SDS 字符串当中,覆盖 SDS 字符串原有内容,并在字符串的末尾添加 '\0' O(N)
sdscpy 将字符串复制到 SDS 当中,覆盖原有内容 O(N)
sdsfromlonglong 根据输入的 long long 类型的值,创建一个 SDS O(N)
sdsll2str 将 long long 类型的值转换为字符串 O(N)
sdsull2str 和 sdsll2str 函数功能相同,不过是用于 unsigned long long 类型的值 O(N)
sdscatvprintf 打印函数,将格式化的字符串追加到 SDS 字符串后,内部使用了 vsnprintf() 函数 O(N^2)
sdscatprintf 打印函数,功能同上,内部调用 sdscatvprintf 函数 O(N^2)
sdscatfmt 功能与 sdscatprintf 函数类似,但是速度要快得多,因为这是 Redis 自己实现的 sprintf() 函数,而不是上面用的libc库的方法 O(N)
sdstrim 接受一个 SDS 和一个 C 字符串作为参数,从 SDS 左右两端分别移除在给定 C 字符串中出现过的字符 O(M*N),M 为 SDS 长度, N 为给定的 C 字符串的长度
sdsrange 保留 SDS 给定区间内的数据,不在区间内的数据将会被覆盖或清除。给定区间的索引可以为负数,且 -1 代表着最后一个字符, -2 代表着倒数第二个字符 O(N),N为被保留数据的字节数
sdstolower 将 SDS 字符串中的所有字符转换为小写 O(N)
sdstoupper 将 SDS 字符串中的所有字符转换为大写 O(N)
sdscmp 对比两个 SDS 字符串是否相同 O(N),N为比较的两个字符串中较短的一个的长度
sdssplitlen 使用分隔符对 SDS 字符串进行分割,并返回分割后的 SDS 数组。并且该函数是二进制安全的 O(n^2)
sdsfreesplitres 释放 sdssplitlen() 函数产生的 SDS 数组 O(N^2)
sdscatrepr 将给定的字符串以带引号的方式追加到给定 SDS 字符串的末尾 O(N)
sdssplitargs 将一行文本分割成多个参数。这个函数主要用于 config.c 中对配置文件进行分析 O(N^2)
sdsmapchars 对 SDS 字符串做字符替换,将 from 中出现的字符替换为 to 中对应位置上的字符。比如:调用 sdsmapchars(mystring, "ho", "01", 2) , "hello" 会转换为 "0ell1" O(N^2)
sdsjoin 将 C 字符串数组连接成一个 SDS 字符串,数组元素间由分隔符 seq 分隔 O(N^2)

三、 SDS 特点

  1. 常熟复杂度获取字符串长度
    SDS 在 len 属性中记录了 SDS 本身的长度,因此获取 SDS 长度的操作,即 sdslen() 的时间复杂度仅为 O(1)。
    与 C 字符串相比,获取字符串长度的时间复杂度从 O(N) 降为了 O(1)。

  2. API 安全,不会造成缓冲区溢出
    在需要对 SDS 进行修改时,API 会对 SDS 的 len 属性和 free 属性进行检查,以确保有足够的空间来满足修改所需的要求,因此不会造成缓冲区溢出问题。

  3. 减少修改字符串时带来的内存重分配次数
    因为 Redis 作为数据库,经常用于速度要求严苛、内容修改频繁的场合,所以如果修改字符串时带来的内存重分配次数要尽可能的少,而在 SDS 里采用了空间预分配惰性空间释放两种策略来进行优化。
    通过以上两种策略,可以将 C 字符串的修改字符串长度 N 次必须进行 N 次内存重分配,优化为修改字符串长度 N 次最多需要进行 N 次内存重分配。

    • 空间预分配
      空间预分配,用于优化 SDS 的字符串增长操作。
      当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩充时,不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用空间(即 free 属性的值)。

      分配的额外空间会受修改后的 SDS 的长度(即 len 属性的值)影响:

      1. len 小于 1MB :将会给 SDS 分配和 len 属性同样大小的未使用空间,即SDS 的 len 属性值将和 free 属性值相同。
      2. len 大于等于 1MB :将会给 SDS 分配 1MB 的未使用空间,即 free 属性值等于 1MB。
    • 惰性空间释放
      惰性空间释放,用于优化 SDS 的字符串缩短操作。
      当 SDS 的 API 需要缩短 SDS 保存的字符串时,并不会立即使用内存重分配来回收多余的字节,而是使用 free 属性来对这些字节数量进行记录。

  4. 二进制安全
    C 字符串中的字符必须符合某种编码,并且除末尾之外,字符串里面不能包含空字符。故而 C 字符串只能用来保存文本数据。
    而 SDS 字符串由于不使用空字符,而是使用 len 属性来作为字符串结尾的标志,所以 SDS 的 buf 属性可以用来存储像图像、音频、视频、压缩文件等的二进制数据。
    所以说, SDS 的 API 都是二进制安全的(binary-safe)。

  5. 兼容部分 C 字符串函数
    虽然 SDS 的 API 都是二进制安全的,但是由于 SDS 字符串同样遵循 C 字符串以空字符结尾的惯例,所以 SDS 可以重用部分 <string.h> 库定义的函数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容