Redis(设计与实现):01---数据结构之SDS动态字符串(raw、embstr、int、struct sdshdr)

一、动态字符串(SDS)介绍

  • Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字 符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示
  • 在Redis里面,C字符串只会作为字符串字面量(string literal)用在一些无须对字符串值 进行修改的地方,比如打印日志:
redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");
  • 当Redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis 就会使用SDS来表示字符串值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。举个例子,如果客户端执行下面的命令,那么Redis将在数据库中创建一个新的键值对,其中:
    • 键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串“msg”的SDS
    • 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world”的 SDS
redis> SET msg "hello world"
OK
  • 当做缓冲区使用:除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区(buffer)AOF模块中的 AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的,在之后介绍AOF持久化 和客户端状态的时候,我们会看到SDS在这两个模块中的应用

二、SDS的底层实现(struct sdshdr)

struct sdshdr {
    //记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
    int len;

    //记录buf 数组中未使用字节的数量
    int free;

    //字节数组,用于保存字符串
    char buf[];
};
  • SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作,都是由SDS函数自动完成的,所以这个空字符对于SDS的使用者来说是完全透明的。遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数
printf("%s", s->buf);
  • 例如:下面的SDS保存了字符串“Redis”。这个SDS为buf数组分配了五字节未使用 空间,所以它的free属性的值为5

三、SDS API

四、SDS与C字符串的区别

①常数复杂度获取字符串长度

  • C语言存在的不足:因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)

  • 和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)

  • 设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无须进行任何手动修改长度的工作
  • 总结:通过使用SDS而不是C字符串,Redis将获取字符串长度所需的复杂度从O(N)降低到了O(1),这确保了获取字符串长度的工作不会成为Redis的性能瓶颈。例如,因为字符串键 在底层使用SDS来实现,所以即使我们对一个非常长的字符串键反复执行STRLEN命令,也不会对系统性能造成任何影响,因为STRLEN命令的复杂度仅为 O(1)

②杜绝缓冲区溢出

  • C语言存在的不足:C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。举个例子,C语言的<string.h>/strcat函数可以将src字符串中的内容拼接到dest字符串的末尾:
char *strcat(char *dest, const char *src);
  • 例如:因为C字符串不记录自身的长度,所以strcat假定用户在执行这个函数时,已经为dest分配了足够多的内存,可以容纳src字符串中的所有内容,而一旦这个假定不成立时,就会产生缓冲区溢出。下面的程序有两个在内存中紧邻着的C字符串s1和s2,其中s1保存了字符 串"Redis",而s2则保存了字符串"MongoDB"

  • 如果一个程序员决定通过执行:
strcat(s1, " Cluster");
  • 将s1的内容修改为"Redis Cluster",但粗心的他却忘了在执行strcat之前为s1分配足够的空 间,那么在strcat函数执行之后,s1的数据将溢出到s2所在的空间中,导致s2保存的内容被意外地修改,如下图所示

  • SDS的改进:与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API 需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的 话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所 以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题
  • 举个例子,SDS的API里面也有一个用于执行拼接操作的sdscat函数,它可以将一个C字符串拼接到给定SDS所保存的字符串的后面,但是在执行拼接操作之前,sdscat会先检查给定 SDS的空间是否足够,如果不够的话,sdscat就会先扩展SDS的空间,然后才执行拼接操作。例如,原本字符串的长度为5,我们执行:

sdscat(s, " Cluster");

  • sdscat将在执行拼接操作之前检查s的长度是否足够,在发现s目前的空间不足以拼接"Cluster"之后,sdscat就会先扩展s的空间,然后才执行拼接"Cluster"的操作,拼接操作完成之后的SDS如下图所示

  • 注意:上图,sdscat不仅对这个SDS进行了拼接操作,它还为SDS分配了13 字节的未使用空间,并且拼接之后的字符串也正好是13字节长,这种现象既不是bug也不是巧合,它和SDS的空间分配策略有关,我们在下面介绍这一策略

③空间预分配策略与惰性空间释放策略

  • C语言的不足之处:C语言在处理字符串时,如果字符串需要拼接或者释放,那么都会进行内存的重分配,然后将新字符串的值保存在新的内存中。但是Redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次 修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占 去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响
  • SDS的改进:
    • 为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录
    • 通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略

①空间预分配策略

  • 空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间
  • 额外分配的未使用空间数量由以下公式决定:
    • 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。举个例 子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间, SDS的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存空字符)。
    • 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte
  • 通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数
  • 通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低 为最多N次
  • 演示案例:对于下图所示的SDS值s来说,如果我们执行sdscat(s,"Cluster")

  • 那么sdscat将执行一次内存重分配操作,将SDS的长度修改为13字节,并将SDS的未使用 空间同样修改为13字节,如下图所示

  • 如果这时,我们再次对s执行sdscat(s,"Tutorial")。那么这次sdscat将不需要执行内存重分配,因为未使用空间里面的13字节足以保存9字节 的"Tutorial",执行sdscat之后的SDS如下图所示

惰性空间释放

  • 策略:当SDS的API需要缩短SDS保存的字符串 时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字 节的数量记录起来,并等待将来使用
  • 通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可 能有的增长操作提供了优化
  • 与此同时,SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用 空间,所以不用担心惰性空间释放策略会造成内存浪费
  • 演示案例:sdstrim函数接受一个SDS和一个C字符串作为参数,移除SDS中所有在C字符 串中出现过的字符。对于下面的SDS,如果我们执行sdstrim(s, "XY")将移除SDS字符串中所有的'X'、和'Y'

  • 会将SDS修改成下图所示的样子。注意执行sdstrim之后的SDS并没有释放多出来的8字节空间,而是将这8字节空间作为未 使用空间保留在了SDS里面,如果将来要对SDS进行增长操作的话,这些未使用空间就可能 会派上用场

④二进制安全

  • C语言的不足:C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符 串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制 使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制 数据。举个例子,如果有一种使用空字符来分割多个单词的特殊数据格式,如图2-17所示,那 么这种格式就不能使用C字符串来保存,因为C字符串所用的函数只会识别出其中的"Redis", 而忽略之后的"Cluster"

  • SDS的改进:
    • SDS的API都是二进制安全的 (binary -safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数 据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被 读取时就是什么样
    • 这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符, 而是用它来保存一系列二进制数据
    • 使用SDS来保存之前提到的特殊数据格式就没有任何问题,因为SDS使用len属性 的值而不是空字符来判断字符串是否结束
  • 例如:

⑤兼容部分C字符串函数

  • 虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例:这些 API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一 个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分库 定义的函数
  • 通过遵循C字符串以空字符结尾的惯例,SDS可以在有需要时重用函数库,从 而避免了不必要的代码重复
  • 例如,我们可以利用C语言的strcasecmp和strcat等函数
strcasecmp(sds->buf, "hello world");
strcat(c_string, sds->buf);
  • 下图对C字符串和SDS之间的区别进行了总结:

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值