PPXu

Redis基本数据结构-SDS

2018-10-14

String

  String是我们最常用的Redis基本数据结构之一。Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
  在Redis里边,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方,比如打印日志:

1
redisLog(REDIS_WARNING, "Redis is now ready to exit, bye bye...");

SDS与C字符串的区别

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

  C语言使用长度为N+1的字符串来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符’\0’

C字符串
  因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须便利整个字符数组,对遇到的每个字符进行技术,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)
#计算C字符串长度的过程
  和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1),这确保了获取字符串长度的操作不会成为Redis的性能瓶颈。譬如”Redis”的长度为5,程序只需要访问SDS的len属性就可以立即得到长度值为5字节
#5字节长的SDS

杜绝缓冲区溢出

  由于C字符串不记录自身长度,会带来另一个问题,就是容易造成缓冲区溢出。

1
char *strcat(char *dest, const char *src)

  假定用户在执行strcat函数时,已经为dest分配了足够多的内存,则可以容纳src字符串中的所有内容,而一旦这个假定不成立时,就会产生缓冲区溢出。
  举个例子,假设程序里有两个在内存中紧邻着的C字符串s1和s2,其中s1保存了字符串”Redis”,而s2则保存了字符串”MongoDB”,如图所示:
在内存中紧邻的两个C字符串

  如果此时要通过执行:

1
strcat(s1, " Cluster");

将s1的内容修改为”Redis Cluster”,但如果粗心的他却忘了在执行strcat之前为s1分配足够的空间,那么在strcat函数执行之后,s1的数据将溢出到s2所在的空间中,导致s2的内容被意外地修改了,如图所示:
#s1的内容溢出到了s2所在的位置上

  SDS的空间分配策略则完全杜绝了发生这种情况的可能性:当SDS API需要对SDS的内容进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动把SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现上述的缓冲区溢出问题。

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

  因为C字符串并不记录自身的长度,所以一个C字符串的底层实现总是额外的多出一个字符空间用于保存空字符。因为C字符串的长度和底层数组长度之间存在着这种关联性,所以每次增长或缩短一个C字符串,都总会在保存这个C字符串的数组时引起一次内存重分配操作

  而因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作:

  • 在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的,但Redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是这个操作的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响

  为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数加1,数组里边可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录
  通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略:

  1. 空间预分配
      空间预分配用于优化SDS的字符串增长操作:利用额外的未使用空间进行预分配以减少内存的频繁分配,这一点类似Java中的ArrayList。
      当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。而字符串最大长度为 512M。
    譬如,假如SDS进行修改后变为13字节(小于1MB),那么此时SDS的buf数组的实际长度将变成13+13+1=27字节(额外的1字节用于保存空字符)。假如SDS进行修改后变为2MB(大于等于1MB),则程序将会分配1MB的未使用空间,也就是说,SDS的buf数组的实际长度将为2MB + 1MB + 1byte。

  2. 惰性空间释放
      当要缩短SDS保存的字符串时,程序并不立即使用内存充分配来回收缩短后多出来的字节,而是使用表头的free成员将这些字节记录起来以备用。

二进制安全

  SDS是二进制安全的,它可以存储任意二进制数据,因为SDS使用len属性的值而不是像C语言字符串那样以空字符(‘\0’)来标识字符串结束。

  因为传统C字符串符合某种编码(比如ASCII),字符串不仅末尾,就连字符串里的内容也不能包含标记着结束的字符。如ASCII这种编码的操作的特点就是:遇零则止。即,当读一个字符串时,只要遇到’\0’结尾,就认为到达末尾,就忽略’\0’结尾以后的所有字符。因此,如果传统字符串保存图片、音频、视频等二进制文件,操作文件时就被截断了。

兼容部分C字符串函数

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

总结

C字符串与SDS之间的区别

扫描二维码,分享此文章