Redis 没有直接使用 C 语言传统的字符串表示(以空字符串结尾的字符数组),而是构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将 SDS 用作 Redis 的默认字符串表示。

在 Redis 中,C 字符串只会作为字符串字面量用在一些无需对字符串进行修改的地方,比如打印日志:

serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");

当 Redis 需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis 就会适应 SDS 来表示字符串。比如在数据库中,包含字符串值的键值对在底层都是由 SDS 实现的。

还是拿简单的 SET 命令举例,执行以下命令

redis> SET msg "hello world"
ok

那么,Redis 将在数据中创建一个新的键值对,其中:

  • 键值对的键是一个字符串对着,对象的底层实现是一个保存着字符串 "msg" 的 SDS。
  • 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串 "hello world" 的 SDS。

除了用来保存数据库中的字符串值之外, SDS 还被用作缓冲区。AOF 模块中的 AOF 缓冲区,以及客户端状态中的输入缓冲区,都是由 SDS 实现的。

接下来,我们就来详细认识下 SDS。

1 SDS 的定义

在 sds.h 中,我们会看到以下结构:

typedef char *sds;

可以看到,SDS 等同于 char * 类型。这是因为 SDS 需要和传统的 C 字符串保存兼容,因此将其类型设置为 char *。但是要注意的是,SDS 并不等同 char *,它还包括一个 header 结构,共有 5 中类型的 header,源码如下:

struct __attribute__ ((__packed__)) sdshdr5 { // 已弃用
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 长度小于 2^8 的字符串类型
uint8_t len; // SDS 所保存的字符串长度
uint8_t alloc; // SDS 分配的长度
unsigned char flags; // 标记位,占 1 字节,使用低 3 位存储 SDS 的 type,高 5 位不使用
char buf[]; // 存储的真实字符串数据
};
struct __attribute__ ((__packed__)) sdshdr16 { // 长度小于 2^16 的字符串类型
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // 长度小于 2^32 的字符串类型
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 { // 长度小于 2^64 的字符串类型
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};

之所以会有 5 种类型的 header,是为了能让不同长度的字符串使用对应大小的 header,提高内存利用率。

一个 SDS 的完整结构,由内存地址上前后相邻的两部分组成:

  • header:包括字符串的长度(len),最大容量(alloc)和 flags(不包含 sdshdr5)。
  • buf[]:一个字符串数组。这个数组的长度等于最大容量加 1,存储着真正的字符串数据。

图 1-1 展示了一个 SDS 示例:

示例中,各字段说明如下:

  • alloca:SDS 分配的空间大小。图中表示分配的空间大小为 10。
  • len:SDS 保存字符串大小。图中表示保存了 5 个字节的字符串。
  • buf[]:这个数组的长度等于最大容量加 1,存储着真正的字符串数据。图中表示数字的前 5 个字节分别保存了 'H'、'e'、'l'、'l'、'o' 五个字符,而最后一个字节则保存了空字符串 '\0'。

SDS 遵循 C 字符串以空字符结尾的惯例,保存空字符的大小不计算在 SDS 的 len 属性中。此外,添加空字符串到字符串末尾等操作,都是由 SDS 函数(sds.c 文件中的相关函数)自动完成的。

而且,遵循空字符结尾的惯例,还可以直接重用一部分 C 字符串函数库中的函数。

例如,我们可以直接使用 printf() 函数打印 s->buf

printf("%s", s->buf);

这样,我们可以直接使用 C 函数来打印字符串 "Redis",无需为 SDS 编写转码的打印函数。

2 SDS 对比 C 字符串有哪些优势

在 C 语言中,使用长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组的最后一个元素总是空字符 "\0"。

C 语言使用的这种字符串表示方式,并不能满足 Redis 对字符串再安全性、效率及功能方面的要求。因此,Redis 设计出了 SDS,来满足自己的相关需求。接下来,我们从以下几方面来认识 SDS 对比 C 字符串的优势:

  1. 获取字符串长度;
  2. 缓冲区溢出;
  3. 修改字符串时的内存重分配次数;
  4. 二进制安全;

2.1 常数复杂度获取字符串长度

由于 C 字符串并不记录自身的长度信息,所以在 C 语言中,为了获取一个 C 字符串的长度,程序必须遍历整个字符串,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为 O(N)。

这个复杂度对于 Redis 而言,一旦碰上非常长的字符串,使用 STRLEN 命令时,很容易对系统性能造成影响。

和 C 字符串不同的是,因为 SDS 在 len 属性中记录了 SDS 保存的字符串的长度,所以获取一个 SDS 长度的复杂度仅为 O(1)。

而且设置和更新 SDS 长度的工作都是由 SDS 的 API 在执行时自动完成的,所以使用 SDS 无需进行任何手动修改长度的工作。

通过使用 SDS,Redis 将获取字符串长度所需的复杂度从 O(N) 降低到了 O(1),确保了获取字符串长度的工作不会成为 Redis 的性能瓶颈。

2.2 杜绝缓冲区溢出

C 字符串不记录自身长度,不仅使得获取字符串长度的复杂度较高,还容易造成缓冲区溢出(buffer overflow)

C 语言中的 strcat() 函数可以将 src 字符串中的内容拼接到 dest 字符串的末尾:

char *strcat(char *dest, const char *src);

因为 C 字符串不记录自身的长度,所以 strcat 函数执行时,假定用户已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容。而一旦这个假定不成立,就会产生缓冲区溢出。

举个例子,假设程序里有两个在内存中紧邻着的 C 字符串 s1 和 s2,其中 s1 保存了字符串 "redis",s2 保存了字符串 "mysql",存储结构如图 2-1 所示:

如果我们执行下面语句:

strcat(s1, " 666");

将 s1 的内容修改为 "redis 666",但却没有在执行 strcat() 之前为 s1 分配足够的空间,那么在执行 strcat() 之后,s1 的数据将移除到 s2 所在的空间,导致 s2 保存的内容被意外修改,如图 2-2 所示:

与 C 字符串不同的是,SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当 SDS 的 API 需要对 SDS 进行修改时,API 会先检查 SDS 的空间十分满足修改所需的要求,如果不满足的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小,然后再执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出问题。

2.3 减少内存重分配次数

由于 C 字符串的长度 slen 和底层数组的长度 salen 总存在着下述关系:

salen = slen + 1; // 1 是空字符的长度

因此,每次增长或缩短一个 C 字符串,总要对 C 字符串的数组进行一次内存重分配操作:

  • 增长字符串。程序需要通过内存重分配来扩展底层数组的空间的大小,如果漏了这步,就可能会产生缓冲区溢出。
  • 缩短字符串。程序需要通过内存重分配来释放底层数组不再使用的空间,如果漏了这步,就可能会产生内存泄漏。

而内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以内存重分配是一个较为耗时的过程

对于 Redis 而言,一切耗时的操作都要优化。基于此,SDS 对于字符串的增长和缩短操作,通过空间预分配惰性空间释放两种方式来优化。

2.3.1 空间预分配

空间预分配是指:在需要对 SDS 的空间进行扩展时,程序不是仅仅分配所必需的的空间,还会为 SDS 分配额外的未使用空间

关于 SDS 的空间扩展,源码如下:

# sds.c/sdsMakeRoomFor()
...
newlen = (len+addlen); // SDS 最新长度
if (newlen < SDS_MAX_PREALLOC) // 预分配最大值 SDS_MAX_PREALLOC 在 sds.h 中定义,值为 1024*1024
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
...

由源码可以看出,空间扩展分为两种情况:

  • 新长度小于预分配最大值。此时,程序将直接为 SDS 新增最新长度大小的未使用空间。举个栗子,现有一个长度为 10 字节的字符串 s1,当给 s1 追加字符串 "redis",那么,程序将除了分配足够 s1 使用的空间,还会为 s1 再分配最新长度大小的预使用空间。所以,s1 的实际长度就变为: 15 + 15 + 1 = 31 个字节。
  • 新长度大于预分配最大值。此时,由于最新字符串较大,程序不会预分配这么多空间,只会给预分配最大值的空间。举个栗子,现有长度为 3M 的字符串 s2,当给 s1 追加一个 2M 大小的字符串,那么程序除了新增 2M 来存储新增的长度,还会为 s2 再分配 1M(SDS_MAX_PREALLOC)的预使用空间。所以,s2 的实际长度就变为:3M + 2M +1M + 1byte

正是通过预分配的策略,Redis 减少了执行字符串增长操作所需的内存重分配次数,保证了 Redis 不会因字符串增长操作损耗性能。

2.3.2 惰性空间释放

预分配对应字符串的增长操作,而空间释放则对应字符串的缩短操作。

惰性空间释放是指:在对 SDS 进行缩短操作时,程序不立即回收缩短后多出来的字节,等待将来使用

举个栗子,我们使用 sdstrim() 函数,移除下图 SDS 中所有指定的字符:

对上图 SDS,执行:

sdstrim(s, "l"); // 移除 SDS 字符串中所有的 'l'

会将 SDS 修改为图 2-4 所示:

可以看到,执行 sdstrim() 之后的 SDS 并没有释放多出来的 3 字节空间,而是将这 3 字节空间作为未使用空间保留在了 SDS 里面,以待备用。

正是通过惰性空间释放策略,SDS 避免了缩短字符串时所需的内存重分配操作,并为将来可能的增长操作提供了优化。

此外,SDS 也提供了相应的 API,让我们在有需要时,真正的释放 SDS 的未使用空间,避免造成内存浪费。

总结

  1. Redis 只会使用 C 字符串作为字面量,大多数情况下,使用 SDS 作为字符串表示。
  2. SDS 对比 C 字符串,有几大优点:常数复杂度获取字符串长度杜绝缓冲区溢出减少修改字符串时所需的内存重分配次数

跟着大彬读源码 - Redis 7 - 对象编码之简单动态字符串的更多相关文章

  1. 跟着大彬读源码 - Redis 8 - 对象编码之字典

    目录 1 字典的实现 2 插入算法 3 rehash 与 渐进式 rehash 总结 字典,是一种用于保存键值对的抽象数据结构.由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字 ...

  2. 跟着大彬读源码 - Redis 9 - 对象编码之 三种list

    目录 1 ziplist 2 skiplist 3 quicklist 总结 Redis 底层使用了 ziplist.skiplist 和 quicklist 三种 list 结构来实现相关对象.顾名 ...

  3. 跟着大彬读源码 - Redis 10 - 对象编码之整数集合

    [TOC] 整数集合是 Redis 集合键的底层实现之一.当一个集合只包含整数值元素,并且元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现. 1 整数集合的实现 整数集合是 Redis ...

  4. 跟着大彬读源码 - Redis 5 - 对象和数据类型(上)

    相信很多人应该都知道 Redis 有五种数据类型:字符串.列表.哈希.集合和有序集合.但这五种数据类型是什么含义?Redis 的数据又是怎样存储的?今天我们一起来认识下 Redis 这五种数据结构的含 ...

  5. 跟着大彬读源码 - Redis 6 - 对象和数据类型(下)

    继续撸我们的对象和数据类型. 上节我们一起认识了字符串和列表,接下来还有哈希.集合和有序集合. 1 哈希对象 哈希对象的可选编码分别是:ziplist 和 hashtable. 1.1 ziplist ...

  6. 跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?

    一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期. 相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入 ...

  7. 跟着大彬读源码 - Redis 2 - 服务器如何响应客户端请求?(上)

    上次我们通过问题"启动服务器,程序都干了什么?",跟着源码,深入了解了 Redis 服务器的启动过程. 既然启动了 Redis 服务器,那我们就要连上 Redis 服务干些事情.这 ...

  8. 跟着大彬读源码 - Redis 3 - 服务器如何响应客户端请求?(下)

    继续我们上一节的讨论.服务器启动了,客户端也发送命令了.接下来,就要到服务器"表演"的时刻了. 1 服务器处理 服务器读取到命令请求后,会进行一系列的处理. 1.1 读取命令请求 ...

  9. 跟着大彬读源码 - Redis 4 - 服务器的事件驱动有什么含义?(上)

    众所周知,Redis 服务器是一个事件驱动程序.那么事件驱动对于 Redis 而言有什么含义?源码中又是如何实现事件驱动的呢?今天,我们一起来认识下 Redis 服务器的事件驱动. 对于 Redis ...

随机推荐

  1. 【数据结构】31、hashmap=》resize 扩容,不测不知道,一测吓一跳

    来来来,今天就跟hashmap杠到底... 不要叫我杠精了,主要是还是被问到hashmap的时候,我并不能很清晰明了得告知这种数据结构到底是一个什么构造,里面细节并不了解 既然这样,我们就把他解析一波 ...

  2. 简洁的描述SpringMVC工作流程

    1.客户端发送来的请求 经过前端控制器(springDispatcherServlet)调用映射器(HandlerMapping)来找到对应的执行链(HandlerExecutionChain)对象, ...

  3. HBase 学习之路(二)—— HBase系统架构及数据结构

    一.基本概念 一个典型的Hbase Table 表如下: 1.1 Row Key (行键) Row Key是用来检索记录的主键.想要访问HBase Table中的数据,只有以下三种方式: 通过指定的R ...

  4. Linux下docker的安装

    前言: 因为之前在自己的mac上直接使用HomeBrew的包管理安装的,使用brew install docker即可,这种方法简单,但最近想尝试在Linux下安装,费了一些时间,主要是启动docke ...

  5. JAVA复习笔记02

    16.interface中的成员变量默认为public static final类型,方法只能是public(默认为public) 17.内部类访问外部类成员: Outer.this.num; 18. ...

  6. python面试题(-)可变数据类型与不可变数据类型

    python3中有六个标准的数据类型:number(数字型).string(字符串型).list(列表).type(元祖).dictionary(字典).set(集合),其中不可变类型三个:numbe ...

  7. easyui 使用jquery动态添加组件样式问题

    可以使用$.parser.parse();这个方法进行处理: 例如:   $.parser.parse(); 表示对整个页面重新渲染,渲染完就可以看到easyui原来的样式了:   var targe ...

  8. 使用Mysql执行SQL语句基础操作

    SQL:  又叫结构化语言,是一种用来操作RDBMS的数据库语言,在关系型数据库中都支持使用SQL语句,如oracle.mysql等等. 注意: 在关系型数据库中sql语句是通用的,而在非关系型数据库 ...

  9. Windows下必备的开发神器之Cmder使用说明

    诚言,对于开发码字者,Mac和Linux果断要比Windows更贴心;但只要折腾下,Windows下也是有不少利器的.之前就有在Windows下效率必备软件一文中对此做了下记载:其虽没oh-my-zs ...

  10. 小白教程 Java web maven项目Windows下部署阿里云服务器全过程(买服务器,打包项目,连接服务器,配置服务器,部署项目)

    -------------首先:购买服务器---------------- Step1: 在https://www.aliyun.com/?utm_content=se_1043015注册登录阿里云 ...