前言
Redis是一款流行的高性能键值存储数据库,而简单动态字符串(Simple Dynamic Strings,SDS)是Redis内部用于处理字符串的关键数据结构之一。本文将深入探讨SDS在Redis中的工作原理、优势以及适用场景,帮助读者更好地理解和应用Redis的核心组件之一。
一、Redis的SDS长啥样呢?🤔
Redis中的简单动态字符串(Simple Dynamic String,SDS)是一种用于保存字符串值的数据结构。
它由以下三部分组成:
1.buf:一个指向字符数组的指针,用于存储实际的字符串数据。
2.len:一个整数值,表示当前字符串的长度(不包括空终止符)。
3.free:一个整数值,表示buf中未使用的字节数。
SDS的结构如下所示:
struct sdshdr {
int len; // 字符串的长度
int free; // buf中未使用的字节数
char buf[]; // 字符数组,存储实际的字符串数据
};
二、SDS这样设计的优势是什么?🤔
-
O(1)复杂度的字符串长度获取:SDS通过len属性记录字符串的长度,因此可以在常数时间内获取字符串的长度,而不需要遍历整个字符串。
-
空间预分配和惰性空间释放:SDS在分配空间时,会预先分配足够的空间来容纳未来的字符串扩展。这减少了频繁重新分配内存的需求,提高了性能。当字符串缩短时,SDS会惰性地释放多余的空间,而不是立即归还给操作系统。
-
二进制安全:SDS不仅可以存储普通的字符串数据,还可以存储二进制数据,因为它使用字符数组来保存数据,而不是依赖于空终止符。比如:我们可以将音视频转化存储在redis中。
-
支持常数时间的追加和修改:由于SDS预分配了足够的空间,追加和修改操作的复杂度为O(1),而不受字符串长度的影响。
-
兼容C字符串:SDS以空终止符结尾,因此可以将SDS作为C字符串使用,并且可以通过C字符串的函数进行操作。
SDS在Redis中采用了惰性释放(Lazy Freeing)的机制来释放多余的空间。惰性释放空间的好处主要体现在以下几个方面:
- 节省内存开销: 惰性释放允许Redis在SDS进行缩小操作时,不立即释放多余的内存空间,而是将其保留以备将来使用。这样可以避免频繁的内存分配和释放操作,减少了内存管理的开销,提高了性能。
- 减少碎片化: 频繁进行内存分配和释放操作可能导致内存碎片化问题。通过惰性释放,Redis可以将多个小块的内存释放合并为一个较大的连续空间,从而减少了内存碎片的产生。
- 避免内存拷贝: 惰性释放使得Redis可以在SDS进行扩展或缩小操作时,直接在现有的内存空间上进行修改,而无需进行内存拷贝。这可以节省CPU的时间和资源,提高性能。
- 适应动态变化: 惰性释放使得SDS可以根据字符串的动态变化,自适应地调整内存空间的大小。当字符串长度增加时,SDS可以动态扩展内存;而当字符串长度减少时,SDS可以惰性释放多余的空间。这种灵活性和自适应能力使得SDS更适合处理动态变化的字符串数据。
三、这样的设计难道只有优点吗?🤔
尽管SDS在Redis中作为字符串值的数据结构具有许多优点,但也存在一些缺点,包括以下几点:
-
内存占用:相比于使用空终止符的C字符串,SDS需要额外的内存空间来存储长度和未使用的字节数。这意味着对于相同的字符串内容,SDS可能占用更多的内存空间。
-
内存碎片:当SDS进行频繁的追加和修改操作时,可能会导致内存碎片问题。由于SDS的长度是动态变化的,当字符串缩短时,SDS不会立即归还多余的空间给操作系统,而是保留在SDS内部。这可能会导致内存碎片的增加,造成内存空间的浪费。
-
额外的复杂性:相比于简单的C字符串,SDS引入了额外的数据结构和操作逻辑。这增加了一定的复杂性,并可能导致一些开发和维护上的困难。
需要注意的是,这些缺点相对较小,并且在大多数情况下不会对Redis的性能和功能产生显著影响。Redis选择使用SDS作为字符串值的数据结构是为了提供更好的灵活性、性能和功能而做出的权衡。
四、sds是如何扩缩容的?
SDS在扩容和收缩时会采取不同的策略来管理内存。
-
扩容(Expansion): 当需要将一个SDS扩展到容纳更长的字符串时,SDS会进行以下步骤:
- 首先,SDS会检查是否有足够的未使用空间来容纳新的字符串。如果有足够的空间,则直接将新字符串复制到未使用空间,并更新长度和未使用字节数。
- 如果没有足够的空间,SDS将根据需要预先分配一块更大的内存空间。这个新分配的空间大小通常是当前字符串长度的两倍,或者按照一定的策略进行动态调整。
- 然后,SDS将原有的字符串内容复制到新分配的空间中,并更新长度和未使用字节数。
- 最后,SDS释放原有的空间,将新分配的空间作为新的SDS使用。
扩容操作的时间复杂度是O(N),其中N是字符串的长度。
-
收缩(Shrinkage): 当SDS的字符串长度减小,或者需要释放一部分未使用空间时,SDS可以进行收缩操作以减少内存占用和内存碎片。SDS的收缩策略是惰性的,只有在一些特定的条件下才会触发收缩操作:
- 当SDS的长度缩小到一定程度,例如小于当前字符串长度的一半时,SDS可以触发收缩操作。
- 当SDS的未使用空间超过一定阈值时,SDS可以触发收缩操作。
收缩操作会将未使用空间的内存归还给操作系统,减少内存碎片。实际上,收缩操作是通过重新分配内存并复制字符串内容来实现的,类似于扩容操作。
收缩操作的时间复杂度也是O(N),其中N是字符串的长度。
需要注意的是,SDS的扩容和收缩都是自动进行的,无需手动干预。Redis会在适当的时机根据需要自动执行这些操作,以提供高效的内存管理和使用。
五、扩缩容源码🤔
// SDS 扩容函数,根据新的长度 len 扩容 sds
// 如果新的长度 len 小于等于 SDS_MAX_PREALLOC,则将 sds 扩容到 len+len 的长度
// 否则,将 sds 扩容到 len+SDS_MAX_PREALLOC 的长度
// 返回扩容后的 sds
sds sdsMakeRoomFor(sds s, size_t len) {
struct sdshdr *sh, *newsh;
// 获取 sds 的头部结构 sdshdr
// 这里假定 s 为合法的 sds
sh = (void*) (s-(sizeof(struct sdshdr)));
// 获取 sds 当前的长度
size_t curlen = sdslen(s);
size_t free = sh->free;
// 如果 sds 的当前长度加上新的长度小于等于 sds 的总长度
// 直接返回 sds,无需进行扩容
if (len <= curlen) return s;
// 计算需要的总长度 newlen
size_t newlen = (curlen+len);
// 根据 newlen 的大小,确定新的总长度
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 分配新的总长度的内存
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存分配失败,返回 NULL
if (newsh == NULL) return NULL;
// 更新新的总长度和剩余空间长度
newsh->len = curlen+len;
newsh->free = newlen-(curlen+len);
// 更新 sds 的指针
return newsh->buf;
}
// SDS 缩容函数,将 sds 缩小为 len 的长度
// 返回缩容后的 sds
sds sdsRemoveFreeSpace(sds s) {
struct sdshdr *sh;
// 获取 sds 的头部结构 sdshdr
// 这里假定 s 为合法的 sds
sh = (void*) (s-(sizeof(struct sdshdr)));
// 获取 sds 当前的长度
size_t curlen = sdslen(s);
size_t free = sh->free;
// 计算需要的总长度 len
size_t len = curlen + free;
// 重新分配内存,将 sds 缩小为 len 的长度
// 返回缩容后的 sds
sh = zrealloc(sh, sizeof(struct sdshdr)+len+1);
sh->free = len - curlen;
// 更新 sds 的指针
return sh->buf;
}
六、sds在redis中的应用有哪些呢?🤔
SDS(Simple Dynamic Strings)在Redis中广泛应用于存储和处理字符串数据。下面是一些Redis中使用SDS的常见场景:
-
存储键名和键值: 在Redis中,键名和键值通常以SDS的形式存储。SDS提供了动态调整长度的能力,使得Redis能够高效地处理不同长度的键名和键值。
-
网络通信: Redis使用SDS作为网络通信的缓冲区,用于接收和发送命令和响应。SDS的动态扩容和缩容特性使得Redis能够适应不同大小的网络数据包,并提供高效的网络通信性能。
-
持久化存储: 当Redis执行持久化存储(如RDB快照或AOF日志)时,SDS用于表示和存储数据库中的字符串数据。SDS的灵活性和性能优势使得Redis能够高效地进行数据持久化操作。
-
发布与订阅: 在Redis的发布与订阅模式中,SDS用于存储和传递消息内容。由于SDS可以动态调整大小,Redis能够处理不同长度的发布消息,并将其传递给订阅者。
七、redis底层数据结构哪些使用到了SDS?🤔
Redis底层数据结构中有以下几个使用了SDS(Simple Dynamic Strings):
-
字符串对象(String Object): Redis中的字符串对象使用SDS来表示键名(key)和键值(value)。SDS提供了动态调整长度的能力,使得Redis能够高效地存储和处理不同长度的字符串数据。
-
列表对象(List Object): Redis中的列表对象使用SDS来存储和表示列表中的元素。每个列表元素都被存储为一个SDS,从而实现了对不同类型和长度的元素进行高效的访问和操作。
-
哈希对象(Hash Object): Redis中的哈希对象使用SDS来存储和表示哈希表中的键和值。每个键和值都被存储为一个SDS,使得Redis能够处理不同长度的哈希键和哈希值。
-
有序集合对象(Sorted Set Object): Redis中的有序集合对象使用SDS来存储和表示有序集合中的成员和分值。成员和分值都被存储为一个SDS,使得Redis能够对不同长度的有序集合元素进行高效的排序和检索。