前一段时间看了《Redis设计与实现》终于了解了Redis底层数据结构的实现原理,最近空闲想自己做笔记总结一下。
0、概述
我们都知道redis是由C语言编写的键值对数据库,所以我们很容易就会猜想redis最常用数据结构之一字符串就是C语言中传统的字符串类型,但实际上在redis中,C字符串只会在用在字面量时被使用,其余大多数情况下,redis是使用了自己构建的一个名为简单动态字符串(SDS)的类型。
1、SDS的定义
struct sdshdr {
//记录buf数组中已使用字节的数量,即SDS所保存的字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
2、为什么SDS比C字符串更适合于Redis?
(1)O(1)复杂度获取字符串长度
- 因为C字符串没有记录自身长度信息,需要遍历字符串去获取长度,复杂度为O(N)。
- 而对于SDS来说,只要访问len属性,立即可以获取长度,复杂度为O(1)。
(2)SDS的空间分配策略杜绝了发生缓冲区溢出的可能性
- 因为C字符串不记录自身长度,所以在执行拼接操作时,是默认假设用户已经在执行这个操作前,已分配了足够多的内存空间,而一旦用户并没有按假设的要求预先分配了足够多的内存,则可能发生缓冲区溢出。
- 而当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。
(3)SDS减少了修改字符串时带来的内存重分配次数
- 每当对C字符串进行拼接/截断操作时,程序需要先通过内存重分配来扩展/释放底层数组的空间大小,否则就会发生缓冲区溢出/内存泄漏。而在redis中,这种每次修改字符串都需要重分配一次内存的方式太过耗时,且频繁发生还可能对性能有影响。
- 而SDS实现的空间预分配和惰性空间释放策略很好地对上面问题进行了优化:
1、空间预分配
即扩展SDS字符串时,SDS API会先检查未使用空间(free)是否足够,若足够的话,API就会使用未使用的空间,无需执行内存的重分配。若未使用的空间不足,程序不仅会为SDS分配修改所必须的空间,还会为SDS分配额外的未使用空间,其分两种情况进行分配:若假设扩展后的len<1M,则会给free分配与len相等的未使用空间;若扩展后的len>=1M,则给free分配1M的未使用空间。
2、惰性空间释放
而惰性空间释放策略是用于优化SDS的字符串缩短操作:当SDS API需要缩短SDS保存的字符串时,程序并不会立即重分配内存释放这些多出来的字节,而是使用free属性将这些字节数量记录起来,并等待将来使用。
注意:SDS也提供了相应API,让我们在有需要时真正的地方式SDS未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。
(4)二进制安全
- C字符串中的字符必须符合某种编码,并且除了字符串末尾之外,字符串里面不允许包括空字符,否则该空字符会被误认为是字符串结尾。这些种种限制导致C字符串稚嫩保存文本,而不能保存图片、音频等二进制数据。
- 而SDS的API都是二进制安全的,即不会对其中数据进行任何限制、过滤,这样使得redis不仅可以保存文本数据还可以保存任意格式的二进制数据。
(5)兼容部分C字符串函数
虽然SDS API都是二进制安全的,但是它们一样遵循C字符串以空字符结尾的惯例,使得那些保存文本数据的SDS可以重用一部分<string.h>定义的函数。