深入理解Redis 数据结构—简单动态字符串sds

Redis是用ANSI C语言编写的,它是一个高性能的key-value数据库,它可以作用在数据库、缓存和消息中间件。其中 Redis 键值对中的键都是 string 类型,而键值对中的值也是有 string 类型,在 Redis 中 string 类型运用还是很广泛的。本文主要介绍 string 的数据结构—— 简单动态字符串(Simple Dynamic String) 简称sds。

sds 实现

sds 的数据结构:

struct sdshdr {

     //buf 已占用的长度
     int len;

     // buf 剩余的可用的长度
     int free;
   
     // 保存字符串数据的地方
     char buf[]; 
}

结构 sdshdr 保存了 len、free 和 buf 三个属性,分别记录字符的已使用的长度,未使用的长度,以及实际保存字符串的数组。
以下是一个新建的,保存 hello world 字符串的 sdshdr 结构:

struct sdshdr {
    len = 5;
    free = 0;
    buf = "hello\0"; 
}
  • free 属性值为0,表示这个sds没有分配未使用的空间。
  • len 属性值为5,表示这个sds保存了一个五字节长的字符串。
  • buf 属性是一个 char 类型的数组,数组的前五个字节分别保存了 'h'、'e'、'l'、'l'、'o' 五个字符,而最后一个字节保存了空字符'\0'。

sds 遵守 C 字符串以空字符串结尾的惯例,保存的空字符串一个字节空间不计算在 sds 的 len 属性里面。添加空字符串到字符串末尾等操作,都是由 sds 函数自动完成的,所以这个空字符对于使用者来说完全是透明的。

通过 len 属性,可以实现时间复杂度 O(1) 的长度计算。另外通过对 buf 分配一些额外的空间,并使用 free 记录未使用空间的长度,sdshdr 可以减少内存的重新分配。这是 sds 相对 c 字符串的一个优势。

为何 Redis 不用 C 语言表示字符串

Redis 是使用 C 语言开发的,而在使用最多的字符串上,Redis 没有使用 C 语言传统的字符串表示,而且使用自己构建的简单动态字符串(sds)
在 C 语言中,字符串可以用一个 \0 结尾的 char 数组表示。比如 hello world 在 C 语言中就可以表示为"hello world\0"。数组一般初始化以后长度就已经固定了,不能支持字符串追加append长度计算操作:

  • 每次计算字符串长度都要遍历一遍数组,所以时间复杂度是O(N)
  • 对字符串每次进行追加操作,需要对字符串进行一次内存分配

sds 优化追加字符操作

Redis 作为数据库,对于查询速度要求严格,数据修改也比较频繁,如果每次修改字符串都需要执行一次内存分配的话,都会占用大量的时间。所以 Redis 选择了 sds 而不是 C 字符串,sds 可以减少追加字符的内存分配。通过举例来说明,执行以下操作时,sds 内部的变化:

redis> set msg "hello world"
OK

redis> append msg " again"
(integer)18

redis> get msg
"hello world again"

首先 set 命令创建并保存hello world 到一个 sdshdr 中,这个 sdshdr 的值如下:

struct sdshdr {
     len = 11;
     free = 0;
     buf = "hello world\0";
}

当执行 append 命令时,相对应的 sdshdr 被更新,字符串 " again" 会被追加到原来的 "hello world" 之后:

struct sdshdr {
     len = 17;
     free = 17;
     buf = "hello world again\0                ";
}

当调用 set 命令创建 sdshdr 时,Redis 没有给 sdshdr 分配多余的空间,free 属性为0。而在执行 append 操作之后,Redis 为 buf 分配了多于所需空间一倍的大小。

在执行 append 命令之后,保存 "hello world again" 共需要17 + 1 个字节,但是程序为 sdshdr 分配了 17 + 17 + 1 = 35 个字节,而后续如果在对 sdshdr 进行追加操作,只要追加的长度不超过 free 属性值,那么就不需要对 buf 进行内存重分配。

比如执行以后命令并不会引起 buf 的内存重分配,因为新追加的字符串长度小于17:

redis> append msg  " again"
(integer) 23

对应的 sdshdr 结构如下:

struct sdshdr {
     len = 23;
     free = 11;
     buf = "hello world again again\0               ";
}

redis 内存分配可以查看源码 sds.s/sdsMakeRoomFor,sdsMakeRoomFor 函数描述了内存分配的策略,下面的该函数的伪代码:

// sdshdr:追加前的字符
// addlen:追加字符串
sds sdsMakeRoomFor(sdshdr, addlen) {
   
    // 多余空间大于追加空间,无序再分配内存,直接返回
    if (free >= addlen) return s;
    // 计算新字符的长度
    newlen = (len+addlen); 
   // 如果新字符的长度小于 SDS_MAX_PREALLOC,就分配两倍新字符空间
  //  如果新字符的长度大于 SDS_MAX_PREALLOC,就分配新字符空间 + SDS_MAX_PREALLOC 空间
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    // 分配内存
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    // 更新 free 属性
    newsh.free = newlen - len;
    return newsh;
}

而对于字符的缩短操作,Redis 保存缩短后的字符串,此时并不会进行内存重分配,而是使用 free 属性记录缩短的字符长度。

总结

Redis 的 string 类型为何使用sds而不是 C 字符串,因为sds有两点优势:

  • 计算字符长度,C 字符串复杂度O(n),而 sds 复杂度为 O(1)
  • 字符追加操作,C 字符串每次都需要对内存进行重分配,而 sds 每次会进行动态扩容,当添加字符小于空闲字符时,不会对内容进行分配,减少系统等待时间

参考

Redis 设计与实现

如果觉得文章对你有帮助的话,请点个赞吧!

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

推荐阅读更多精彩内容