Redis是查询数据很快的no Sql数据库。其原因不只是因为它存储在内存中,还因为它存储各种类型的数据结构。比如这期说到的String类型。String类型在Redis中应用很广泛。Redis中存储数据是以键值对的方式。这个键都是String类型。所以下面我们看下String类型的底层数据结构吧。
1. 简单字符串(SDS)
1.1Redis是由C语言开发的。为啥字符串没有使用C字符串呢?
- 这是因为C字符串是以\0为结尾。所以保存的字符串长度总是比原字符串长1。而且存储的特殊字符也可能由于\0得特性导致数据丢失。如果Redis想要存储文件数据则就不能使用C字符串。否则会导致数据丢失。
- C字符串是以char[]数组来实现的,在创建出来之后就不能改变其长度了,所以如果执行字符串拼接或者字符串截取就需要创建新的char[]数组。这样就就会徒增性能的消耗。
1.2 Redis 字符串实现
Redis使用SDS来实现。
1.2.1 SDS结构
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
Redis虽然还是以\0为结尾但是不会再遇到\0就认为字符串到头了。这样做的好处还可以使用C语言中一些操作字符串的方法。
1.2.2 获取字符串长度
C字符串需要判断\0的位置,所以计算长度是O(n)。而SDS有len字段可以直接判断字符串长度。所以是O(n)。
1.2.3 杜绝缓冲区溢出
- C语言字符串如果需要把一个字符串后面拼接另一个字符串。因为C语言字符串没有记录字符串的长度。假定没有分配足够的内存给后面一个字符串的长度。则会导致产生缓存区溢出。
- SDS在进行拼接时会判断可用长度free是否可以容纳拼接字符串的长度。如果不能够容纳则会进行扩容。扩容后达到足够容纳拼接字符串的可用空间后才会继续进行拼接操作。这样就不会造成缓存区的溢出了。
1.2.4 减少修改字符串时带来的内存重分配次数
- 正如上面所说拼接字符串。C语言需要每次都对目标字符串进行内存重新分配。所以只要每次操作都需要进行重新分配。
- 为了解决上面的问题SDS实现了空间预分配和惰性空间释放两种策略。
a. 空间预分配
ⅰ. 对SDS进行修改后如果长度小于1MB。那么SDS就会分配和len一样长度的大小的额外空间。比如分配后的长度为13。最终的len = 13 + 13 + 1 = 27。额外的一字节用于保存空字符。
ⅱ. 如果分配后长度大于1MB。那么SDS就会分配多1MB的额外的空间。比如分配后的长度为30MB。最终的len = 30MB + 1MB + 1字节。
b. 惰性空间释放
ⅰ. 如果把字符串缩短。也不会把空余出来的空间给立马释放掉。会等待字符串变长时使用。SDS也提供了响应的API。让我们可以完全释放掉free的空间。
1.2.5 二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
SDS的API都是二进制安全的(binary-safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。
总结
所以根据上面的几个方面我们可以看出Redis使用SDS对字符串进行了优化。保证了字符串的安全性,提高了字符串操作的性能。
参考:《Redis开发与实现》