前言
你要是问我,在程序世界中我们程序员打交道最多的一种数据类型是什么?
我肯定会回答你是 字符串
那么在Redis中是如何实现的这种字符串数据结构呢?在Java中我们有 java.lang.String类,在C语言中通过char[]字符数组实现的字符串,众所周知Redis是由C语言实现的,那么Redis中的字符串就是通过C语言的char[]字符数组实现的吗?
不是的,Redis是通过一个名叫SDS(Simple Dynamic String)简单动态字符串实现的
接下来,我们就来研究研究,为什么不直接使用C字符串,而是要在实现一套Redis自己的字符串数据结构。请系好安全带,马上发车!
什么是SDS
代码定义
struct sdshdr{
//记录buf数组中已使用的字节的数量
//等于SDS保存的字符串长度
int len;
//buf数组中未使用的字符个数
int free;
//字符数组,用于保存字符串
char buf[];
}
上面这个名为sdshdr的结构体就是Redis中SDS的定义,很简单的对吧
下面使用两幅插图,形象的展示一下SDS中存放数据的时候是个什么样子的
图1
在图1中我们可以看到,当存放一个叫做‘Redis’这样的一个字符串的时候,在SDS中的一个内存分布的情况
- free属性的值为0,表示这个SDS没有分配任何未被使用的空间
- len属性的值为5,表示这个SDS的长度为5个字节
- buf属性是一个char字符数组,数组的前5个字节分别保存了Redis字符串的5个字符,最后一个字节保存了空字符‘\0’
SDS遵守C字符串的规定,在字符串的末尾保存一个字节的‘\0’表示字符串结束,遵守这规定的好处是,SDS可以直接使用C字符串函数库中的一部分函数
图2
在图2中我肯可以看到和图1的最大的区别就是多出了3个分配未被使用的字节空间,这种空间预分配的策略正是SDS和C字符串实现上的最大的不同之处。
SDS与C字符串的区别
传统的C字符串使用长度N+1的字符数组表示一个长度为N的字符串,并且字符串以'\0'结尾。
C语言使用这种简单的字符串表示方式,并不能满足Redis在对字符串的安全性,效率以及功能方面的要求,所以SDS需要去解决C字符串不能满足的这些问题。
获取字符串长度时效率不同
在SDS中因为存储了len表示字符串长度的属性,所以可以以O(1)的时间复杂度获取字符串的长度;在C字符串中,需要遍历char[]字符数组才能获取到字符串的长度,时间复杂度为O(N);
避免缓冲区溢出
在C字符串中,如果用户在执行strcat函数的时候,会存在内存覆盖的问题。比如有两个字符串,它们在内存中刚好是连续分布存在的。如下图所示:
图3
有S1 ‘Bob’,S2 ‘Dave’,当执行 strcat(s1,"niu")的时候,那么就会出现如下情况
图4
字符串S2原本的值被覆盖了,这种现象又被称为缓冲区溢出。SDS为了避免这种情况,在修改的时候,会先检查SDS的空间是否满足修改的所需要求,如果不满足的话,会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。这样就避免了缓冲区溢出的问题。
减少字符串修改时重新内存分配的次数
由于C字符串中不会保存自身的长度,所以它的长度总是和被使用的字节数相等,就会导致了每次修改字符串的时候,都会重新分配一块连续的内存空间,然后在修改。内存分配是一件耗时的操作,如果一个字符串被修改的频率不高,那么使用这种方式也是Ok的,但是Redis作为一款数据库它是需要解决这种对数据频繁修改的问题的。
在SDS中通过实现了空间预分配和惰性空间释放这两种优化策略
空间与分配
空间预分配用于优化SDS增长的操作:当对一个SDS进行修改的时候,在需要对SDS进行空间扩展的时候,不仅为SDS分配修改所必要的空间,还会对SDS分配未使用的空间。
其中未被使用的空间分配数量算法如下:
- 如果对SDS进行修改之后,SDS的长度小于1MB,那么程序分配和len属性同样大小未使用空间,这时SDS的len属性的值将和free属性的值相同。比如说在在修改之后SDS的len变为了10个字节,还会分配10个字节作为未使用空间。那么在buf数组的实际长度成为了10+10+1=21
- 如果对SDS进行修改之后,SDS的长度大于1MB,那么会分配1MB的未使用空间。比如说当对一个SDS修改之后,len变为了30M,那么还会再分配1MB的字节空间作为未被使用的空间。在bug数组中的实际长度变为了30MB+1MB+1字节
惰性空间释放
惰性空间释放用于优化SDS的字符串缩短的操作。当一个SDS需要缩短的时候,并不会立即使用内存重分配来回收缩短后的字节,而是使用free属性将这些字节数量记录下来,并等待将来使用
二进制安全
在C字符串中,因要求在字符串中除了在末尾,其它位置上不能包含空字符,否则就会被误识别为字符串结尾。这个限制了C字符串只能保存文本数据,不能存储二进制的数据,例如视频、音频等。
SDS不会有任何限制,当数据被存放进去的时候是什么样子,在取出来的时候就是什么样子的。
总结
总结一下C字符串和SDS字符串的主要区别
| C字符串 | SDS字符串 |
|---|---|
| 获取字符串长度时间复杂度O(N) | 获取字符串长度时间复杂度O(1) |
| API不安全,会造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
| 修改字符串长度N次,必然进行N次内存重新分配 | 修改字符串长度N次,最多进行N次内存重新分配 |
| 只能保存文本数据 | 能保存文本、视频等二进制数据 |
| 能使用<string.h>所有的函数 | 能使用<string.h>部分的函数 |