在开始学习本章内容之前,我们应该来学习一下什么是 Redis?
什么是 Redis
Redis(Remote Dictionary Server)是一个开源的内存数据存储系统,通常被用作缓存、数据库和消息中间件。它支持多种数据结构,包括字符串(strings)、哈希表(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。Redis 最显著的特点之一是其高性能,主要由于数据存储在内存中,以及采用了单线程的事件循环模型。
以下是一些关键特点和用途:
-
内存存储:Redis 主要将数据存储在内存中,这使得它能够提供快速的读写操作。然而,为了持久性,可以将数据定期写入磁盘或者使用快照功能。
-
数据结构:Redis 支持多种数据结构,包括字符串、哈希表、列表、集合和有序集合。这种多样性使得 Redis 适用于各种用途,如缓存、计数器、排行榜等。
-
持久性:Redis 提供持久性选项,可以将数据保存到磁盘上,以便在重启时保留数据。
-
原子性操作:Redis 的命令是原子性的,这意味着对于给定的操作,要么全部执行成功,要么全部失败,没有中间状态。
-
分布式:Redis 支持分布式架构,可以通过主从复制(master-slave replication)实现数据的复制,以提高可用性和容错性。
-
发布/订阅:Redis 支持发布/订阅模式,允许多个客户端订阅频道,从而实现消息的广播。
-
事务:Redis 支持事务,通过 MULTI、EXEC、DISCARD 和 WATCH 等命令实现事务操作。
由于其灵活性和高性能,Redis 被广泛应用于各种场景,包括缓存、实时分析、排行榜、计数器等。它是一个流行的开源项目,并且有着庞大的社区支持。
简单动态字符串
在 Redis 中,简答动态字符串(SDS)是一种特殊的字符串表示形式,用于表示 Redis 中的字符串对象。SDS 是 Redis 自行实现的一种字符串结构,相比于 C 语言中的原始字符串,SDS 具有更多的优势,主要体现在动态调整内存大小、二进制安全、O(1) 复杂度的字符串长度获取等方面。
SDS 的定义
每个 sds/sdshdr 结构表述一个 SDS 值:
c
struct sdshdr{
int len;
int free;
char buf[];
}
有如下结构图所示:
上图中展示了 SDS 的结构示例,其中:
- free:该属性的值为 0,表示这个 SDS 没有分配任何未使用空间。
- len:该和塑性的值为 3。
- buf:该属性是一个 char 类型的数组,数组的前三个字符同时保存了三个 R。
SDS 遵循 C 字符串以空字符串结尾的惯例,保存空字符串的 1 字节空间不计算在 SDS 的 len 属性里面,并为空字符串分配额外的 1 字节空间以及添加空字符串到末尾等操作,都是由 SDS 函数自动完成的,所以这个空字符串对于 SDS 的使用来说是完全透明的。
SDS 和 C 字符串的区别
简单动态字符串(SDS)和 C 字符串在实现和特性上存在一些区别,这些区别使得 SDS 更适合作为 Redis 中字符串对象的内部表示。
动态调整内存大小
C 字符串
C 字符串是一系列字符的数组,以 null 字符('\0')结尾。在 C 语言中,字符串的内存大小是固定的,由数组的大小决定。例如,如果你声明一个字符数组来存储字符串,数组的大小就是字符串能够容纳的最大字符数。这就导致了一些限制和潜在的性能开销:
-
固定大小: C 字符串的内存大小是固定的,因为数组的大小在声明时就确定了。如果字符串的长度超过了数组的大小,就可能导致数据溢出或截断。
-
内存重新分配: 当字符串长度变化时,如果新的长度超过了之前分配的内存大小,就需要重新分配内存来容纳更长的字符串。这涉及到动态内存分配和释放的操作,通常通过 malloc、realloc 和 free 等函数来实现。
-
性能开销: 动态内存分配和释放可能引入性能开销。重新分配内存需要时间,并且频繁的分配和释放操作可能导致内存碎片化,增加了程序的复杂性和运行时的不确定性。
SDS
SDS 是 Redis 中使用的一种字符串表示方式,它具有动态调整内存大小的特性,以适应字符串实际长度的变化。这种设计的目标是降低频繁的内存分配和释放操作,提高内存利用率,并减轻程序员在处理字符串时的负担。
SDS 的核心特性是它可以动态地调整内存大小。当字符串的长度变化时,SDS 能够根据实际需要自动扩展或收缩内存,而无需手动进行内存管理。这使得 SDS 更适合处理动态变化的字符串内容。
SDS 通过提前分配一些额外的空间,避免了每次字符串长度变化都进行内存重新分配的需要。这种方式减少了频繁的内存分配和释放操作,提高了性能,尤其在有大量字符串动态变化的情况下效果显著。
SDS 的设计允许字符串的内存大小比实际字符串长度大一些,这使得 SDS 可以更灵活地适应字符串的变化。这种方式提高了内存利用率,减少了内存碎片。
通过上面的比较,可以得出 C 字符串是一固定大小的字符数组,长度确定后无法自动调整。在长度变化时,可能需要手动重新分配内存,导致性能开销。
SDS 是 Redis 中使用的字符串表示方式,具有动态调整内存大小的特性。它可以根据字符串长度自动扩展或收缩内存,避免频繁的内存分配和释放,提高性能。SDS 还通过提前分配额外空间,灵活适应字符串变化,提高内存利用率。
二进制安全
C 字符串
C 字符串以 null 字符('\0')作为结束标志,这表示字符串的结束。这意味着在 C 字符串中,任何包含 null 字符的数据都会被视为字符串的结束,可能导致对于包含 null 字符的二进制数据的不友好。
C 字符串对二进制数据不太友好,因为它将 null 字符用于终止字符串。如果二进制数据中包含 null 字符,字符串处理函数会在遇到 null 字符时停止处理,导致数据截断。
SDS
SDS 对字符串的内容没有任何假设,可以包含任意的二进制数据,包括空字符('\0')。SDS 并不以 null 字符作为字符串的结束标志,而是使用自己的长度属性来确定字符串的长度。
SDS 是二进制安全的,因为它可以包含任意的二进制数据,而不受 null 字符的限制。这使得 SDS 更适合存储和处理二进制数据,例如图像、音频或其他不以 null 字符结尾的数据。
SDS 使用一个额外的长度属性来存储字符串的实际长度,因此可以正确处理包含 null 字符在内的任意二进制数据。
总体而言,SDS 在处理二进制数据时更为灵活和安全,因为它不依赖于 null 字符来表示字符串的结束,而是使用长度属性来确保对字符串的正确处理。这使得 SDS 在存储和处理二进制数据时更具通用性。
长度获取复杂度
获取 C 字符串的长度需要遍历整个字符串,复杂度为 O(n),其中 n 是字符串的长度。而获取 SDS 字符串的长度是 O(1)复杂度的操作,因为 SDS 结构中保存了字符串的长度信息。
惰性空间释放
惰性空间释放是一种优化策略,它主要涉及在字符串长度减小时延迟释放多余的内存,只在需要重新分配内存时才进行释放。
C 字符串
C 字符串的内存大小是固定的,由数组的大小决定。如果字符串长度减小,内存并不会随之减小。
由于 C 字符串的内存大小固定,即使字符串的实际长度变小,也不会自动释放多余的空间。这可能导致内存浪费,尤其是在字符串长度经常变化的情况下。
SDS
SDS 具有动态调整内存大小的特性,可以根据字符串长度的变化自动扩展或收缩内存。
SDS 采用惰性空间释放策略。当字符串长度减小时,并不立即释放多余的空间,而是等到需要重新分配内存时才进行释放。这样可以减少频繁的内存分配和释放操作,提高性能。
SDS 在分配内存时通常会预留一些额外的空间,避免了每次字符串长度变化都进行内存重新分配的需要。这也有助于减轻内存碎片化的问题。
SDS 通过延迟释放多余的空间,避免了在每次字符串长度减小时都立即进行内存释放的开销。这在处理动态变化的字符串长度时非常有用。
总体而言,SDS 通过惰性空间释放策略,可以更有效地管理内存,特别是在字符串长度动态变化的情况下,避免了频繁的内存分配和释放,提高了性能和内存利用率。
兼容性
C 字符串
C 字符串是 C 语言中的基本字符串表示,由字符数组构成,以 null 字符('\0')结尾。
C 字符串与现有的 C 库和函数直接兼容。C 标准库中的字符串处理函数(如 strlen、strcpy、strcmp 等)以及其他 C 函数都是设计用于操作以 null 结尾的 C 字符串的。
由于 C 字符串本质上是字符数组,因此可以方便地与其他 C 数据类型进行交互和操作。
SDS
SDS 是 Redis 中使用的一种动态字符串表示,具有长度属性和动态调整内存大小的特性。
SDS 可以通过头部和尾部的附加空间与 C 字符串兼容。即,SDS 的内容部分可以直接当作 C 字符串来使用。
SDS 头部包含一个长度属性,因此可以通过访问该属性来获取 SDS 的长度,这使得 SDS 可以适应使用 C 字符串表示的接口。
SDS 的内容部分可以包含 null 字符,因此在需要时可以将 SDS 作为 C 字符串来处理,保留 C 字符串的兼容性。
SDS 也提供了一些与 C 字符串兼容的函数,例如 sdslen 用于获取 SDS 的长度,sdscmp 用于比较 SDS 的内容等。
参考文献
- 书籍:Redis 设计于实现
总结
C 字符串 | SDS |
---|---|
获取字符串长度的复杂的为 O(N) | 获取字符串长度的复杂度为 O(1) |
API 是安全的,可能会造成缓冲区溢出 | API 是安全的不会造成缓冲区溢出 |
修改字符串长度 N 次必然需要执行 N 次内存重新分配 | 修改字符串长度 N 次最多需要执行 N 次内存重新分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有的 <string.h> 库中的函数 |
可以使用一部分的 <string.h> 库中的函数 |