前提常识

咱们先从百科上摘下Redis的解释:

Redis是一个应用 ANSI C 编写的开源、反对网络、基于内存、分布式、可选持久性的键值对存储数据库。
(不必过多在意ANSI,它只是一个规范,你能够了解为晚期民间版本很多,起初对立了规范,大学课程里包含当初在用的都是标准化后的C语言版本)

没错!Redis的底层是由 C语言 实现的!大学不论是什么业余应该都有这个课,然而不论大家还有没有它的记忆,都不影响咱们接下来的学习哈哈哈~

redis第一步,字符串是根底

回想当初学习Java,第一个学习的数据类型应该是根本类型,但redis里最根底的构造其实是字符串,简直哪里都有它。在mysql定义字段类型时,个别老哥会很中意varchar这个类型,因为万物皆可varchar(不探讨性能的状况下),其实redis就有点这个意思,很多构造的根底都是字符串。

下面提到Redis的底层是由C语言实现的,那岂不是间接拿C语言的字符串(以空字符结尾的字符数组,以下简称C字符串)来用就行?

我先答复了:不行,因为C语言作为晚期的编程语言,C字符串是有些“有余”或者说是须要补充的,尤其是像redis这样对速度要求严苛,字符串可能会被频繁批改的服务,于是redis在C字符串的根底上,本人结构了名为 简略动静字符串(simple dynamic string,SDS)的形象类型,用作redis本人的默认字符串。

(redis也不是就不必C字符串了,只会使用在一些无需对字符串值做批改的中央,例如打印日志时)

C字符串简略介绍,嗯,简略。


首先上图就是一个值为"yikun"的C字符串。
不理解的小伙伴可能不晓得为什么前面多了个'\0',其实这个字符是空字符,也能够了解为C语言的“字符串终止符”。
长度为N的字符串,会用长度为N+1的字符数组来示意,最初多进去的1长度就是专门用来存储空符'\0'的。

而后没了。
C语言的字符串就是这么简略。

不急,咱们持续往后说~

那么C字符串有哪些“有余”呢?

俗话说得好:知己知彼,屡战屡败。
咱们得晓得C语言的“有余”,能力晓得redis为了补救这种状况,在SDS中做了什么措施。

1.C字符串并没有记录本身长度。
2.会依据空字符'\0'来判断字符串是否完结。
3.只能依据空字符'\0'来判断字符串是否完结。

2和3如同差不多,但意思其实是有细微差别的。
(果然中华文化博大精深~)

C字符串因为下面的“有余”会引起什么问题?

1.获取字符串长度复杂度高

因为 有余1 有余3,所以C字符串只能通过遍历字符来获取长度。

咱们这里用Java的for循环(只是用Java举例,理论是C语言实现的)简略阐明下C语言如何获取字符串长度。
可见字符串越长,这个操作就越耗时,复杂度为O(N)。

2.缓冲区溢出 和 内存泄露

因为 有余2,C字符串可能会在批改字符串时呈现问题,因为大家晓得内存是间断的,如果我要在字符串前面拼接新的内容,我首先要通过 内存重调配 来扩大底层数组的空间大小。


比方我要在字符串'yikun'后拼接'lucky',但当初前面紧挨存储的是'happy'字符串,所以我首先要扩大数组能力拼接'lucky',不然就把'happy'给笼罩掉,造成缓存区溢出了。

那如果我要缩短'yikun',改成'yi',倒是不必扩大数组。

但我空的这部分又造成内存透露,所以还是须要进行 内存重调配
内存重调配 是个非常耗时的操作,如果下面的字符串批改频繁产生的话,对于性能的影响就会很大。

3.没方法存储二进制数据

因为 有余3,如果咱们的数据自身就蕴含空字符串'\0',而代码逻辑是大公无私的,会认为是字符串终止的意思;而像图片、音频、视频、压缩文件这样的二进制数据是会蕴含空字符串'\0'的,所以可想而知,数据会呈现问题,可能只能辨认到后面的局部数据,而失落前面的内容。

所以针对下面的问题,redis的SDS的构造是怎么设计的?

废话不多说,咱们间接来看下SDS的构造:

直观来看,SDS多保护了 free 属性和 len 属性。
len 属性用来记录buf数组中已应用字节的数量,同时也等于SDS所保留字符串的长度。

而属性 free 是用来记录buf数组中未应用字节的数量。

SDS是如何依附引入两个属性值len、free来解决具体问题呢?

1.获取字符串长度

当初能够间接拜访 len 属性来获取字符串长度,就好比Java属性的get办法,复杂度由原来的O(N)一下子降到了O(1)。
而且这个属性是SDS在执行操作时主动实现的,咱们无需进行任何保护。

2.空间预调配 和 惰性空间开释

下面咱们说内存重调配操作耗时,所以在须要对SDS进行空间扩大的时候,会调配额定的未应用空间。

上面是额定调配空间数量的公式:
1.如果SDS批改之后,SDS的长度(len属性的值)小于1MB,那么就会调配和len属性同样大的未应用空间,也就是free属性会和len属性雷同,相当于将原数组长度double。
2.如果SDS批改之后,SDS的长度(len属性的值)大于或等于1MB,那么就会调配固定的1MB未应用空间。

而free属性就决定了是不是须要进行额定的内存重调配操作,如果free为7,而你须要拼接的字符串长度只有5,那就不须要进行内存重调配操作,间接存储就能够。

free:太好了,我这空座就是给老弟你留的,快坐快坐~

所以针对拼接操作来说,原本N次 肯定须要N次 的内存重调配操作,当初 最多只须要N次
(比方你第一次拼接后,free就大于后续要拼接的字符串长度之和了,那其实就只有1次内存重调配操作,所以说最多N次)

缩短操作也是同理,删掉N个字符后,我free就减少N,我先不做内存重调配操作,就先给你留着呗,万一你前面又做拼接是不是。

所以总结:拼接时咱们做 空间预调配,缩短时咱们做 惰性空间开释,都是为了 缩小内存重调配操作

(SDS其实也提供了响应的办法,在有须要时,能够真正地开释SDS的未应用空间,所以不必放心未应用空间会造成内存节约。)

3.存储二进制数据

C字符串不能存储二进制数据的起因是只能依据'\0'来判断数据是否完结,不能保障其完整性,但因为SDS的 len 属性,无论你数据里有多少'\0'都没关系,我是依据 len 属性值来判断数据长度的,必然是残缺的,所以SDS能够平安地存储二进制数据。

SDS你都有len了,那为啥还要跟C字符串一样在字符串前面加'\0'?

这个很好答复,因为SDS放弃和C字符串一样的个性,就不必专门编写多余的函数了,在肯定的状况下能够间接用C语言的函数,防止了不必要的重写。

最初的总结

C字符串和SDS之间的区别:

C字符串SDS
获取字符串长度复杂度为O(N)获取字符串长度复杂度为O(1)
批改字符串时须要执行N次内存重调配批改字符串时最多须要执行N次内存重调配
不能保留二进制数据能够保留二进制数据
能够应用C字符串函数库的所有函数能够应用C字符串函数库的一部分函数

写在最初的最初

我是苏易困,大家也能够叫我易困,一名Java开发界的小学生,文章可能不是很优质,但肯定会很用心。

呜呜呜~ 真是边整顿便有播种,本人学习的能源也增长了起来,还真是那句话:你本人明确,能力跟其他人说明确(也不晓得是不是说明确了),而且写的时候也会思考一些取舍,比方有的货色需不需要标注?有的中央是不是过于啰嗦了?是不是写的太入门了一点?可能这些常识在一些人看来都是很根底很入门甚至不必讲的货色。
但怎么说呢,我的初心其实也就是整顿和梳理本人的常识,如果能帮到你,我会很开心;如果你感觉不够程度,我也会虚心接受;人嘛,总要有个学习的过程,我也会始终致力的~ 大家一起加油~