Redis String 为什么不是 String?SDS 到底解决了什么问题?
上一篇我们讲了《Redis 为什么只有五种数据类型,却能支撑几乎所有业务?》,知道了 Redis 对外只有五种数据类型,而内部真正工作的却是各种不同的数据结构。
其中,使用频率最高的,就是 String。
很多人看到这里都会觉得:
String 不就是字符串吗?直接用 C 语言的
char*不就行了?
毕竟 Redis 本身就是 C 写的。
为什么还要自己重新设计一套字符串?
甚至专门发明了 SDS(Simple Dynamic String)。
Redis 官方为什么要多此一举?今天,我们就来看看到底发生了什么。
C 语言的字符串,到底有什么问题?
先来看最普通的 C 字符串。
c
char name[] = "hello";
它在内存中的样子,大概是这样:
text
+---+---+---+---+---+----+
| h | e | l | l | o | \0 |
+---+---+---+---+---+----+
最后必须以 \0 作为结束标志。
所以,每次获取字符串长度时,C 都只能这样做:
c
strlen(name);
strlen() 会从第一个字符开始,一直扫描,直到遇到 \0。
例如:
text
h↓e↓l↓l↓o↓\0↓结束
如果字符串长度只有几十个字符,这几乎感觉不到性能问题。
但是,如果字符串有几 MB 呢?
每次调用 strlen(),都要重新遍历一遍。
时间复杂度就是:
text
O(N)
对于 Redis 来说,这是不能接受的。
Redis 怎么解决这个问题?
Redis 没有继续使用 char*。
而是重新设计了一种字符串。最经典的 SDS,大概可以理解成下面这样:
c
struct sdshdr {
int len;
int free;
char buf[];
};
看起来比普通字符串多了两个字段。但是,就是这两个字段,让整个性能发生了巨大变化。
例如:
text
len = 5
free = 10
buf = hello
现在再获取字符串长度,已经不需要遍历了。
直接返回:
c
len
时间复杂度立即变成:
text
O(1)
这也是 Redis String 为什么查询速度这么快的重要原因之一。
free 字段又有什么用?
很多人第一次看源码时都会疑惑。既然已经有了长度,为什么还要保存一个 free?
举个例子。假设现在有:
text
hello
然后执行:
redis
APPEND name world
普通字符串会发生什么?
很可能需要:
text
申请更大的内存
↓
复制 hello
↓
追加 world
↓
释放旧内存
如果不断追加。
例如:
text
hello
↓
helloworld
↓
helloworldredis
↓
......
每次都申请新内存,效率会越来越低。于是 Redis 想了一个办法。
一次扩容的时候,顺便多申请一点空间。
例如:
text
len = 5
free = 15
那么下一次追加时,直接写进去即可,不用再次申请内存。
这就是 SDS 的空间预分配机制。它可以极大减少内存分配次数。
Redis 为什么追加字符串越来越快?
来看一个简单例子。
第一次:
redis
APPEND msg hello
Redis:
text
申请 32 字节
真正使用:
text
5 字节
剩余:
text
27 字节
下一次:
redis
APPEND msg world
直接利用剩余空间,不用重新申请内存。很多追加操作,因此都只是简单的内存复制。这也是 Redis String 写入性能很高的重要原因。
删除字符串,为什么也不急着释放内存?
再来看一个问题。
假设:
text
helloworldredis
执行:
redis
SETRANGE
或者:
redis
GETRANGE
甚至删除一部分内容。
很多人觉得:应该立即释放不用的空间,Redis 并没有这样做。
它通常只是:
text
len --
free ++
真正的数据空间仍然保留。
例如:
text
len = 5
free = 20
这样,下次再次追加,又可以继续利用这块空间。避免频繁申请、释放内存。这就是惰性空间释放。
SDS 为什么支持二进制安全?
这是很多面试都会问的问题。
先来看普通字符串。
例如:
text
ABC\0DEF
对于 C 来说。
遇到 \0 字符串已经结束了。
真正读取到的是:
text
ABC
后面的内容全部丢失,但是 Redis 不一样。
因为 Redis 根本不依赖 \0 判断长度。
而是:
text
len = 7
所以,即使中间存在:
text
\0
Redis 仍然可以完整保存。
例如:
- 图片
- 音频
- 视频
- 压缩文件
- Protocol Buffer
- 序列化对象
都可以直接存入 Redis,这就是所谓的:
二进制安全(Binary Safe)
Redis String 真的是字符串吗?
现在,我们再回到最开始的问题,Redis String 真的是 String 吗?
其实并不是。上一篇我们已经介绍过:
text
RedisObject
↓
Type
↓
Encoding
↓
真正的数据结构
对于 String 来说,底层甚至有多种 Encoding。
例如:
text
INT
EMBSTR
RAW
其中:真正保存字符串数据时,最终使用的就是:
text
SDS
也就是说我们每天执行:
redis
SET
GET
APPEND
背后操作的,并不是 C 字符串,而是 Redis 自己设计的一套高性能字符串。
为什么 Redis 不直接使用 C 字符串?
现在答案已经很清楚了,因为普通字符串存在很多问题:
- 获取长度需要遍历。
- 每次追加都可能重新申请内存。
- 无法保存二进制数据。
- 容易发生缓冲区溢出。
而 SDS 全部解决了这些问题:
len实现 O(1) 获取长度。free减少内存重新分配。- 支持空间预分配和惰性释放。
- 支持二进制安全。
- 所有操作都带长度控制,更安全。
所以,Redis 并不是简单使用 C 语言开发。它把很多基础组件,都重新设计了一遍。
总结
很多人觉得 Redis String 就是普通字符串。实际上,它和 C 字符串几乎已经不是同一种东西。Redis 自己设计 SDS,并不是为了炫技,而是为了真正解决性能问题。可以把 SDS 的几个核心特点总结成一句话:
用空间换时间,用额外的元数据换来更高的性能、更好的安全性以及更灵活的扩展能力。
也正因为如此,SDS 才成为 Redis 最基础、也是最重要的数据结构之一。
上一篇:《Redis 为什么只有五种数据类型,却能支撑几乎所有业务?》
下一篇:《Redis 为什么不用链表保存 List?QuickList 到底是什么?》
如果这篇文章让你真正理解了 SDS 为什么存在,欢迎点赞、收藏。
你以前是不是也认为 Redis String 就是 C 的 char*?欢迎在评论区聊聊你的理解。