「Redis数据结构」动态字符串(SDS)
一、前言
我们都知道Redis中保存的Key是字符串,value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。
Redis采用C语言来实现,但并没有使用C语言传统字符串的表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的数据结构,并将SDS作为Redis的默认字符串表示。在Redis里C字符串只会作为字面量用在一些无需对字符串修改的地方,入打印日志。
二、概述
Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS。
例如,我们执行命令:
.
那么Redis将在底层创建两个SDS,其中一个是包含“name”的SDS,另一个是包含“frozenpenguin”的SDS。
Redis是C语言实现的,其中SDS是一个结构体,源码如下:
例如,一个包含字符串“name”的sds结构如下:
SDS之所以叫做动态字符串,是因为它具备动态扩容的能力,例如一个内容为“hi”的SDS:
假如我们要给SDS追加一段字符串“,Amy”,这里首先会申请新内存空间:
如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;
如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。
三、C字符串与SDS的区别
Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题。
获取字符串长度复杂度
C语言使用N+1的字符串数组来表示长度为N的字符串,并且数组的最后一个元素总是空字符串’/0’,C字符串并不记录自身的长度信息,每次获取长度需要重新遍历字符串直到遇到’/0’为止,这个复杂度为O(N)。
SDS在len属性中保存了字符串的长度,所以获取字符串长度的复杂度为O(1),设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,不需要进行手动的修改。
杜绝缓冲区溢出
C字符串如果在修改字符串之前没有分配足够的内存空间会造成缓冲区溢出。
SDS在修改时会先检查SDS的空间是否足够,如果空间不足,会自动扩展空间至修改所需要的大小,再去执行修改操作。
减少修改字符串时的内存分配次数
C字符串的底层实现总是一个N+1个字符长度的数组,因为C字符串和底层数组之间的长度存在这种关联性,所以每次修改字符串时会重新分配数组的内存空间(如果增长字符串而不进行重新分配内存会产生缓冲区溢出,减少的话会产生内存泄露)。
为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组之间的关联,在SDS中buff长度不一定是字符数量+1,数组里面可以包含未使用的字节,而这些字节数量由SDS的free属性记录。
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
空间预分配:
当对一个SDS进行修改并需要扩展SDS的空间时,程序不仅会分配修改所需要的空间,还会为SDS额外分配未使用的空间。(SDS小于1M会额外分配等于SDS的空间,大于等于1M则额外分配1M)
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存从新分配次数。(如果要增长的长度小于未使用的空间就不会重新分配内存)
惰性空间释放:
惰性空间释放用于优化SDS的字符串缩短操作,当SDS需要缩短保存的字符串时,程序并不立即使用内存分配来回收缩短后多出来的字节,而是使用free属性将这些字节数量记录起来,并等待将来使用。
二进制安全
C字符串中的字符必须符合某种编码格式,并且除了字符串的末尾处,不允许有空字符串,否则会被认为是字符串结尾,这些限制使C字符串只能保存文本数据,不能保存二进制数据。
SDS会以处理二进制的方式来处理SDS存放在buf数组中的数据,程序不会对数据做任何限制,数据写入时是什么样,读取时就是什么样。(SDS的buf属性不是保存字符,而是保存的一系列二进制数据)
兼容部分C字符串函数
虽然SDS的API都是二进制安全的,但他们一样遵守C字符串以空字符串结尾的惯例,为了SDS可以重用一部分string.h库定义的函数,从而避免代码重复。