【redis设计与实现】简单动态字符串 SDS

480 阅读4分钟
原文链接: www.mayou18.com

【redis设计与实现】简单动态字符串 SDS

2017年11月28日 17:50:29 Redis & Redis设计与实现 作者 :书生

什么是简单动态字符串 (SDS)

redis 简单动态字符串是自定义的字符串类型,对于可以被修改值的字符串,Redis底层并没有采用C语言传统的字符串表示,即以空字符结尾的字符数组,而是采用专门为其设计的简单动态字符串作为其默认字符串表示,其英文全称为Simple Dynamic String,简称SDS。除了用于保存数据库中字符串值外,SDS也可以用于缓冲区buffer,比如AOF中的缓冲区、客户端输入缓冲区等。

例如:SET  msg “hello 码友十八年”

键值对的健 msg 就是一个SDS,对象的底层实现就是一个保存这字符串 “msg”的SDS ,值也是一样道理。就想我们JAVA中的基础类型char一样。

SDS 定义

每个 sds.h/sdshdr 结构表示一个SDS值:

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

}

SDS也是以''表示结束, 这一个字节不会计入已使用的长度. 这样做的好处是可以重用C字符串函数库里面的一部分函数.

SDS 与 C字符串区别

C语言使用长度为N+1的数组表示长度为N的字符串, 最后一个元素总是空字符'',  这种方式不能满足Redis对字符串效率, 安全性, 以及功能方面的要求. 下面介绍SDS比C语言字符串的更加适用Redis的原因.

 常数时间获取字符串长度

C字符串需要遍历, 时间复杂度为O(n).

SDS直接获取,  时间复杂度为O(1).

 防止缓冲区溢出

C语言不记录自身长度, 容易造成缓冲区溢出,. 例如内存中有两个相邻字符串是s1 和 s2 .  s1长度为5个字节, s2长度为5个字节.   这时如果想要在s1后面添加一个字符串s3, 长度为5个字节, 执行

strcat(s1, s3)

但是忘记给s1分配足够的内存空间, s1的数据就会溢出到s2的空间, 导致s2的内容被修改.

SDS的空间分配策略完全杜绝了这种可能性.  当API需要对SDS进行修改时,  API会首先会检查SDS的空间是否满足条件, 如果不满足, API会自动对它动态扩展,   然后再进行修改, 这个过程是完全透明的.

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

C语言对字符串修改后都需要手动重新分配内存; 当增加长度时需要扩展内存, 否则会产生缓冲区溢出;  当缩小长度时需要释放内存, 否则会产生内存泄露.

由于Redis频繁操作数据, 内存分配和释放耗时可能对性能造成影响, SSD避免了这种缺陷, 实现空间预分配和惰性空间释放两种优化策略

空间预分配

如果修改后len长度将小于 1 M, 这时分配给free的大小和len一样, 例如修改过后为13字节,  那么给free也是13字节 .    buf实际长度变成了  13 byte+ 13byte + 1byte = 27byte

如果修改后len长度将大于等于1 M, 这时分配给free的长度为 1 M,     例如修改过后为30M,  那么给free是1M .    buf实际长度变成了  30M + 1M + 1 byte

在修改时, 首先检查空间是不是够, 如果足够, 直接使用, 否则执行内存重分配.

惰性空间释放

当缩短SDS长度时, 不进行内存释放, 而是记录到free字段中, 等待下次使用.  与此同时, 也提供相应的API, 可以手动释放内存.

二进制安全

C字符串只有末尾能保存空格, 中间如果有空格会被截取, 认作结束标识. 这样就不能保存图片, 音频视频等二进制数据了.

所有的SDS API会以二进制的方式处理SDS buf数组里面的数据, 程序不会对其中数据做任何限制, 过滤,修改和假设,  数据写入是什么样子, 读取出来就是什么样子.

例如 :  保留的数据中间出现'',  这是没有任何问题的. ,  因为它使用len而不是空字符判断结束.

兼容部分C字符串函数