当前位置: 首页 > 科技观察

Redis数据结构SDS

时间:2023-03-12 08:34:48 科技观察

Redis有很多底层数据结构,包括SDS、ZipList、SkipList、LinkedList、HashTable、Intset等,如果你对Redis的理解只停留在get和set层面,那就是远远不够应对面试题。本文简单介绍一下Redis底层最重要的数据结构——简单动态字符串(SimpleDynamicString,简称SDS)。Redis是用C语言开发的,但它并没有使用C语言传统的字符串表示方式(以空字符结尾的字节数组,以下简称C字符串),而是构建了一个简单的动态字符串(simpledynamicstring,SDS),并使用SDS作为Redis的默认字符串表示形式。在Redis中,C字符串仅在不需要修改字符串值的地方用作静态字面量。当Redis需要的不仅仅是一个字符串字面量,而是一个可以修改的字符串值时,Redis会使用SDS来表示字符串值,比如Redis数据库中包含字符串的键值对底层都是由SDS实现的。举个例子,如果客户端执行命令:redis>SETmsg"helloworld"ok,那么Redis会在数据库中新建一个键值对,其中:键值对的key是一个字符串对象,并且对象的底层实现是一个保存字符串“msg”的SDS。键值对的值也是一个字符串对象,对象的底层实现是一个存储字符串“helloworld”的SDS。SDS除了用来在数据库中保存字符串值外,还用作缓冲区:AOF模块中的AOF缓冲区和客户端状态中的输入缓冲区都是由SDS实现的。总之,SDS是Redis最基本也是最重要的数据结构。1.SDS的定义每个sds.h/sdshdr结构体代表一个SDS值:structsdshdr{//记录buf数组中使用的字节数//等于SDS中存储的字符串长度intlen;//记录在bufarrayThenumberofunusedbytesintfree;//字节数组,用来保存字符串charbuf[];}用图片表示:SDS遵循C字符串以null字符结尾的约定,保存null的1个字节character空格不计入SDS的len属性中,为空字符额外分配1个字节的空间,在字符串末尾添加空字符等操作由SDS函数自动完成,所以这个空字符对SDS用户很重要,是完全透明的。2、SDS和C字符串的区别现在,C语言用一个长度为N+1的字符数组来表示一个长度为N的字符串,而字符数组的最后一个元素永远是空字符“”。这种简单的C语言字符串表达方式,在安全性、效率、功能性等方面都无法满足Redis对字符串的要求。具体有以下几个方面。2.1获取复杂度不变的字符串长度由于C字符串没有记录字符串的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到复杂度这个操作的复杂度是O(n),直到空字符。在Redis的SDS中,这个时间复杂度仅为O(1)。2.2消除缓冲区溢出除了获取字符串长度复杂度高之外,C字符没有记录自己的长度带来的另一个问题就是缓冲区溢出。比如C语言的strcat函数可以将字符串的内容拼接到dest字符串的末尾,但是当字符串的容量不够时,就会出现缓冲区溢出,因为字符串也是基于实现的一个数组并且有大小限制。Redis的SDS已经排除了这个问题,那么它是怎么解决的呢?当API要修改SDS时,API会先检查SDS的空间是否满足修改所需的空间。如果不够用,API会自动更改SDS的扩展空间,然后再进行实际的修改操作。这避免了缓冲区内存溢出。2.3减少修改字符串引起的内存重新分配次数。如前所述,API会在修改SDS字符串时自动扩展。如果每次修改都伴随着字符串中数组的内存重新分配,效率可想而知。所以Redis实现了空间预分配和惰性空间释放两种优化策略。空间预分配空间预分配用于优化SDS的字符串增长操作:当SDSAPI修改了SDS,需要扩展SDS的空间时,程序不仅会把修改所需的空间分配给SDS,还要为SDS分配额外的未使用空间。一般来说,额外分配的未使用空间的大小有两种可能:如果修改SDS后,SDS的长度将小于1MB,那么程序会分配与len属性大小相同的未使用空间。这时,SDS的free属性的值就会和len属性的值一样。也就是说,修改后的SDS字符串还有将近一半的容量。如果修改SDS后SDS的长度大于或等于1MB,程序将分配1MB的未使用空间。这是固定的。通过空间预分配,Redis可以减少连续执行字符串操作所需的内存重新分配次数。LazyspacereleaseLazyspacerelease用于优化SDS的字符串缩短操作:当SDSAPI需要缩??短保存在SDS中的字符串时,程序不会立即使用内存重新分配来回收缩短后的多余字节,而是使用free属性记录了这些字节的数量并等待将来使用。2.4二进制安全在C语言中,字符串的存储必须符合一定的编码(ASCII),字符串中不能包含空字符,否则会被认为是字符串的结尾。这些限制使得C字符串只能存储文本数据,而不能存储图片、音频、视频、压缩文件等二进制数据。所以,为了解决C字符串的不足,Redis的buf数组存储的是二进制数据,这也是SDS的buf数组被称为字节数组的原因。2.5与一些C字符串函数的兼容尽管RedisAPI都是二进制安全的,但它们也遵循C字符串以空字符串结尾的约定。这些API总是将SDS中保存的数据的结尾设置为一个空字符,并且总是在为buf数组分配空间时,会多分配一个字节来容纳这个空字符。这是为了让那些存储文本数据的SDS可以重用一些C函数。比如我们有一个SDS的指针s,那么我们可以直接使用stdio.h/printf函数打印出SDS保存的字符,执行如下语句:printf("%s",s->buf);字符串值“Redis”,无需为SDS编写特殊的打印功能。