1、简单动态字符串

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

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

redisLog(REDIS_WAINING,"Rdeis is now ready to exit,bye bye...")

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

1.1,SDS的定义

每个sds.h/sdshdr结构表示一个SDS值:

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

  • free 属性的值为0,表示这个SDS没有分配任何可使用的空间
  • len 属性的值为5,表示这个SDS保存了一个五字节长的字符串
  • buf 属性是一个char类型的数组,数组的前五个字符分别保存了’r’,‘e’,‘d’,‘i’,'s’五个字符,而最后一个字节则保存了空字符。

1.2、SDS与C字符串的区别

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

C语言使用这种简单的字符串表示并不能满足redis对字符串在安全性,效率,以及功能方面的要求,接下来将详细对比C字符串以及SDS之间的区别,并说明SDS比C字符串更适合redis的原因。

1.2.1、常数复杂度获取字符串长度

因为C字符串并不记录自身的长度,所以获取C字符串长度的时候就要遍历这个字符数组,知道遇到空字符为止,这个操作的时间复杂度为O(N)。

SDS在len属性中记录了了SDS本身的长度,所以获取一个SDS长度的时间复杂度仅为O(1)。

1.2.2、杜绝缓冲区溢出

C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出,比如,<string.h>/strcat函数可以将src字符串中的内容拼接到dest字符串的末尾:

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

因为C语言是不记录自身的长度的,所以strcat假定用户在执行这个函数的时候,已经为dest分配好了足够的空间可以容纳src的大小,则不会出现问题,可是如果执行这个函数的时候没有为dest分配足够的空间,那么就会出现缓冲区溢出的问题。

举个栗子:假如程序李有两个在内存中紧挨着的字符串s1和s2,s1保存了字符串"Redis",s2保存了字符串"MongoDB",如下图所示

如果另一个程序员通过执行strcat(s1," cluster");将s1的内容修改为"Redis cluster",但是s1没有足够的空间去拼接,那么在执行了strcat函数之后,s1的数据将溢出到s2所在空间中,导致s2字符串被修改,如下图所示

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

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

例如:我们执行sdscat(s," Cluster");其中SDS值s如图1-4所示,那么sdscat将在执行前检查s的长度是否足够进行拼接,发现SDS的空间不足以拼接” Cluster“时,sdscat会先扩展s的空间,然后再执行拼接” Cluster“的操作,拼接操作完成后的SDS如图1-5所示

注意:图1-5所示的SDS,sdscat不仅对这个SDS进行了拼接操作,它还为SDS分配了13个字节的未使用空间,这是SDS的空间分配策略,接下来会进行讲解

1.1.3、减少字符带来的内存重分配次数

因为C字符串并不记录自身的长度,所以对于一个包含了N个字符的C字符串来说,这个C字符串的底层实现总是一个N+1个字符串长度的数组(额外一个字符空间用于保存空字符串)。因为C字符串的长度和底层数组的长度之间存在这种关系,所以每次增加或者缩短一个字符串就会进行一次内存重分配操作:

  • 如果程序执行的是增长字符串的操作,比如拼接操作,那么在执行这个操作之前,程序要先通过内存重分配来扩展底层数组的空间大小,如果忘了这一操作就会发生缓冲区溢出。
  • 如果程序执行的是缩短字符串的操作,比如截断操作,,那么在执行这个操作之后,程序需要通过内存分配策略来释放字符串不再使用的那部分空间,如果忘了这一操作,就会发生内存泄漏。

内存分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作:

  • 在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的
  • 但是redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分,如果频繁发生的话可能还会对性能造成影响。

为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度的之间的关联:在SDS中,buf数组的长度不一定就是字符串加1,数组里面可以包含未使用的字节,而这些字节的数量就是SDS的free属性记录。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

1.空间预分配

空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改时,并且需要对SDS进行空间扩展时,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配分配额外的未使用空间

  • 如果对SDS进行修改之后,SDS的长度(也就是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同,举个栗子,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外一个字节用于保存空字符)。
  • 如果对SDS进行修改后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间,举个栗子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB + 1MB + 1byte。

在扩展SDS空间之前,SDS的API会先检查未使用的空间是否足够,如果足够的话就直接分配,不用进行内存重分配。

2、惰性空间释放

惰性空间释放时优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

同时,SDS也提供了相应的API,让我们可以在有需要的时候,真正的释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。

1.1.4、二进制安全

C字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里不能包含空字符,否则最先呗程序读到的空字符将被认为时字符串的结尾,这些限制导致C字符串只能保存文本数据,而不能保存像图片,音频,压缩文件这样的二进制数据。

举个栗子:如下图,假如一种特殊的数据格式,它包含了空字符,那它就不能使用C字符串来保存,因为C字符串所用的函数只会识别出其中"Redis",而忽略之后的"Cluster".

为了确保Redis可以适用于各种不同的的使用场景,SDS的API都是二进制安全的,所有的SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何的限制,修改或者假设,数据在写入时是什么样,它在读取时就是什么样,这也是我们将SDS的buf属性成为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。

举个栗子:使用SDS来保存之前提到的特殊数据格式就没有任何问题,因为SDS使用len属性的值而不是通过空字符串来判断字符串是否结束。如下图所示

1.1.5兼容部分C字符串函数

虽然SDS的API都是二进制安全的,但是他们一样遵循C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,目的就是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。

举个栗子:假如我们有一个保存文本数据的SDS的值sds,那么我们就可以重用<string.h>/strcasecmp函数,使用它来对比SDS保存的字符串和另一个C字符串:strcasecmp(sds->buf,“hello woeld”);

同样的,我们还可以将一个保存文本数据的SDS作为strcat的第二个参数,将SDS保存的字符串追加到一个C字符串后面:strcat(c_string,sds->buf);

通过遵循C字符串以空字符串结尾的惯例,SDS可以在需要的时候重用<string.h>函数库,从而避免了不必要的代码重复。

1.1.6、总结

下表对C字符串和SDS之间的区别进行了总结。

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

1.3、SDS API

下表列出了SDS的主要操作API

函数 作用 时间复杂度
sdsnew 创建一个包含给定C字符串的SDS O(N),N为给定C字符串的长度
sdsempty 创建一个不包含任何内容的空SDS O(1)
sdsfree 释放给定的SDS O(N),N为被释放的SDS的长度
sdslen 返回已使用的空间字节数 这个值可以直接通过读取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给定区间内的数据,不在区间内的数据hi被覆盖或清除 O(N),N为被保留数据的字节数
sdstrim 接受一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符 O(M*N),M为SDS的长度,N为给定C字符串的长度
sdscmp 对比两个SDS字符串是否相同 O(N),N为两个SDS中较短的那个SDS的长度

该文章是看了黄健宏老师所著的《Redis的设计与实现》后所做的笔记,若有侵权,请联系删除

Redis数据结构——简单动态字符串相关推荐

  1. Redis数据结构——简单动态字符串-SDS

    1.SDS简介: redis没有使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作redis的默认字符串表示. 除了用来保存 ...

  2. redis学习 -- 简单动态字符串

    Redis没有使用C语言字符串的形式,通过'\0'作为结尾,而是使用了简单动态字符串(simple dynamic string). 当Redis使用的字符串不需要修改字符串的内容的时候,可以使用C语 ...

  3. Redis之简单动态字符串sds

    转载:https://segmentfault.com/a/1190000012262739 redis在处理字符串的时候没有直接使用以'\0'结尾的C语言字符串,而是封装了一下C语言字符串并命名为s ...

  4. Redis内部数据结构详解之简单动态字符串(sds)

    本文所引用的源码全部来自Redis2.8.2版本. Redis中简单动态字符串sds数据结构与API相关文件是:sds.h, sds.c. 转载请注明,本文出自:http://blog.csdn.ne ...

  5. Redis数据结构之简单动态字符串SDS

    Redis的底层数据结构非常多,其中包括SDS.ZipList.SkipList.LinkedList.HashTable.Intset等.如果你对Redis的理解还只停留在get.set的水平的话, ...

  6. 【Redis】Redis数据结构与对象(一)简单动态字符串(SDS)

    目录 1. C字符串与SDS 2. SDS的定义 3. SDS与C字符串的区别 3.1 常数复杂度获取字符串长度 3.2 杜绝缓冲区溢出 3.3 减少修改字符串时带来的内存重分配次数 3.3.1 空间 ...

  7. Redis源码初探(1)简单动态字符串SDS

    前言 现在面试可太卷了,Redis基本是必问的知识点,为了在秋招中卷过其他人(虽然我未必参加秋招),本菜鸡决定从源码层面再次学习Redis,不过鉴于本菜鸡水平有限,且没有c语言基础,本文不会对源码过于 ...

  8. 【Redis系列2】Redis字符串对象之SDS(简单动态字符串)实现原理分析

    Redis字符串对象之SDS实现原理分析 前言 字符串对象 为什么Redis的字符串对象是二进制安全的 SDS空间分配策略 空间预分配 惰性空间释放 SDS和C语言字符串区别 SDS的底层存储对象 d ...

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

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

最新文章

  1. 5亿整数的大文件,怎么排?
  2. 移动端自动化==Appium定位方式总结
  3. Nginx解决PATH_INFO新解决办法
  4. [PYTHON] 核心编程笔记(18.多线程编程)
  5. 网络推广专员浅析如何在日常网络推广过程中增加网站转化率?
  6. 全球最快学术超算Frontera,也用英特尔至强可扩展处理器
  7. wxWidgets:wxTaskBarIcon类用法
  8. Java5泛型的用法,T.class的获取和为擦拭法站台
  9. Native Vlan(本征vlan)
  10. LaTeX中添加\usepackage{subfigure}一直报错的解决办法,亲测
  11. Ajax同步链接在IE 与FireFox的使用差别 open(GET,url,false)
  12. (原創) 何谓可读性高的程序? (C/C++)
  13. windows系统下运行bat脚本实现后台运行及停止jar文件
  14. IE8的样式兼容性适应方法【转】
  15. 开源的胜利:意大利法院判定开源协议条款可强制执行
  16. 三边测距定位算法详解
  17. 服务器防火墙firewalld,指定端口开放
  18. 多个数据表格合并计算计算机,多个excel表格某个数据合计-Excel怎么才能快速将几个表的某一列数据求和在一个......
  19. 阿里云建站保证百度收录3000+网站模板
  20. 手把手教你写专利申请书/如何申请专利

热门文章

  1. 建议找黑人的都登记一下,避免艾滋传染。
  2. 使用 jQuery Jcrop 实现图像裁剪
  3. 八皇后问题详解(四种解法)
  4. python中的空语句_Python系列之常用语句
  5. 小米手机html在哪,小米手机设置的访问上网权限在哪
  6. C语言输入单个字符避免回车的方法
  7. LeetCode P70 爬楼梯 (动规、滚动、递归 三大解法)
  8. 织梦dedecms文章发布日期时间调用标签大全
  9. ubuntu 15.10 64bit 下 steam无法启动
  10. 英雄无敌6 玩了下人类战役的一些心得和期望