1、简单动态字符串
redis是用C语言写的,而redis语言没有直接使用C语言的字符串(以\0结尾的的字符数组),而是自己构建一种名为SDS(简称)的抽象类型。
在redis内如果仅仅需要字符串字面量,无需对字符串进行修改的时候,redis会使用C字符串进行表示(打印日志),当不仅仅需要字面量,redis会用SDS进行表示。
例:
redis> SET msg "Hello World!"
以上例子实际保存着msg
与Hello World!
的SDS。
1.1 SDS的定义
每个sds.h/sdshdr结构表示一个SDS值:
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
图2-1展示了一个长度为5的SDS
- free为0,表示这个SDS没有分配任何未使用的空间
- len为5,表示这个SDS保存着一个五子节长度的字符串
- buf是一个char类型的数组,实际保存字符串的内容,以
\0
结尾
SDS遵循C字符串以空字符结尾的惯例,保存空子符的1字节空间不计算在SDS的len
属性里面,遵循这一惯例的好处是可以治直接重用一部分C字符串函数库里面的函数。例:printf("%s", s->buf)
1.2、SDS与C字符串的区别
C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是\0
。
1.2.1、常数复杂度获取字符串的长度
C字符串不记录自身长度信息,所以获取C字符串的长度,必须遍历整个字符串,对遇到的每个字符串进行计数,知道遇到代表字符串结尾的空字符为止,这个操作复杂度为O(N)。
因为SDS在len
属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。
PS:O(N)表示时间复杂度,表示的是线性阶,随着问题规模N的不断增大,上述时间复杂度不断增大,算法执行效率越低。
1.2.2 杜绝缓冲区溢出
C字符串会有缓冲区溢出问题:
如图2-7,内存中有两个连续的字符串,假定程序员为Redis
字符串进行拼接操作,且没有分配足够的空间,那么拼接的字符串就会溢出到s2所在的空间中,导致s2被意外修改(见下图)。
SDS的空间分配策略杜绝了缓冲区溢出的可能性,当API需要对SDS进行修改的时候,API会先检查SDS的空间是否满足修改所需的需求(free
的大小是否足够),不满字API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作(并同时分配未使用的空间)。
PS:缓冲区:从磁盘读取数据放在缓冲区,计算机从缓冲区读取数据,减少磁盘读写次数。
1.2.3 减少修改字符串时带来的内存重分配次数
C字符串每次增加或缩短一次,都会进行一次内存重分配操作:
增长字符串:先扩展底层数组空间大小,如果没做则造成内存溢出。
缩短字符串:释放掉缩短的那部分空间,如果没做则造成内存泄漏。
内存重分配涉及复杂算法,是一个比较耗时的操作
- 一般程序中,修改字符串的情况不太常出现,每次修改字符串重新分配一次内存也是可以接受的。
- redis作为数据库,对于速度要求严苛,数据被频繁修改的场合,如果每次修改字符串都会执行一次内存重新分配的话,那么光是内存分配就会占去一大部分时间,如果修改频繁,可能还会对性能造成影响。
为了避免C字符串的缺陷,SDS解除了字符串长度和底层数组长度之间的关联,在SDS中,数组可以包含未使用的字节(free
属性)。
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
1、空间预分配
优化字符串增长操作
对SDS进行增加操作的时候:
- 如果SDS的长度小于1MB,那么程序将分配和
len
属性同样大小的未使用空间,free
与len
属性值相等。 - 如果SDS的长度大于1MB,那么程序将分配1MB未使用的空间。
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。
在扩展SDS空间之前,会先检查未使用空间是否足够,足够则直接使用未使用空间,而无需执行内存重分配。
通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。
2、惰性空间释放
优化字符串缩短操作
对SDS进行缩短操作的时候,程序不立即回收缩短之后多出来的字节,而是使用free
属性进行记录,并等待将来的使用。
SDS也提供相应API,在有需要的时候,真正的释放SDS未使用的空间,所以不必担心惰性释放造成内存浪费。
通过惰性空间释放,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长提供了优化。
1.2.4 二进制安全
C字符串必须符合某种编码(比如ASCII),并且除字符串结尾,其他位置不能出现空字符,否则最先被程序读入的空字符将被误认为是字符串的结尾,这些限制使得C字符串只能保存文本数据,而不能保存图片、音频、视频压缩文件这样的二进制数据。
SDS的API二进制都是安全的,数据在写入是什么样的,读取就是什么样的,通过buf
数组进行保存一系列二进制数据,同时是使用len
属性值而不是使用空字符来判断字符串是否结束的。
所以SDS是二进制安全的,不仅仅可以保存文本数据,还可以保存任意格式的二进制数据。
1.2.5 兼容部分C字符串函数
虽然SDS都是二进制安全的,但是它一样遵循着C字符串以空字符结尾的惯例,使得SDS可以重用一部分<string.h>
库定义的函数,这样SDS就不用自己专门去写一些函数了,避免了不必要的代码重复。
1.3 总结
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
修改字符串长度N次必然执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或二进制数据 |
可以使用所有<string.h> 库中定义的函数 | 可以使用部分<string.h>`库中定义的函数 |
本文为《Redis设计与实现》读书笔记
大佬文章(敖丙):
https://mp.weixin.qq.com/s/uYUQ1P8Dq1Cdknxif7lF-g
https://mp.weixin.qq.com/s/VY31lBOSggOHvVf54GzvYw