简单动态字符串

Redis中没有直接使用C语言的字符串类进行表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string ,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

在Redis中,C字符串只会作为字符串字面量用在一些字符串不需要修改的地方,比如打印日志那里,如果是需要一个可以修改的字符串的时候,就需要用到SDS表示字符串,比如在Redis中,包含字符串值的键值对在底层都是由SDS实现的。

例如:

SET msg "hello world"在Redis中执行命令,Redis会在数据库中创建一个新的键值对

键值对的键和值都是一个字符串对象,对象底层实现是一个保存这字符串"msg"和"hello world"的SDS

RPUSH fruits "apple" "banana" "cherry"

键值对的键是一个字符串对象,保存着"fruits"的SDS
键值对的值是一个列表对象,列表对象中包含三个字符串对象,这三个字符串对象分别由三个SDS实现,第一个SDS保存"apple"字符串,第二个SDS保存"banana"字符串,第三个SDS保存"cherry"字符串

SDS的功能

1,保存数据库中的字符串
2,SDS用作缓冲区:AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的

SDS的定义

struct sdshdr{
int len;//记录buf数组中已使用的字节的数量,等于SDS所保存字符串的长度 
int free;//记录buf数组中未使用字节的数量
char buf[];//字节数组,用于保存字符串
}
SDS示例.png

1,其中free属性为0,表示SDS没有分配任何未使用的空间
2,len属性5,表示SDS保存了一个5字节长的字符串
3,buf属性是一个char类型的数组,前5个字节保存了redis这5个字符,最后一个保存了空字符'\0'

SDS遵循C字符串一空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性中,遵循空字符结尾这个好处是,SDS可以直接重用一部分C字符串函数库里面的函数

比如有一个指向SDS的指针s,那么我们可以直接用printf("%s",s->buf)来打印redis这个字符串,不需要为SDS编写专门的打印函数

SDS和C字符串的比较

1,常数复杂度获取字符串长度

SDS获取字符串长度,所需复杂度O(1),因为SDS中有len这个属性,可以在SDS的API执行时自动完成,不需要手动修改长度的工作,而C字符串获取长度需要O(N),这样复杂度降低了,获取字符串长度不会成为Redis的性能瓶颈,无论我们对一个非常长的字符串键反复执行STRLEN命令也不会对系统性能造成任何影响因为STRLEN的复杂度为O(1)

2,杜绝缓冲区溢出

C字符串很容易出现缓冲区溢出的问题
比如使用strcat(char *dest,const char *src),把src的字符串拼接到dest上,假设s1="redis",s2="mongodb",假设此时s1有足够的空间来容纳src字符串中所有内容,那么不会出现溢出,一旦不够了,就会出现溢出,如果此时将s1的内容修改为s1="redis cluster",而此刻没有为s1分配足够的空间,那么s1的数据就会溢出到s2所在的空间中

s1和s2拼接在内存中的样子.png

修改s1之后的样子.png

SDS的空间分配策略:当SDS API需要对SDS修改的时候,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展到执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题
SDS API中进行拼接的函数是sdscat,可以将c字符串拼接到给定的SDS所保存的字符串的后面,但是在执行拼接操作之前,sdscat会先检查给定的SDS的空间是否足够,如果不够,sdscat会先扩展SDS的空间,然后执行拼接操作
sdscat(s," cluster")
sdscat执行之前的SDS

sdscat执行后的SDS

3,减少修改字符串时带来的内存重分配次数

c字符串的底层是一个n+1的字符长的数组,c字符串的长度和底层数组的长度之间存在这种关联性,每次增长或缩短一个c字符串,都会对这个c字符串的数组进行一次内存重分配的操作
如果增长字符串的操作,那么会先通过内存重分配来扩展底层数组的空间大小,如果没有这个,会产生缓冲区溢出
如果缩短字符串操作,会通过内存重分配来释放字符串不在使用的空间,如果忘了这个会发生内存泄漏

redis作为数据库,会被频繁的修改,这样如果用c字符串会对性能造成影响

为了避免这种问题,SDS通过未使用空间解除字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就是由SDS的free属性记录

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略

1,空间预分配

用于优化SDS的字符串增长的操作:当SDS的API对一个SDS进行修改的时候,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额为的未使用的空间

额外分配的未使用空间数量由以下公式决定:

如果对SDS进行修改之后,SDS的长度将小于1MB,那么程序分配和len属性同样大小的未使用空间

如果对SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间

通过预分配策略,redis可以减少连续执行字符串增长操作所需的内存重分配次数

在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够,API就会直接使用未使用空间,而无须执行内存重分配,通这种策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次

2,惰性空间释放

用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串的时候,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

sdstrim(s,"XY")//移除SDS字符串中所有的X和Y
通过惰性空间释放策略,SDS避免了缩短字符串字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化
SDS也提供了相应的API,让我们可以在有需要的时候真正的释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费

4,二进制安全

c字符串中字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,这就限制了c字符串只能保存文本数据,不能保存图片,音频,视频,压缩文件这样的二进制数据

sds中不会对其中的数据做任何限制,过滤,或者假设,数据写入什么样,读取就是什么样,redis不是用这个数组来保存字符,而是用他来保存一系列二进制数据,是根据len属性来决定是否结束,而不是空字符

5,兼容部分C字符串函数

虽然SDS的API都是二进制安全的,但是他们一样遵循c字符串以空字符结尾的惯例:这些API会将SDS保存的数据的末尾设置为空字符,并会为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数

<string.h>/strcasecmp(sds->buf,"hello")函数
strcat(c_string,sds->buf);

总结
C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N此内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数
SDS API
函数 作用 时间复杂度
sdsnew 创建一个包含给定的C字符串的SDS O(N),N是给定c字符串的长度
sdsempty 创建一个不包含任何内容的空SDS O(1)
sdsfree 释放给定的SDS O(N),N是被释放SDS的长度
sdslen 返回SDS已使用的空间字节数 可以读取len属性获取,O(1)
sdsavail 返回SDS的未使用空间字节数 可以通过读取free属性,O(1)
sdsdup 创建一个给定的SDS副本 O(N),N是SDS长度
sdsclear 清空SDS保存的字符串内容 因为惰性空间释放策略,O(1)
sdscat 将给定的c字符串拼接到SDS字符串末尾 O(N),N是被拼接c字符串的长度
sdscatsds 将给定的sds拼接到另一个sds字符串末尾 O(N),N是被拼接SDS字符串的长度
sdscpy 将给定的c字符串复制到SDS里面,覆盖SDS原有的字符串 O(N),N是被复制C字符串的长度
sdsgrowzero 用空字符将sds扩展到给定长度 O(N),N是扩展新增的字节数
sdsrange 保留SDS给定区间内的数据,不在区间内的 数据会被覆盖或清空 O(N),N是被保留字段的字节数
sdstrim 接受一个SDS和一个c字符串作为参数,从SDS中移除所有在c字符串中出现过的字符 O(n^2),n是给定c字符串的长度
sdscmp 对比两个sds字符串是否相同 O(n),n为两个sds中较短的那个sds的长度
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容

  • redis没有直接使用c语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种“简单动态字符串”(si...
    亮亮_ff3d阅读 368评论 0 0
  • Redis SDS与C字符串区别 Redis没有直接使用C语言传统的字符串,而自己构建了一种简单动态字符串(Sim...
    binge1024阅读 1,500评论 0 0
  • 前言 从今天开始,从头到尾来整理一遍Redis,为了督促自己保持良好的学习习惯,打算用写文章的方式来控制自己,尽量...
    于情于你阅读 314评论 1 2
  • 简单动态字符串 C语言传统的字符串表示,是以空字符('\0')结尾的字符串数组,下面简称为C字符串。 Redis自...
    xMustang阅读 393评论 0 0
  • 简介   Redis 没有直接使用C语言的字符串表示,而是构建了一种称为简单动态字符串(Simple Dynami...
    阳光课代表阅读 1,360评论 0 0