简单动态字符串

Redis中的字符串并不是传统的C语言字符串(即字符数组,以下简称C字符串),而是自己构建了一种简单动态字符串(simple dynamic string,SDS),并将SDS作为Redis的默认字符串表示。在Redis中,C字符串一般只用在无需对字符串值进行修改的地方,比如Redis的启动时的日志。Redis需要的字符串是一个可修改字符长度的字符串,就会用到SDS来表示一个字符串。比如下面这个例子:

127.0.0.1:6379> set msg "hello world"
OK

  

这是一条很简单的命令,将"hello world"这个字符串与msg这个键建立映射关系。而"hello world"在Redis中的表示,就是一个SDS。说了那么久的SDS,那这个SDS到底长什么样呢?我们来看看

sds.h

struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中尚未使用的字节数量
int free;
//字节数组,用于保存字符串
char buf[];
};

  

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

  • free属性的值为0,表示这个SDS没有任何剩余的可使用字节数
  • len为5,表示这个SDS保存了一个长度为5的字符串
  • buf属性是一个char类型的数组,数组的前五个字节分别保存了'R'、'e'、'd'、'i'、's'五个字符,而最后一个字节则保存空字符'\0',代表字符串结束

图1-1

看到这里,可能还有人不明白使用SDS的好处。没关系,我们接下来再看看另一个示例。我们看图1-2,这个SDS和图1-1的SDS不一样,虽然都保存字符串“Redis”。但图1-2中SDS的buf字符数组长度以及free所保存的值都与图1-1的SDS不一样

图1-2

我们都知道,Redis作为一款非关系型的内存数据库,他的值很容易变动。同时我们也知道,C语言中字符数组的长度是无法变动的。如果Redis中使用的字符串是C字符串,而不是SDS,当我们变动一个键所对应的字符串,如果新字符串的长度小于等于原先字符串的长度,那么我们只要替换字符数组上的内容,再把代表字符串结尾的提前(如果新旧字符串长度相等,则空字符串还留在原先的位置)。但如果新字符串的长度大于原先旧字符串的长度,那么很不幸,我们只能重新申请一个能容纳新字符串长度的数组,用于保存新字符串,这对Redis无疑是不利的

于是,Redis在为一个字符串创建一个SDS对象时,通常会申请比字符串长度更长的字节数组(buf),Redis将字符串保存进这个数组,同时在len这个变量保存字符串的长度,再用free这个变量保存buf尚未使用的字节数量。当客户端要求变动一个键所对应的字符串时,如果buf的长度大于新字符串的长度,那么就无需再声明一个新的数组来容纳新字符串了

我们再来看sdshdr这个结构体,这里面有free、len和buf这三个域。那么这个len会不会有些多余?因为free已经记录尚未使用的字节数量了,同时len我们也可以通过:strlen(buf)的方式来计算字符串长度,那么这个len真的是多余的吗?其实不是,如果有使用过Java、Python等这些高级语言的人都有经验,在这些高级语言中,我们可以轻而易举的调用一个函数来获取字符串的长度,而这些高级语言的字符串内部实现,同样也记录了字符串的长度,假设我们要计算一个字符串长度,每次都要调用strlen(buf),这个操作的时间复杂度为O(N),图1-3展示了C程序中计算字符串长度的过程

图1-3

从图1-3我们可以知道,当通过strlen(buf)计算一个C字符串的长度,游标会遍历到空字符处才停止,而大家在编程中,多多少少在一些业务场景会重复用到某个字符串长度。于是,SDS中,len域的作用就在于此,我们只需要计算一次字符串的长度,当需要用到时直接从len中取,这时的时间复杂度为O(1)

除了获取字符串长度的时间复杂度高之外,C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。C语言中的strcat函数可以拼接两个字符串,具体定义如下:

#include <string.h>
char *strcat(char *dest, const char *src);

  

strcat函数可以将src字符串中的内容拼接到dest字符串之后,但因为C本身不记录字符串长度,默认认为dest已经分配了足够的内存空间。举个例子,假设程序中有紧邻的两个字符串S1和S2,其中S1保存了字符串“Redis”,而S2保存了字符串“MongoDB”,如图1-4所示

图1-4   在内存中紧邻的两个C字符串

如果程序员没有注意S1的长度,直接执行strcat(S1, "Cluster"),那么势必会覆盖到S2的内存,换言之S2所对应的字符数组的内容会被修改,如图1-5所示

如图1-5   S1的内容溢出到S2所在的位置上

与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性,当SDS API需要对SDS进行修改时,API会检查SDS的空间是否满足修改的要求,如果不满足的话,API会自动将SDS的空间扩展至执行所需的大小,然后才执行修改操作

SDS的API里面有一个用于执行拼接操作的sdscat函数,它可以将一个C字符串拼接到给定的SDS所保存的字符串后面,但是在执行拼接操作之前,sdscat会检查给定的SDS的空间是否足够,如果不够的话,sdscat就会扩展SDS的空间,然后才执行拼接操作

例如,我们执行sdscat(s, " Cluster"),其中SDS值s如图1-6所示,那么sdscat检查后发现,目前s的空间并不足以拼接" Cluster",之后,sdscat就会扩展s空间,然后执行拼接" Cluster"操作,拼接完之后的SDS如图1-7所示

图1-6   sdscat执行之前的SDS

图1-7   sdscat执行之后的SDS

Redis作为内存数据库,经常被用于速度要求严苛,数据被频繁修改的场合,如果每次修改字符串长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁发生的话,可能还会对性能造成影响。为了避免C字符串的缺陷,SDS通过未使用空间解除字符串长度和底层数组长度之间的关联。在SDS中,buf数组的长度不一定是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略

空间预分配

空间预分配用于优化SDS的字符串增长操作,我们都知道当SDS的API对一个SDS进行修改时,除了分配给本身所需的字节空间,还会再额外分配一些备用空间。那么,这个备用空间是多大呢?备用空间由以下公式决定:

  • 如果对SDS进行修改后,SDS的长度(即len属性的值)小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS的free属性的值将于len属性的值相同。比如经过修改之后,SDS的len将变为13个字节,那么程序也会分配13个字节的备用空间,外加一个字节用于存储空字符串标识字符串结束,所以SDS的buf数组实际长度为13+13+1=27字节
  • 如果对SDS进行修改之后,SDS的长度大于等于1MB,那么程序会多分配1MB的未使用时间。比如经修改后,SDS的len为30MB,那么程序会多分配1MB的未使用空间,SDS的buf数组的实际长度为30MB+10MB+1byte

惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作,当SDS的API需要缩短SDS保存的字符串时,程序其实并不立即使用内存重分配回收缩短后多出来的字节,而是将修改后尚未被使用的字节数存放在free中,用于以后使用

二进制安全

C字符串的必须必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符串将被误认到达字符串末尾,这限制了C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据

虽然数据库一般用于保存文本数据,但使用数据库来保存二进制数据的场景也不少见。因此,为了确保Redis可以适用于各种不同的场景,SDS的API都是二进制安全的,所有SDS API都会以处理二进制的方式来处理存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或假设,数据在写入时是什么样的,它被读取时就是什么样的

这也是我们将SDS的buf属性称为字节数组的原因,因为Redis不是用这个数组来保存字符,而是用它来保存一系列的二进制数据。且在SDS中,并非以一个空字符来判断是否到达字符数组的末尾,而是通过len属性的值,如图1-8

图1-8   保存了特殊数据格式的SDS

兼容部分C字符串函数

虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节的空间来容纳空字符,这是为了让保存了文本数据的SDS可以重用一部分<string.h>库定义的函数。比如:strcasecmp是用于忽略大小写比较两个字符串的函数,使用它来对比SDS保存的字符串和另一个C字符串

strcasecmp(sds->buf, "hello world");

  

又或者,我们可以将sds中的buf所保存的内容,追加到另一个C字符串上。这样,就无需再去编写另外一套与<string.h>中功能类似的函数了

strcat(c_string, sds->buf);

  

现在,我们对C字符串和SDS的区别进行总结

表1-1   C字符串和SDS之间的区别
C字符串 SDS
获取字符串长度的时间复杂度为O(N) 获取字符串长度的时间复杂度为O(1)
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据 可以保存文本或二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

SDS API

表1-2   SDS的主要操作API
函数 作用 时间复杂度
 sdsnew 创建一个包含给定C字符串的SDS  O(N) ,N 为给定C字符串的长度 
sdsempty  创建一个不包含任何内容的空SDS   O(1)
sdsfree  释放给定的SDS  O(1) 
sdslen  返回SDS的已使用空间字节数  这个值可以通过读取SDS的len属性来直接获得,复杂度为O(1) 
sdsavail  返回SDS的未使用空间字节数  这个值可以通过读取SDS的free属性来直接获得,复杂度为 O(1) 
sdsdup 创建一个给定SDS的副本(copy)  O(N),N为给定SDS的长度 
sdsclear 清空SDS保存的字符串内容   因为惰性空间释放策略,复杂度为O(1)
sdscat   将给定C字符串拼接到SDS字符串的末尾  O(N),N为被拼接C字符串的长度
sdscatsds 将给定SDS字符串拼接到另一个SDS字符串的末尾  O(N),N为被拼接SDS字符串的长度 
sdscpy  将给定的C字符串复制到SDS里面,覆盖SDS原有的字符串  O(N),N为被复制C字符串的长度 
sdsgrowzero  用空字符将SDS扩展至给定长度  O(N),N为扩展新增的字节数 
sdsrange  保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除  O(N),N为被保留数据的字节数 
sdstrim  接受一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符   O(M*N),M为SDS的长度,N为给定C字符串的长度
sdscmp  对比两个SDS字符串是否相同  O(N),N为两个SDS中较短的那个SDS的长度 

Redis实现之字符串的更多相关文章

  1. 峰Redis学习(3)Redis 数据结构(字符串、哈希)

    第一节:Redis 数据类型介绍 五种数据类型: 字符串(String) 字符串列表(list) 有序字符串集合(sorted set) 哈希(hash) 字符串集合(set)   第二节:Redis ...

  2. redis 基本数据类型-字符串(String)

    不瘦原来对redis也是有个大概的了解(就你知道的多), 但是最近和大神聊天的过程中才明白自己知道的简直就是鸡毛蒜皮(让你得瑟),所以不瘦打算从头在捋一遍,顺便把过程也记录下来,如果能给大家在学习re ...

  3. PHP操作redis之String(字符串)、List(列表)(一)

    Redis 简介 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库. Redis 与其他 key – value 缓存产品有以下三个特点: Redis支持数据的持久 ...

  4. Redis数据类型(字符串)

    Redis存放的字符串为二进制是安全的.字符串长度支持到512M. incr 递增数字INCR key 当存储的字符串是整数时,redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递 ...

  5. Redis 数据结构之字符串的那些骚操作

    Redis 字符串底层用的是 sds 结构,该结构同 c 语言的字符串相比,其优点是可以节省内存分配的次数,还可以... 这样写是不是读起来很无聊?这些都是别人咀嚼过后,经过一轮两轮三轮的再次咀嚼,吐 ...

  6. Redis—简单动态字符串(SDS)

    目录 Redis-简单动态字符串(SDS) SDS的定义 SDS与C字符串的区别 1. 常数复杂度获取字符串长度: 2. 杜绝缓冲区溢出: 3. 减少修改字符串时带来的内存重分配次数 4. 二进制安全 ...

  7. Redis数据结构详解(1)-redis中的字符串(SDS)

    前提知识 我们先从百科上摘下Redis的解释: Redis是一个使用ANSI C编写的开源.支持网络.基于内存.分布式.可选持久性的键值对存储数据库. (不用过多在意ANSI,它只是一个标准,你可以理 ...

  8. Redis学习——SDS字符串源码分析

    0. 前言 这里对Redis底层字符串的实现分析,但是看完其实现还没有完整的一个概念,即不太清楚作者为什么要这样子设计,只能窥知一点,需要看完redis如何使用再回头来体会,有不足之处还望告知. 涉及 ...

  9. Redis自定义动态字符串(sds)模块(一)

    Redis开发者在开发过程中没有使用系统的原始字符串,而是使用了自定义的sds字符串,这个模块的编写是在文件:sds.h和sds.c文件中.Redis自定义的这个字符串好像也不是很复杂,远不像ngin ...

随机推荐

  1. java 基础 03 运算符 分支结构 循环结构

    今天内容: (1)运算符 (2)分支结构 (3)循环结构 1运算符 1.1赋值运算符 (1)简单赋值 = 表示赋值运算符,用于将=右边的数据赋值给=左边的变量来覆盖原来的数值. 笔试题: ia == ...

  2. 第2章 TCP-IP的工作方式

    第2章 TCP-IP的工作方式 TCP/IP协议系统 为了实现TCP的功能,TCP/IP的创建者使用了模块化的设计.TCP/IP协议系统被分为不同的组件,每个组件分别负责通信过程的一个步骤.这种模块化 ...

  3. iQuery stop()

    jQuery stop() 方法 jQuery stop() 方法用于停止动画或效果,在它们完成之前. stop() 方法适用于所有 jQuery 效果函数,包括滑动.淡入淡出和自定义动画. 语法 $ ...

  4. [转]ubuntu16.04安装teamviewer12依赖包解决

    安装teamviewer下载地址:http://www.teamviewer.com/en/download/linux/ 下载的是:teamviewer_12.0.76279_i386.deb   ...

  5. silverlight数据绑定模式TwoWay,OneWay,OneTime的研究

    asp.net开发中,数据绑定是一个很简单的概念,控件与数据绑定后,控件可以自动把数据按一定的形式显示出来.(当然控件上的值改变后,可以通过提交页面表单,同时后台服务端代码接收新值更新数据) silv ...

  6. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  7. sublime完美编码主题

    Theme – Soda 使用Ctrl+Shift+P快捷键或者进入菜单:Preferences(首选项) - Package Control(插件控制),调出命令输入框,输入Install Pack ...

  8. 详细步骤教你安装yii高级应用程序和配置composer环境

    现在开始工作,应公司的要求,要开始接触yii了,作为一个没有碰过yii的小白,首先一个问题就是怎么去安装高级程序应用,过程不麻烦,但是也需要细心和耐心,百度资料里面的教程都不太全,漏这漏那的,所以在这 ...

  9. 无法通过CTRL+空格及SHIFT+CTRL调出输入法的解决方案

    打开任务管理器: 运行:CTFMON.EXE

  10. V2EX 神回复 #1

    "抠图"用英文怎么说 今天突然被"抠图"这个单词给难住了," image segmentation "," image cut & ...