Redis 不直接使用原始 C 字符串,而是本身构建了一种字符串类型,叫作 SDS(simple dynamic string), 并将 SDS 做为 Redis 的默认字符串表示.git
看一下 SDS 的定义:github
// file : sds.h struct sdshdr { // 字符串当前长度, //等于 buf 数组中已使用字节数 unsigned int len; // 剩余可用长度 unsigned int free; // 字符数组(具体存放字符串的地方) char buf[]; };
SDS buf 数组仍是遵循 C 字符串以空字符结尾的惯例,这样能够直接重用一部分 C 字符串函数库里函数算法
由于 C 字符串并不记录自身的长度信息,因此获取一个 C 字符串的长度,必须遍历整个字符串,这个操做的的复杂度为 $O(N)$.
SDS 在 len 属性中记录了 SDS 的 len 属性,获取一个 SDS 长度的复杂度仅为$O(1)$数据库
除了获取字符串长度的复杂度高以外, C 字符串不记录自身长度带来的另外一个问题是容易形成缓冲区溢出.
看看字符串拼接函数 strcat
的调用:数组
char *strcat( char *dest, const char *src );
在执行 strcat
时假定 dest 分配了足够多的内存,能够容纳 src 字符串中的内容,而一旦这个假定不成立,就会产生缓冲区溢出.安全
举个例子,假设程序中有两个在内存中紧邻着的 C 字符串 s1 和 s2,执行 strcat(s1, s2)
. 如图所示:函数
若是在执行以前忘了给 s1 分配足够的空间,在执行以后, s1的数据将溢出到 s2 的空间之中,致使 s2 的内容被意外更改.性能
与 C 字符串不一样, SDS 的 空间分配彻底杜绝了发生缓冲区溢出的可能性,当 Redis 须要对 SDS 进行修改时,API 会首先检查 SDS 的空间是否知足修改的要求,若是不知足, API 会自动将 SDS 空间扩展至执行修改所需的大小优化
举个例子, Redis 里也有一个执行拼接的函数: sdscat
, 假如执行 sdscat(s," WORLD");
, 若是检查发现 s 的空间不足以拼接, sdscat 会先扩展s 的空间,在执行拼接.
拼接操做先后如图所示:编码
注意上图中的 SDS, sdscat 不只进行了拼接操做,还额外分配了11 字节的未使用空间,刚好等于拼接以后的字符串长度, 这并非巧合,它是 SDS 的空间分配策略,下面会讲到
对于 C 字符串,每次增加或 缩短一个 C 字符串,程序总要多保存这个 C 字符串的数组进行一次内存重分配操做:
若是执行的增加字符串的操做,在执行以前,程序要先经过内存重分配来扩展空间的大小,若是忘了可能会产生缓冲区溢出
若是执行的是缩短字符串的操做,在执行以后,程序要经过内存重分配来释放再也不使用的部分空间,若是忘了,可能会产生内存泄露
内存重分配涉及到复杂的算法,而且可能须要执行系统调用.
在通常程序中,若是修改字符串长度的状况不太常出现,那么每次修改都执行一次内存重分配是能够接受的
可是 Redis 做为数据库,常常用于对于速度要求严苛,数据被频繁修改的场合,若是每次修改都执行一次内存重分配的话,光是执行内存重分配就会占去修改字符串所用时间的一大部分,若果修改频繁发生,可能会对系统性能形成影响
为了不 C 字符串的这种缺陷, SDS 使用了未使用空间这一律念, 在 SDS 中, buf 数组的长度不必定是字符数量加上一个结束符,数组里面还包含未使用的字节,这些字节的数量由 SDS 的 free 属性记录.
经过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略.
空间预分配
空间预分配用于优化 SDS 的字符串增加操做: 当 SDS 的 API 对一个 SDS 进行修改,而且须要空间扩展时, 程序不只会为 SDS 分配修改所需的空间,还会为 SDS 分配额外的未使用空间.
额外的未使用空间分配规则以下:
若是对 SDS 修改后,字符串的长度( len 的值) 小于 1 MB, 那么程序分配和 len 同样大小的未使用空间,此时 free=len.
在上节实例中, s="HELLO" + " WORLD", len = 11,程序会分配11字节的未使用空间, SDS的 buf 数组的实际长度变成 11+11+1 = 23 字节(额外的一字节保存空字符)
若是对 SDS 修改后, 字符串的长度大于 1 MB, 那么程序会分配1 MB 的未使用空间.举个例子,若是进行修改以后, SDS 的len 变成 10 MB, 那么程序会分配 1MB 的未使用空间(free=1 MB),SDS 的 buf数组实际长度将变为 10 MB + 1 MB + 1 byte.
经过预分配策略, Redis
能够减小连续执行字符串增加操做所需的内存重分配次数.
惰性空间释放
惰性空间释放用于优化SDS 的字符串缩短操做: 当须要缩短 SDS 保存的字符串时,程序并不当即回收缩短以后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,等待未来使用.
举个例子, sdstrim 函数接受一个 SDS 和一个 C 字符串,从 SDS 字符串两侧删除在 C 中出现的字符,对于某个SDS中 buf保存着 `s = "xxabcyy", 执行
sdsstrim(s, "xy");
再执行:
sdscat(s, "Redis");
SDS 结构变化以下图:
注意执行 sdstrim 以后 SDS 并无释放多出来的 6 字节,而是将这6 字节做为未使用空间保留在了 SDS 里面,在以后的 sdscat 操做中没必要为了拼接从新分配空间.
SDS 也提供了相应的 API, 在有须要时,真正地释放 SDS 未使用空间,不用担忧惰性空间策略形成的空间浪费.
C 字符串中的字符必须符合某种编码 ,除了末尾以外,其余位置不能包含空字符,不然最早被读入的空字符会被误认为字符串结束标志,这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片,音频,视频这样的二进制文件.
维基百科的Null-terminated string 词条给出了空字符结尾字符串的定义,说明了这种表示的来源)
数据库保存二进制数据的场景并很多见,为了确保 Redis 可使用各类不一样的场景, SDS 中的 API 都是二进制安全的,全部的 SDS API 会以处理二进制的方式来处理 buf 中数据,程序不会对其中的数据作任何假设,数据在写入时什么,在读出时就是什么样.
个人博客: http://ygmyth.github.io