正如前两个小节所说,因为C字符串并不记录自身的长度,所以对于一个包含了N个字符的C字符串来说,这个C字符串的底层实现总是一个N+1个字符长的数组(额外的一个字符空间用于保存空字符)。因为C字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或者缩短一个C字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作:
- 如果程序执行的是增长字符串的操作,比如拼接操作(append),那么在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小——如果忘了这一步就会产生缓冲区溢出。
- 如果程序执行的是缩短字符串的操作,比如截断操作(trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用的那部分空间——如果忘了这一步就会产生内存泄漏。
因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作:
- 在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的。
- 但是Redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响。
为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
1.空间预分配
空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。
其中,额外分配的未使用空间数量由以下公式决定:
- 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。举个例子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存空字符)。
- 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte。
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无须执行内存重分配。通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。
2.惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
举个例子,如果有一种使用空字符来分割多个单词的特殊数据格式,如图2-17所示,那么这种格式就不能使用C字符串来保存,因为C字符串所用的函数只会识别出其中的"Redis",而忽略之后的"Cluster"。

虽然数据库一般用于保存文本数据,但使用数据库来保存二进制数据的场景也不少见,因此,为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的(binary -safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。
Redis的数据结构还是非常重要的,了解Redis的内部数据结构,才更加有利于我们利用Redis这个缓存库去解决实际上的问题。并且可以利用它的这种解决办法思想,来结合我们生活实际上的问题,看看能否有更加好的解决方案。
一、前言 Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dy namic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。 在Redis里面,C字符串只会作为字符串字面量(stri...
众所周知,redis以高性能著称。毫无疑问,redis的高性能一定要有一些自己设计的特殊、易用且安全的数据结构作为支撑,来满足不同场景下的存储需求。下面我们就对redis常使用的几种数据结构进行详解,从中大家可以看到redis是如何使用这些数据结构,选择这些数据而不使用市面已存在的类似数据结构的原因以及这些数据结构是如何保证redis的高性能的。
简单动态字符窜
我们知道redis没有使用C语音传统的字符窜表示(以空字符结尾的字符数组,在我所有文章中统一称为C字符串),而是自己构建了一种名为**简单动态字符
Redis数据库中的每个键值对都是由对象组成的,其中:本篇文章将对以上提到的五种不同类型的对象进行介绍,刨析这些对象所使用的底层数据结构,并说明这些数据结构是如何深刻的影响对象的功能和性能的Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的抽象类型,并将SDS用作Redis的默认字符串表示例如
那么Redis将创建一个新的键值对,其中:除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区 ( buffer ):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都
Redis没有适用传统农的C语言的字符串表示(以空字符结尾的字符数组),而是自己设计了一个名为简单动态字符串(SDS)的的抽象类型。
在Redis中,C字符串只会作为字符串字面量,用在一些无须对字符串值进行修改的地方,比如打印日志。
在Redis中,包含字符串的键值对在底层都是由SDS实现的。
2.SDS的定义
结构定义如下:
struct sdshdr{
//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
② Redis是单线程的,采用了IO多路复用技术;
③ Redis未使用C语言字符串,使用了SDS字符串。
然而,很少有人能说清楚SDS字符串到底是什么,为什么使用SDS字符串比使用C语言字符串效率要高。
关于SDS
SDS全拼为:simple dynamic ...