字节面试题:String 是使用什么存储的?为什么不用 c 语言中的字符串?
我们都知道,Redis 是一款高性能的 NoSQL 数据库,它提供了很多不同的数据类型来帮助我们解决不同的问题。
其中,Redis 中的 String 类型特别常用,很多时候我们使用 Redis 来存储和获取字符串数据。
但是,你是否好奇,Redis 是如何高效地存储和操作字符串的呢?为什么 Redis 不使用 C 语言中传统的字符串处理方式呢?今天我们就来聊聊这个问题。
在 C 语言中,我们习惯使用 char* 来表示字符串,实际上一串字符串就存储在一个字符数组中,这个数组的末尾是由一个特殊字符 \0 来标识字符串的结束。
虽然这种方式很直观,但它也有很多缺点,特别是在处理动态字符串时,比如无法在 O(1) 时间内获取字符串的长度,容易发生缓冲区溢出等问题。
那么,Redis 作为一款高效的数据库,是怎么解决这些问题的呢?答案就是:Redis 使用了 SDS(Simple Dynamic String)结构。
SDS 是 Redis 自定义的字符串数据结构,它对传统 C 语言字符串进行了优化,解决了 C 字符串的一些痛点。让我们一起深入探讨一下。
SDS 的结构
首先,我们来看一下 Redis 5.0 中 SDS 的数据结构:
struct sdshdr {
int len; // 当前字符串的长度
int alloc; // 已分配的空间大小
unsigned char flags; // 字符串的类型标识
char buf[]; // 字符数组,实际存储字符串数据
};
SDS 数据结构主要由四个部分组成:
len:记录字符串的实际长度。在 C 语言中,获取字符串的长度需要遍历整个字符数组,时间复杂度是 O(N),而在 SDS 中,我们直接保存了字符串的长度,获取时只需要返回这个值,时间复杂度是 O(1)。
alloc:记录当前分配给
buf[]的空间大小。在 C 语言中,我们常常需要手动管理内存,字符串的扩展需要开发者去考虑空间是否足够。而在 Redis 的 SDS 中,alloc用于记录分配的空间,如果扩展空间不足,Redis 会自动为 SDS 申请新的空间。flags:标记 SDS 的类型。Redis 对不同大小的 SDS 进行了优化,有五种类型:
sdshdr5、sdshdr8、sdshdr16、sdshdr32和sdshdr64,它们的区别在于len、alloc和flags的存储方式。具体来说,如果字符串的长度小于 5 字符,Redis 会使用sdshdr5类型,它将len和alloc都合并为一个字节来节省空间。buf[]:存储实际的字符数据,可以是任意二进制数据。
buf[]用来保存字符串数据,虽然我们常常将它用来存储文本字符串,但实际上它可以用来存储任意类型的二进制数据。
Redis 为了能够处理二进制数据,设计了 SDS 结构,可以存储包含 \0 的数据,而不像 C 字符串那样依赖 \0 作为结束标志。
这一设计使得 SDS 具有“二进制安全性”,即数据写入时保持原样,读取时也不会进行修改。
Redis 中使用 SDS 的好处
1. O(1) 复杂度获取字符串长度
在 C 语言中,字符串的长度是通过遍历字符串中的字符直到 \0 来计算的,时间复杂度是 O(N),其中 N 是字符串的长度。
而 Redis 的 SDS 结构通过 len 成员直接记录了字符串的长度,因此获取字符串长度的操作时间复杂度是 O(1)。
例如,假设我们有一个字符串 "hello",在 C 语言中,调用 strlen("hello") 时,程序需要从字符数组的开头开始遍历,直到遇到 \0,才知道字符串的长度。
而在 Redis 中,通过 SDS,只需要返回 len 的值就可以立即知道字符串的长度。
2. 二进制安全
传统的 C 字符串依赖 \0 作为结束标志,这使得 C 字符串不能安全地处理包含 \0 字符的二进制数据。举个例子,如果我们存储一个二进制文件的数据(例如图片),其中可能包含 \0 字符,这时传统的 C 字符串就无法正确处理了。
然而,Redis 的 SDS 结构通过 len 来标识字符串的长度,完全避免了 \0 的问题,保证了二进制数据的安全存储。
3. 自动扩展内存空间
C 字符串在操作时,往往需要手动管理内存。如果你向一个字符数组追加数据,你需要确保数组有足够的空间,否则就会发生缓冲区溢出,导致程序崩溃或者产生不可预知的行为。
更糟糕的是,C 的一些字符串操作函数(如 strcat)并不会自动检查空间是否足够,完全依赖开发者自行判断。
而 Redis 使用的 SDS 结构,则通过 alloc 和 len 两个成员变量来避免这个问题。每次对字符串进行修改时,Redis 会检查当前分配的内存是否足够,如果不够,它会自动扩展内存空间,以确保修改操作不会发生缓冲区溢出。
例如,在进行字符串拼接时,如果当前字符串的空间不足,Redis 会自动为字符串扩展空间,而不是依赖开发者来判断和手动管理内存。
4. 内存管理更高效
SDS 的内存管理机制使得 Redis 在内存分配和释放时更为高效。C 语言中的 malloc 和 free 在动态内存分配时并不总是能做到高效利用,可能会出现内存碎片问题。
而 SDS 在设计时考虑到了内存的扩展策略和释放策略,能够更好地利用内存空间,提高内存的使用效率。
最后,我们来看一道相关面试题:
问题:Redis 为什么使用 SDS 而不是 C 语言中的传统字符串?
回答:
Redis 使用 SDS 代替传统的 C 字符串,主要是为了克服 C 字符串的一些缺点,提升性能和安全性。
首先,SDS 通过
len成员变量实现 O(1) 复杂度获取字符串长度,而 C 字符串需要遍历整个字符数组来计算长度,时间复杂度是 O(N)。其次,SDS 支持二进制安全,能够存储包含
\0的数据,而 C 字符串无法做到这一点。此外,SDS 在内存管理上更加高效,通过alloc和len来管理内存空间,避免了缓冲区溢出和内存碎片问题。最后,SDS 支持自动扩展内存空间,而不需要开发者手动管理内存,减少了因缓冲区溢出而引起的程序崩溃风险。