简单动态字符串

Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示, 几乎所有的 Redis 模块中都用了 sds。

本章将对 sds 的实现、性能和功能等方面进行介绍, 并说明 Redis 使用 sds 而不是传统 C 字符串的原因。

sds 的用途

Sds 在 Redis 中的主要作用有以下两个:

  1. 实现字符串对象(StringObject);
  2. 在 Redis 程序内部用作 char* 类型的替代品;

以下两个小节分别对这两种用途进行介绍。

实现字符串对象

Redis 是一个键值对数据库(key-value DB), 数据库的值可以是字符串、集合、列表等多种类型的对象, 而数据库的键则总是字符串对象。

对于那些包含字符串值的字符串对象来说, 每个字符串对象都包含一个 sds 值。

“包含字符串值的字符串对象”,这种说法初听上去可能会有点奇怪, 但是在 Redis 中, 一个字符串对象除了可以保存字符串值之外, 还可以保存 long 类型的值, 所以为了严谨起见, 这里需要强调一下: 当字符串对象保存的是字符串时, 它包含的才是 sds 值, 否则的话, 它就是一个 long 类型的值。

举个例子, 以下命令创建了一个新的数据库键值对, 这个键值对的键和值都是字符串对象, 它们都包含一个 sds 值:

  1. redis> SET book "Mastering C++ in 21 days"
  2. OK
  3.  
  4. redis> GET book
  5. "Mastering C++ in 21 days"

以下命令创建了另一个键值对, 它的键是字符串对象, 而值则是一个集合对象:

  1. redis> SADD nosql "Redis" "MongoDB" "Neo4j"
  2. (integer) 3
  3.  
  4. redis> SMEMBERS nosql
  5. 1) "Neo4j"
  6. 2) "Redis"
  7. 3) "MongoDB"

用 sds 取代 C 默认的 char* 类型

因为 char* 类型的功能单一, 抽象层次低, 并且不能高效地支持一些 Redis 常用的操作(比如追加操作和长度计算操作), 所以在 Redis 程序内部, 绝大部分情况下都会使用 sds 而不是 char* 来表示字符串。

性能问题在稍后介绍 sds 定义的时候就会说到, 因为我们还没有了解过 Redis 的其他功能模块, 所以也没办法详细地举例说那里用到了 sds , 不过在后面的章节中, 我们会经常看到其他模块(几乎每一个)都用到了 sds 类型值。

目前来说, 只要记住这个事实即可: 在 Redis 中, 客户端传入服务器的协议内容、 aof 缓存、 返回给客户端的回复, 等等, 这些重要的内容都是由 sds 类型来保存的。

Redis 中的字符串

在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。

比如说, hello world 在 C 语言中就可以表示为 "hello world\0" 。

这种简单的字符串表示,在大多数情况下都能满足要求,但是,它并不能高效地支持长度计算和追加(append)这两种操作:

  • 每次计算字符串长度(strlen(s))的复杂度为 θ(N)θ(N) 。
  • 对字符串进行 N 次追加,必定需要对字符串进行 N 次内存重分配(realloc)。

在 Redis 内部, 字符串的追加和长度计算很常见, 而 APPEND 和 STRLEN 更是这两种操作,在 Redis 命令中的直接映射, 这两个简单的操作不应该成为性能的瓶颈。

另外, Redis 除了处理 C 字符串之外, 还需要处理单纯的字节数组, 以及服务器协议等内容, 所以为了方便起见, Redis 的字符串表示还应该是二进制安全的: 程序不应对字符串里面保存的数据做任何假设, 数据可以是以 \0 结尾的 C 字符串, 也可以是单纯的字节数组, 或者其他格式的数据。

考虑到这两个原因, Redis 使用 sds 类型替换了 C 语言的默认字符串表示: sds 既可高效地实现追加和长度计算, 同时是二进制安全的。

sds 的实现

在前面的内容中, 我们一直将 sds 作为一种抽象数据结构来说明, 实际上, 它的实现由以下两部分组成:

  1. typedef char *sds;
  2.  
  3. struct sdshdr {
  4.  
  5. // buf 已占用长度
  6. int len;
  7.  
  8. // buf 剩余可用长度
  9. int free;
  10.  
  11. // 实际保存字符串数据的地方
  12. char buf[];
  13. };

其中,类型 sds 是 char * 的别名(alias),而结构 sdshdr 则保存了 len 、 free 和 buf 三个属性。

作为例子,以下是新创建的,同样保存 hello world 字符串的 sdshdr 结构:

  1. struct sdshdr {
  2. len = 11;
  3. free = 0;
  4. buf = "hello world\0"; // buf 的实际长度为 len + 1
  5. };

通过 len 属性, sdshdr 可以实现复杂度为 θ(1)θ(1) 的长度计算操作。

另一方面, 通过对 buf 分配一些额外的空间, 并使用 free 记录未使用空间的大小, sdshdr 可以让执行追加操作所需的内存重分配次数大大减少, 下一节我们就会来详细讨论这一点。

当然, sds 也对操作的正确实现提出了要求 —— 所有处理 sdshdr 的函数,都必须正确地更新 len 和 free 属性,否则就会造成 bug 。

优化追加操作

在前面说到过,利用 sdshdr 结构,除了可以用 θ(1)θ(1) 复杂度获取字符串的长度之外,还可以减少追加(append)操作所需的内存重分配次数,以下就来详细解释这个优化的原理。

为了易于理解,我们用一个 Redis 执行实例作为例子,解释一下,当执行以下代码时, Redis 内部发生了什么:

  1. redis> SET msg "hello world"
  2. OK
  3.  
  4. redis> APPEND msg " again!"
  5. (integer) 18
  6.  
  7. redis> GET msg
  8. "hello world again!"

首先, SET 命令创建并保存 hello world 到一个 sdshdr 中,这个 sdshdr 的值如下:

  1. struct sdshdr {
  2. len = 11;
  3. free = 0;
  4. buf = "hello world\0";
  5. }

当执行 APPEND 命令时,相应的 sdshdr 被更新,字符串 " again!" 会被追加到原来的 "hello world" 之后:

  1. struct sdshdr {
  2. len = 18;
  3. free = 18;
  4. buf = "hello world again!\0 "; // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
  5. }

注意, 当调用 SET 命令创建 sdshdr 时, sdshdr 的 free 属性为 0 , Redis 也没有为 buf 创建额外的空间 —— 而在执行 APPEND 之后, Redis 为 buf 创建了多于所需空间一倍的大小。

在这个例子中, 保存 "hello world again!" 共需要 18 + 1 个字节, 但程序却为我们分配了 18 + 18 + 1 = 37 个字节 —— 这样一来, 如果将来再次对同一个 sdshdr 进行追加操作, 只要追加内容的长度不超过 free 属性的值, 那么就不需要对 buf 进行内存重分配。

比如说, 执行以下命令并不会引起 buf 的内存重分配, 因为新追加的字符串长度小于 18 :

  1. redis> APPEND msg " again!"
  2. (integer) 25

再次执行 APPEND 命令之后, msg 的值所对应的 sdshdr 结构可以表示如下:

  1. struct sdshdr {
  2. len = 25;
  3. free = 11;
  4. buf = "hello world again! again!\0 "; // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
  5. }

sds.c/sdsMakeRoomFor 函数描述了 sdshdr 的这种内存预分配优化策略, 以下是这个函数的伪代码版本:

  1. def sdsMakeRoomFor(sdshdr, required_len):
  2.  
  3. # 预分配空间足够,无须再进行空间分配
  4. if (sdshdr.free >= required_len):
  5. return sdshdr
  6.  
  7. # 计算新字符串的总长度
  8. newlen = sdshdr.len + required_len
  9.  
  10. # 如果新字符串的总长度小于 SDS_MAX_PREALLOC
  11. # 那么为字符串分配 2 倍于所需长度的空间
  12. # 否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
  13. if newlen < SDS_MAX_PREALLOC:
  14. newlen *= 2
  15. else:
  16. newlen += SDS_MAX_PREALLOC
  17.  
  18. # 分配内存
  19. newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)
  20.  
  21. # 更新 free 属性
  22. newsh.free = newlen - sdshdr.len
  23.  
  24. # 返回
  25. return newsh

在目前版本的 Redis 中, SDS_MAX_PREALLOC 的值为 1024 * 1024 , 也就是说, 当大小小于 1MB 的字符串执行追加操作时,sdsMakeRoomFor 就为它们分配多于所需大小一倍的空间; 当字符串的大小大于 1MB , 那么 sdsMakeRoomFor 就为它们额外多分配 1MB 的空间。

这种分配策略会浪费内存吗?

执行过 APPEND 命令的字符串会带有额外的预分配空间, 这些预分配空间不会被释放, 除非该字符串所对应的键被删除, 或者等到关闭 Redis 之后, 再次启动时重新载入的字符串对象将不会有预分配空间。

因为执行 APPEND 命令的字符串键数量通常并不多, 占用内存的体积通常也不大, 所以这一般并不算什么问题。

另一方面, 如果执行 APPEND 操作的键很多, 而字符串的体积又很大的话, 那可能就需要修改 Redis 服务器, 让它定时释放一些字符串键的预分配空间, 从而更有效地使用内存。

sds 模块的 API

sds 模块基于 sds 类型和 sdshdr 结构提供了以下 API :

函数 作用 算法复杂度
sdsnewlen 创建一个指定长度的 sds ,接受一个 C 字符串作为初始化值 O(N)O(N)
sdsempty 创建一个只包含空白字符串 "" 的 sds O(1)O(1)
sdsnew 根据给定 C 字符串,创建一个相应的 sds O(N)O(N)
sdsdup 复制给定 sds O(N)O(N)
sdsfree 释放给定 sds O(N)O(N)
sdsupdatelen 更新给定 sds 所对应 sdshdr 结构的 free 和 len O(N)O(N)
sdsclear 清除给定 sds 的内容,将它初始化为 "" O(1)O(1)
sdsMakeRoomFor 对 sds 所对应 sdshdr 结构的 buf 进行扩展 O(N)O(N)
sdsRemoveFreeSpace 在不改动 buf 的情况下,将 buf 内多余的空间释放出去 O(N)O(N)
sdsAllocSize 计算给定 sds 的 buf 所占用的内存总数 O(1)O(1)
sdsIncrLen 对 sds 的 buf 的右端进行扩展(expand)或修剪(trim) O(1)O(1)
sdsgrowzero 将给定 sds 的 buf 扩展至指定长度,无内容的部分用 \0 来填充 O(N)O(N)
sdscatlen 按给定长度对 sds 进行扩展,并将一个 C 字符串追加到 sds 的末尾 O(N)O(N)
sdscat 将一个 C 字符串追加到 sds 末尾 O(N)O(N)
sdscatsds 将一个 sds 追加到另一个 sds 末尾 O(N)O(N)
sdscpylen 将一个 C 字符串的部分内容复制到另一个 sds 中,需要时对 sds 进行扩展 O(N)O(N)
sdscpy 将一个 C 字符串复制到 sds O(N)O(N)

sds 还有另一部分功能性函数, 比如 sdstolower 、 sdstrim 、 sdscmp , 等等, 基本都是标准 C 字符串库函数的 sds 版本, 这里不一一列举了。

小结

  • Redis 的字符串表示为 sds ,而不是 C 字符串(以 \0 结尾的 char*)。
  • 对比 C 字符串, sds 有以下特性:
    • 可以高效地执行长度计算(strlen);
    • 可以高效地执行追加操作(append);
    • 二进制安全;
  • sds 会为追加操作进行优化:加快追加操作的速度,并降低内存分配的次数,代价是多占用了一些内存,而且这些内存不会被主动释放。

简单动态字符串-redis设计与实现的更多相关文章

  1. 《Redis设计与实现》阅读笔记(二)--简单动态字符串

    简单动态字符串 Redis只在一些无需对字符串进行修改的地方使用C字符串,大部分时候使用简单动态字符串(simple dynamic string, SDS),字符串的抽象类型.二进制安全,可以存放任 ...

  2. 【Redis】简单动态字符串SDS

    C语言字符串 char *str = "redis"; // 可以不显式的添加\0,由编译器添加 char *str = "redis\0"; // 也可以添加 ...

  3. 《redis 5设计与源码分析》:第二章 简单动态字符串

    介绍 简单动态字符串(Simple Dynamic Strings, SDS)是Redis的基本数据结构之一,用于存储字符串和整型数据.它的特点是:方便扩容.二进制安全. 二进制安全 在C语言中,用& ...

  4. Redis设计与实现读书笔记——简单动态字符串

    前言 项目里用到了redis数据结构,不想只是简单的调用api,这里对我的读书笔记做一下记录.原文地址: http://www.redisbook.com/en/latest/internal-dat ...

  5. 1.redis设计与实现--简单动态字符串

    1.redis没有使用c语言的字符串表示,而是使用更加适合自己的SDS(simple dynamic string),简单动态字符串,结构如下: 2.sys与c字符串的对比: 3.总结: redis采 ...

  6. 小白的Redis学习(一)-SDS简单动态字符串

    本文为读<Redis设计与实现>的记录.该书以Redis2.9讲解Redis相关内容.请注意版本差异. Redis使用C语言实现,他对C语言中的char类型数据进行封装,构建了一种简单动态 ...

  7. 图解Redis之数据结构篇——简单动态字符串SDS

    图解Redis之数据结构篇--简单动态字符串SDS 前言     相信用过Redis的人都知道,Redis提供了一个逻辑上的对象系统构建了一个键值对数据库以供客户端用户使用.这个对象系统包括字符串对象 ...

  8. Redis中的简单动态字符串

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

  9. redis 笔记01 简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表

    文中内容摘自<redis设计与实现> 简单动态字符串 1. Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS(Simple Dynamic String,简单动态 ...

随机推荐

  1. bootstrap环境搭建

    Bootstrap 是stwitter公司的两名前端设计师设计的基于html css javascript的超强的前端框架. Bootstrap 是一移动设备为优先,pc机,平板,手机皆适用的框架. ...

  2. 安装jQuery

    description jQuery,顾名思义,也就是JavaScript和Query(查询),即辅助JavaScript开发的库.jQuery是一个快速.简洁的JavaScript框架,是继Prot ...

  3. py-2 python介绍与安装

    一.python介绍 1.python背景 python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,Guido开始写能够解释Python语言语法的解释器.Py ...

  4. jenkins中的流水线( pipeline)的理解(未完)

    目录 一.理论概述 Jenkins流水线的发展历程 什么是Jenkins流水线 一.理论概述 pipeline是流水线的英文释义,文档中统一称为流水线 Jenkins流水线的发展历程 ​ 在Jenki ...

  5. 助教培训第四次作业——熟练掌握GitHub及Git的使用方法

    助教培训第四次作业——熟练掌握GitHub及Git的使用方法 1.Git 命令的理解和使用 常用的Git命令可以查看官方文档,官方文档的网址:https://git-scm.com/docs .虽然是 ...

  6. pymysql的增删改查、索引

    1.pymysql增删改 一定要有commit() import pymysql username = input("请输入用户名:") pwd = input("请输入 ...

  7. Nginx入门(四)——反向代理

    server { listen 8020; server_name localhost; location / { root html; index index.html index.htm; pro ...

  8. Mybatis之动态SQL&OGNL表达式

    1.接口 public interface MemberMapperDynamicSQL { public List<Members> selectMembersByIf(Members ...

  9. msaa mrt load store action unity

    unity buildin renderpipeline 和lightweight rp 对于开了msaa的rt 的load store action设置失效 buildin的时候set render ...

  10. Mongo mongoexport/mongoimport介绍

    一.Mongoexport导出数据 1,导出json数据 mongoexport -d db -c collection -o save-file.dat 2,导出CSV数据 mongoexport ...