redis - 再谈redis的基础数据类型SDS

62 阅读5分钟

世界上并没有完美的程序,但是我们并不因此而沮丧,因为写程序就是一个不断追求完美的过程。

redis是C语言编写的,但是为了更好的实用性,在其基础上又进行了封装。其中SDS简单动态字符串就是一个典型的实现。与C语言字符串结构的对比如下:
C语言中字符串的结构
图1 C语言的字符串的结构

Redis自定义的简单字符串的结构
图2 Redis自定义的字符串的结构

由上图可知,与C语言原生的字符串结构相比,Redis自定义的简单动态字符串增加了预分配空间free与字符串长度len的记录,并且通过buf[]存储字符串的值。

这样做有三个好处,第一,获取字符串长度的时间复杂度缩短为O(1);第二,可以通过预分配空间与空间的惰性释放减少空间重分配的次数;第三,二进制安全。

首先对于SDS缩短字符串长度获取的时间复杂度。当我们要将一个字符串比如“redis”存储到redis中时,redis首先会判断这个字符串的长度,然后根据字符串长度分配对应大小的内存空间,即字符串长度加1,其中多出的1是空字符’\0’,在C语言中用空字符’\0’来代表一个字符串的结尾,但是在redis中却并不是如此,redis中以’\0’结尾是为了复用C语言的一些函数功能,这一点需要注意。同时将字符串的长度5存储到len字段中,并且由于还没有预分配的空间所以free的值为0。这样第一个字符串就存储到redis中了。以上过程除了要记录字符串长度以外与C语言字符串的存储过程是一致的。但就是因为存储了len的值,所以在我们要获取字符串长度时,就可以直接取len的值而无需遍历整个字符串,使得获取字符串长度这个函数的时间复杂度从O(n)缩短为了O(1),并且还有一个好处,就是当我们要对当前字符串执行拼接等操作时,可以预先根据记录的len值判断内存空间是否足够,从而避免内存溢出。

然后对于预分配空间与空间的惰性释放。接下来,如果我们想要对这个字符串执行拼接操作,将"good"拼接到"redis"字符串后面,在拼接之前首先判断一下free预分配空间是否足够,即free的值的大小是否大于等于要存储的字符串"good"的长度,如果预分配空间不足则需要分配内存空间,内存空间分配好以后,将字符串"good"拼接到字符串"redis"后面,并且更新拼接后字符串的长度len为9,同时预分配9字节长度的空闲内存空间,并将free的值更新为9,这样不仅将"good"拼接到了"redis"后面,将字符串更新为"redisgood",而且还预分配9字节长度的空闲空间。如果下次要拼接一个字符串"hello",会首先判断预分配空间是否足够,因为"hello"的长度为5,而预分配空间的大小为9,所以可以直接将"hello"拼接到"redisgood"字符串后面而无需调用底层去分配新的空间了,这样就减少了底层分配空间的次数,而底层分配内存空间时必然会花费一定的时间,通过这种方式减少底层分配空间的次数,也节省了字符串写入的时间。空间惰性释放也是同样的道理,当我们要缩短字符串的长度时,空出来的空闲空间并不会立即被回收,而是加入到free预分配空间中,这样后续字符串长度如果变长时如果预分配空间足够就无需底层再次重新分配内存空间了,节省了内存回收和分配两次空间重分配的次数的时间,可以大大提高写操作的效率。当然,redis也提供了api供我们手动释放预分配空间。最后,对于free预分配空间大小的规则进行介绍:当扩容操作后len的值小于1M时,预分配空间的大小与len相等;当len的值大于1M时,预分配空间的大小为1M。

最后对于二进制安全。buf[]是以字节数组而非字符数组的形式存储的,所以可以存储任何二进制的数据。由于在SDS中记录了字符数组的长度,所以我们在判断一个字符串的范围时,并不需要以空字符’\0’作为结尾的标志,完全可以通过len判断一个完整字符串的开头和结尾,所以,当我们存入二进制的数据时,有时一些特殊的字符如空字符’\0’只是作为数据的一部分存在并不标志字符串的结尾,如果使用原生的C语言字符串存储,将’\0’当作结尾标志,那么取出的值,可能就是不完整的,但是如果用redis的SDS结构就不会受到特殊字符’\0’的干扰,只根据len判断存储数据的范围,能够准确的获取到存储的完整数据。所以说SDS结构是二进制安全的。

本节通过对redis SDS结构的介绍以及对字符串的操作实例,形象的描述了SDS结构在操作字符串时的优势,通过free、len、buf[]这种三属性的记录方式,奠定了redis安全高效处理字符串的基础。

更多信息,请关注公众号:
在这里插入图片描述