Redis并没有直接使用C语言中的字符串类型,而是自己设计了一种数据结构,在Redis中,C语言中的字符串只使用在一些字符串值不会发生修改的地方,比如打印日志。它为什么要这样做?这样做的好处是什么呢?让我们先来了解一下这种被称为Simple dynamic string(SDS)简单动态字符的结构。
SDS遵循了C字符串以空字符结尾的管理,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,已经添加空字符到末尾等操作都是自动完成的。为什么要这么做?这样做的好处是SDS可以直接重用一部分C字符串函数库里面的函数。
leg: print("%s",s->buf);
回到刚才的问题,探究SDS存在的意义以及给我们带来的便捷:
1.常熟复杂度获取字符串长度 C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串进行累加,复杂度为O(N),而SDS在len属性中记录了SDS本身的长度,获取长度复杂度仅为O(1)。
2.杜绝缓冲区溢出 除了获取字符串的复杂度高之外,C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。当字符串修改时,SDS会先检查空间是否满足所需的要求,如果不满足的话,会将SDS的空间扩展至执行修改所需的大小,所以SDS不需要手动修改SDS的空间大小。
3.减少修改字符串带来的内存重分配次数 C字符串每次增减或者缩短一个字符,程序都要对保存这个C字符串的数组进行一次内存分配操作:
*如果是字符串增长操作,每次增加1个字节,N次操作就N次内存重分配,Nginx作为一个高性能的数据库,数据被修改较频繁,在这样的情况下,仅执行内存重分配就会占用一小部分的时间,如果修改频繁的话,可能会对性能造成影响。 SDS通过空间预分配和惰性空间释放来减少内存重分配所需的总时间。 若字符串修改后大小小于1M,则系统多分配一倍的空间。leg:修改后25个字节 则buf数组长度为 25+25+1("/0")=51.若大于1M,至多分配1M。 通过这种策略,内存重分配的次数由必定N次降为最多N次。
*字符串长度缩小时,空出的空间并不会立刻回收。
二进制安全
经常说Redis是二进制安全的,这是什么意思?是指其他类型的数据存储进去,取出来内容就变了么?这个理解也对,也不对。首先C语言中的字符串只能存储文本文件,对一下图片、文件不能存储,这是为什么呢?“/0”空字符是C语言用来表示字符串的结尾,但同样以为着你的字符串中不能存在空字符,否则你的数据信息就会提前结束,也就是说,你存进去的跟你取出来的数据不一样。Redis中的SDS对每个字符都是平等的,不会对任何字符进行替换、过滤等特殊处理。那么问题来了,既然SDS中的buf数组跟C的字符串不一样,那么它依靠什么判断字符的结束? 还记得么?Simple dynamic string的数据结构中有一个len属性,存放着buf数组中实际的数据长度,这便是SDS的判断依靠。
不看不知道,一看吓一跳。平常在使用Redis的String类型的时候总觉得会是一个简单的String类型,谁知道底层做了这么多小而美的细节处理,这种设计思想才是我们最应该学习的。