🚀深入理解redis的简单动态字符串(SDS)🚀

538 阅读10分钟

前言

Redis是一款流行的高性能键值存储数据库,而简单动态字符串(Simple Dynamic Strings,SDS)是Redis内部用于处理字符串的关键数据结构之一。本文将深入探讨SDS在Redis中的工作原理、优势以及适用场景,帮助读者更好地理解和应用Redis的核心组件之一。

一、Redis的SDS长啥样呢?🤔

image.png

Redis中的简单动态字符串(Simple Dynamic String,SDS)是一种用于保存字符串值的数据结构。

它由以下三部分组成:

1.buf:一个指向字符数组的指针,用于存储实际的字符串数据。

2.len:一个整数值,表示当前字符串的长度(不包括空终止符)。

3.free:一个整数值,表示buf中未使用的字节数。

SDS的结构如下所示:

struct sdshdr {
    int len;         // 字符串的长度
    int free;        // buf中未使用的字节数
    char buf[];      // 字符数组,存储实际的字符串数据
};

二、SDS这样设计的优势是什么?🤔

  1. O(1)复杂度的字符串长度获取:SDS通过len属性记录字符串的长度,因此可以在常数时间内获取字符串的长度,而不需要遍历整个字符串。

  2. 空间预分配和惰性空间释放:SDS在分配空间时,会预先分配足够的空间来容纳未来的字符串扩展。这减少了频繁重新分配内存的需求,提高了性能。当字符串缩短时,SDS会惰性地释放多余的空间,而不是立即归还给操作系统。

  3. 二进制安全:SDS不仅可以存储普通的字符串数据,还可以存储二进制数据,因为它使用字符数组来保存数据,而不是依赖于空终止符。比如:我们可以将音视频转化存储在redis中。

  4. 支持常数时间的追加和修改:由于SDS预分配了足够的空间,追加和修改操作的复杂度为O(1),而不受字符串长度的影响。

  5. 兼容C字符串:SDS以空终止符结尾,因此可以将SDS作为C字符串使用,并且可以通过C字符串的函数进行操作。

SDS在Redis中采用了惰性释放(Lazy Freeing)的机制来释放多余的空间。惰性释放空间的好处主要体现在以下几个方面:

  1. 节省内存开销: 惰性释放允许Redis在SDS进行缩小操作时,不立即释放多余的内存空间,而是将其保留以备将来使用。这样可以避免频繁的内存分配和释放操作,减少了内存管理的开销,提高了性能。
  2. 减少碎片化: 频繁进行内存分配和释放操作可能导致内存碎片化问题。通过惰性释放,Redis可以将多个小块的内存释放合并为一个较大的连续空间,从而减少了内存碎片的产生。
  3. 避免内存拷贝: 惰性释放使得Redis可以在SDS进行扩展或缩小操作时,直接在现有的内存空间上进行修改,而无需进行内存拷贝。这可以节省CPU的时间和资源,提高性能。
  4. 适应动态变化: 惰性释放使得SDS可以根据字符串的动态变化,自适应地调整内存空间的大小。当字符串长度增加时,SDS可以动态扩展内存;而当字符串长度减少时,SDS可以惰性释放多余的空间。这种灵活性和自适应能力使得SDS更适合处理动态变化的字符串数据。

三、这样的设计难道只有优点吗?🤔

尽管SDS在Redis中作为字符串值的数据结构具有许多优点,但也存在一些缺点,包括以下几点:

  1. 内存占用:相比于使用空终止符的C字符串,SDS需要额外的内存空间来存储长度和未使用的字节数。这意味着对于相同的字符串内容,SDS可能占用更多的内存空间。

  2. 内存碎片:当SDS进行频繁的追加和修改操作时,可能会导致内存碎片问题。由于SDS的长度是动态变化的,当字符串缩短时,SDS不会立即归还多余的空间给操作系统,而是保留在SDS内部。这可能会导致内存碎片的增加,造成内存空间的浪费。

  3. 额外的复杂性:相比于简单的C字符串,SDS引入了额外的数据结构和操作逻辑。这增加了一定的复杂性,并可能导致一些开发和维护上的困难。

需要注意的是,这些缺点相对较小,并且在大多数情况下不会对Redis的性能和功能产生显著影响。Redis选择使用SDS作为字符串值的数据结构是为了提供更好的灵活性、性能和功能而做出的权衡。

四、sds是如何扩缩容的?

SDS在扩容和收缩时会采取不同的策略来管理内存。

  1. 扩容(Expansion): 当需要将一个SDS扩展到容纳更长的字符串时,SDS会进行以下步骤:

    • 首先,SDS会检查是否有足够的未使用空间来容纳新的字符串。如果有足够的空间,则直接将新字符串复制到未使用空间,并更新长度和未使用字节数。
    • 如果没有足够的空间,SDS将根据需要预先分配一块更大的内存空间。这个新分配的空间大小通常是当前字符串长度的两倍,或者按照一定的策略进行动态调整。
    • 然后,SDS将原有的字符串内容复制到新分配的空间中,并更新长度和未使用字节数。
    • 最后,SDS释放原有的空间,将新分配的空间作为新的SDS使用。

    扩容操作的时间复杂度是O(N),其中N是字符串的长度。

  2. 收缩(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的常见场景:

  1. 存储键名和键值: 在Redis中,键名和键值通常以SDS的形式存储。SDS提供了动态调整长度的能力,使得Redis能够高效地处理不同长度的键名和键值。

  2. 网络通信: Redis使用SDS作为网络通信的缓冲区,用于接收和发送命令和响应。SDS的动态扩容和缩容特性使得Redis能够适应不同大小的网络数据包,并提供高效的网络通信性能。

  3. 持久化存储: 当Redis执行持久化存储(如RDB快照或AOF日志)时,SDS用于表示和存储数据库中的字符串数据。SDS的灵活性和性能优势使得Redis能够高效地进行数据持久化操作。

  4. 发布与订阅: 在Redis的发布与订阅模式中,SDS用于存储和传递消息内容。由于SDS可以动态调整大小,Redis能够处理不同长度的发布消息,并将其传递给订阅者。

七、redis底层数据结构哪些使用到了SDS?🤔

Redis底层数据结构中有以下几个使用了SDS(Simple Dynamic Strings):

  1. 字符串对象(String Object): Redis中的字符串对象使用SDS来表示键名(key)和键值(value)。SDS提供了动态调整长度的能力,使得Redis能够高效地存储和处理不同长度的字符串数据。

  2. 列表对象(List Object): Redis中的列表对象使用SDS来存储和表示列表中的元素。每个列表元素都被存储为一个SDS,从而实现了对不同类型和长度的元素进行高效的访问和操作。

  3. 哈希对象(Hash Object): Redis中的哈希对象使用SDS来存储和表示哈希表中的键和值。每个键和值都被存储为一个SDS,使得Redis能够处理不同长度的哈希键和哈希值。

  4. 有序集合对象(Sorted Set Object): Redis中的有序集合对象使用SDS来存储和表示有序集合中的成员和分值。成员和分值都被存储为一个SDS,使得Redis能够对不同长度的有序集合元素进行高效的排序和检索。