Redis数据类型包括String、Hash、List、Set、Zset但是这些数据类型在Redis底层是怎么存放到内存里面的,下面这几种数据结构和对象可以让你更加深入的理解Redis(本文内容参考了《redis设计与实现》不得不说这是一本好书)。
Redis对与String的存储并没有直接采用C语言的传统空字符串,这里说的是没有直接采用并不是不采用。Redis在C语言字符串的基础上构建了一种叫做简单动态字符串(SDS)的数据结构。既然名为动态字符串相对于C语言的传统字符串一定是有一些可变的地方,这个地方就是相遇于C语言的传统字符串他的扩充和拼接相对来说更加简单。
值得注意的是当客户端往Redis里面set一个字符串的时候,键值对的键和值都是使用SDS存储的,此外不只是String数据类型是使用SDS存储的,加入set进Redis的是一个字符串数组那么数组里面的字符串在Redis的存储方式也是SDS(例如 redis> RPUSH name “H” “X” “L” 那么字符串“name”、“H”、“X”、"L"都是使用SDS存储的)。其实在Redis的数据库里面,包含字符串值得键值对在底层都是使用Redis实现的。
除了用于存储Redis数据库中的字符串值之外,SDS还被用作缓冲区:AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。
struct sdshdr {
// 记录buf数组中已经使用过得字节数量,就是数组被使用的长度
int len;
// 记录buf数组中未被使用的字节数量
int free;
// 字节数组,用于保存字符串 string中的内容就存放在这个数组里面
char buf[];
}
对与len和free就比较简单字面意思,在buf字段,这个buf属性是一个char数组,这个char数组和C语言的传统字符串相同他用来保存字符串内容,而且最后一个字节保存空字符串’\0’(和C传统字符串保持一致)同时这个空字符串的不记录在len里面也不记录在free里面,这个空字符串最大的作用就是可以让我们用C语言字符串函数库里面的函数。
C语言的传统字符串使用长度为N+1的字符串数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符串’\0’。
获取字符串长度
传统的字符串需要遍历整个数组知道遇到代表结束的空字符串为为止,所以传统字符串获取字符串长度的复杂度为O(N);那么对于SDS太简单了字符串的长度就记录在len变量里面复杂度为O(1)。
杜绝缓冲溢出
除了计算长度复杂度高,C字符串不记录带来的另一个问题是容易造成缓冲区溢出。举个 strcat函数可以将一个字符串拼接到另一个字符串的后面,strcat函数假定用户在执行这个函数的时候已经为要拼接的字符创建了足够多的空间,而一旦这个假设不成立那么就有可能产生缓冲区溢出(strcat的缓冲不溢出需要程序员提前判断,并且在空间不足的情况下分配空间,函数的本身并不保证不溢出。)。SDS杜绝了这个的发生,当要拼接的字符串长度超过free的数量之后API会自动将SDS的空间扩展至执行修改所需要的大小。然后再执行拼接操作。所以SDS你并不需要关心是否会溢出。
减少修改字符串时带来的内存重分配次数
前面也说到了,C字符串使用N+1个长度的字节数组保存长度为N的字符串。所以在C语言执行和字符串长度相关的操作时都需要对C字符串的数组进行一次内存重分配操作,也就是不管你是往原来的字符串添加还是减少字符只要你的操作设计长度的变化你都需要调用操作系统中内核态的代码执行内存重新,这通常是一个比较耗时的操作,C字符串这么设计是考虑到一般程序中修改长度的情况出现频次不高,所以每次涉及长度修改重新内存分配是可接受的(没有最好的设计,只有最合适的设计)。
但是Redis这样的设计是不能接受的,字符串的内容修改完全是看用户行为,没有办法假设你所有的用户的修改都不频繁,如果每次涉及长度的修改都要执行内存重新分配的话,光是用户态内核态的切换以及内核态的内存重新分配就已经很耗时了。SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联,SDS的这种设计也是一种典型的空间还时间。
a、空间预分配
空间预分配用于优化SDS字符串增长操作,当SDS的API对于一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。其中额外分配规则为:如果SDS进行修改后,SDS的长度(也就是len的值)小于1MB,那么程序分配和len属性值相同的未使用的大小,这个时候free=len(举个 假如修改之后len变成了13字节,那么程序会额外预分配13字节给SDS,这个时候buf数组的长度是13+13+1=27字节,len是13,free也是13 );如果修改SDS进行修改之后,SDS的长度大于1MB,那么程序只会额外预分配1MB的空间给SDS(举个 假如修改之后len是13MB,那么预分配1MB给SDS这个时候free=1MB,buf数组的长度是13MB+1MB+1byte)。通过空间预分配让分配次数从N次变为最多N次。
b、惰性空间释放
当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重新分配来回收多出来的空间,而是使用free属性将这些字节的数量记录起来并等待将来使用。与此同时,SDS也提供了相应的API,让我们可以在有需要时,真正的释放SDS的未使用空间。
二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读人的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
虽然数据库一般用于保存文本数据,但使用数据库来保存二进制数据的场景也不少见,因此,为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的 (binary-safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写人时是什么样的,它被读取时就是什么样。这也是我们将SDS的buf属性称为字节数组的原因一Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。
兼容部分C字符串函数
虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分《string.h》库定义的函数。