当前位置: 首页 > 科技观察

Redis中的“SOS”,不,是SDS

时间:2023-03-12 08:03:16 科技观察

大家好,我是鸭血粉(大家会亲切的叫我“粉”),一个喜欢吃鸭血粉的程序员。总结了线上OOM的情况,相信大家可以从中学到一些东西,作为一个有抱负的程序员,阿粉,我的理解是光吃老本是不够的,所以一直在学习,大家今天一起来我来学习Redis的SDS(不是SOS~)。01、SDS数据结构Redis的底层是基于C语言开发的,但它并没有使用C语言传统的字符串表示,而是自定义了一种数据结构SDS(SampleDynamicString,简单动态字符串)来表示字符串。传统的C语言字符串是以空字符(\0)结尾的字符数组。SDS的数据结构稍微复杂一点。整个结构由三部分组成,是Redis的基础。(阿芬猜测,这就是传说中的青从岚而来,比岚更胜一筹的地方)。1.1.数据结构SDS的数据结构在源码sds.h/sdshdrstructure中定义,包括free,len,buf[]三部分,含义如下buf[]:字节数组,用来存放实际的细绳;len:记录buf数组使用的字节数,等于存储在SDS中的字符串长度;free:记录buf数组中未使用的字节数。注意上图中的SDS代表一个包含'RED'的字符串,使用的长度为3,未使用的长度为2(这里用空格表示未使用),'\0'代表一个字符串的结尾不计入SDS的len中,由SDS底层函数自动添加,对用户透明。这里在最后统一使用空字符(\0),以复用C语言的相关函数。我相信每个人都能很好地理解这一点。毕竟有祖宗可以依靠,没必要这么辛苦的靠自己~。02、为什么要用SDS2.1,SDS和C语言字符串的区别?我们只能知道这种认识的原因。2.1.1、O(1)获取字符串的长度如果要获取传统C语言字符串的长度,需要遍历整个字符串,直到遇到'\0'字符,不知道整个字符串的长度是多少,运算复杂度是O(n)。但是在SDS中,由于我们记录的是字符串的长度,所以在获取字符串长度的时候可以直接获取,整个操作复杂度为O(1)。如上例,我们不用遍历就可以直接得到字符串的长度为3。另外Redis中字符串Key的底层实现使用的是SDS,所以这个特性保证了我们不需要去计算Key的长度。出现的任何瓶颈都不会影响系统的性能。2.1.2.动态扩展由于未使用的空间被记录在SDS中,如果已有的字符串被修改或赋值,SDS的底层函数会自动检测剩余空间是否可以满足修改。如果可用空间足够则直接修改;如果空闲空间不够,它会先进行动态扩展,以满足可以满足的空间大小,然后再执行修改动作。整个扩展动作由SDS底层功能自动完成,对用户无动于衷。对于传统的C语言字符串,如果修改前忘记手动扩容,字符串后面的数据会被覆盖。这里不得不说一句,为了方便广大程序员,其他骨灰级程序员(好吧,好像看到了未来的A粉)也操碎了心~2.1.3,减少传统C中的内存分配次数语言字符串,我们每次修改字符串都会涉及到字符串内存的重新分配,无论是增加还是减少字符串的长度。在这种情况下,如果我们多次调整字符串的长度,就会造成多次内存重新分配。在SDS中,当我们初始化一个SDS时,我们会根据实际的buf[]字符串的长度预先分配空间,并标记为空闲。这种方法称为空间预分配,可以在很大程度上减少因增加字符串长度而引起的内存重新分配。free的空间分配策略是根据buf[]的大小来决定的,如果buf[]的大小小于1MB,那么len的大小就和free一样大;如果buf[]的大小大于1MB,则free固定为1MB。上面说的是SDS字符串的长度增加。另外,如果SDS字符串的长度减少,SDS会将减少的长度存储在free中,而不是直接回收。这样方便下次再次使用,减少内存重分配。.这种策略称为惰性空间释放。同样以上两个操作对用户来说是完全不敏感的。阿芬觉得这个办法很合理。你怎么看“元芳”?2.1.4.二进制安全我们都知道Redis可以存储各种类型的数据,不仅可以是字符串,还可以存储图片、视频等二进制数据流。这是因为Redis不依赖'\0'空字符作为结束字符。C语言之所以不支持,是因为二进制流中携带了'\0'字符,导致无法知道字符串的真正结束位置。这带来了另一个Redis特性,二进制安全性。2.2为什么使用SDS通过上面阿芬提到的内容,我们知道SDS相对于传统的C语言字符串有很多优势,也正是这些本质的优势导致了SDS的存在。Redis是一个高性能的内存数据库,所以对性能的要求特别高。这种设计方式虽然浪费了一定的空间,但是满足性能要求还是值得的。在软件设计领域,用空间换取时间的方法还有很多。2.3SDS的常用API上面阿芬说的是一些原理,下面我从源码上给大家介绍一下。2.1中提到有获取长度len和释放空间free的动作,所以SDS底层必须有API提供支持。我们通过源码来看几个常用的API。1、源码sds.c文件中sdsfree函数定义如下:/*Freeansdsstring.Nooperationisperformedif's'isNULL.*/voidsdsfree(sdss){if(s==NULL)return;s_free((char*)s-sdsHdrSize(s[-1]));}2.在源码sds.h文件中,sdslen函数的定义如下:->len;caseSDS_TYPE_16:returnSDS_HDR(16,s)->len;caseSDS_TYPE_32:returnSDS_HDR(32,s)->len;caseSDS_TYPE_64:returnSDS_HDR(64,s)->len;}return0;}以上两个就是SDS底层对应的sdsfree和sdslen函数,用于释放SDS空间,获取SDS长度。3、源码sds.c文件中创建sds的函数定义如下:/*Createanempty(zerolength)sdsstring.Eveninthiscasethestring*alwayshasanimplicitnullterm.*/sdssdsempty(void){returnsdsnewlen("",0);}/*CreateanewsdsstringstartingfromanullterminatedCstring.*/sdssdsnew(constchar*init){size_tinitlen=(init==NULL)?0:strlen(init);returnsdsnewlen(init,initlen);}上面两个就是最下面对应的sdsempty和sdsnew函数SDS层,顾名思义,就是创建一个空的SDS,并创建一个新的SDS字符串。03.总结这篇文章,阿芬介绍了Redis的SDS以及SDS的底层结构,并且和C语言中的传统字符串进行了详细的对比,说明了SDS解决了哪些问题,最后带大家从源码中简单看了下在几个底层函数的实现中。在成为硬核程序员的路上,阿芬从未懈怠,斗志昂扬。你呢?你是不是也和阿芬一样,对未来充满期待!今天是2020年的第一个周末,你想怎么度过呢?是的?欢迎加入我们的Java极客技术知识星球留言,我们共同进步,共同成长。04.参考文档https://github.com/antirez/redishttps://redis.io/《Redis 设计与实现(第二版)》——黄建宏