文章目录
- 前言
- 源码
- 数据安全
- 动态
- 总结
前言
首先,Redis中的key使用的是字符串,而value则有各种类型,不过多数为字符串。
因此字符串是Redis中最常用的一种数据结构。
Redis虽然使用了C语言类实现,但是并没有直接使用C语言的字符串,原因有如下几点:
- 本质为字符数组,计算长度麻烦
- 通过特定标识作为字符串结尾,若value中有该标识则可能出现字符串保存错误问题
- 通过指针指向数组,不方便修改,只能使得指针指向另一字符串
综上,如果直接使用C语言中的字符串,那么将会带来一些不必要的麻烦。
因此Redis采用了类似于Java中字符串实现方式的方式来实现。
但是C语言并没有类,因此C语言使用的是结构体来完成这一结构。
该结构称为简单动态字符串(Simple Dynamic String)SDS。
我们插入一个以字符串作为键值对的数据的时候,Redis就会在底层创建两个SDS。
其中一个为key,另一个为value。
下面通过源码来讲解这一结构。
源码
如果没有学过C语言的,可以将结构体类比于Java中的类。
这里 struct 就类似于class 。
这里我们以sdshdr8为例(sdshdr5由于一些问题基本已经废弃)
uint8_t 代表的是unsigned int,并且这个int类型的长度为8bit。也就是最大255。
len:当前buf中已经保存的字符的长度
alloc:为当前buf申请的字节数大小,长度不一定等于len
flags:当前SDS的头类型,由于len的长度受限于数据类型,因此如果只有255可能保存不下,需要有更大的长度,而这个flags就用于标识当前无符号int数据类型的长度。
可选8,16,32,64(单位为bit)
buf:SDS中具体的数据
数据安全
例如一个字符串name,其SDS结构如下:
C语言结构体将会开辟一段连续的空间,该空间的大小为结构体内所有属性所需空间的和。
首先name的长度为4,并且第一次开辟的内存长度一般于与len一样,因此alloc也为4,由于使用的是sdshdr8,因此flags为1。之后为buf中的数据,其中保存的是具体的SDS所要保存的数据,这里为name\0,其中\0代表的是结束符。
同时由于SDS头中已经设定了len为4,因此Redis必须读取4个字符之后才会结束,因此即使当前buf中有结束标识\0也不会被当作真的结束标识,Redis依旧会继续向后读,这就解决了数据安全问题,如果是传统C语言,那么如果字符串中有结束标识\0,之后的数据就会直接被抛弃,会造成数据安全问题。而Redis的这种设计就解决了这一问题。
动态
SDS之所以被叫做动态字符串,是因为其具备动态扩容的能力。依旧以上面的SDS为例,假设我们需要向其追加一段内容“dog”,那么这里首先需要先申请内存空间,而在申请内存空间的时候就会设计以下情况:
- 如果追加后生成的新字符串小于1M,则新空间为扩展后字符串长度的2倍+1
- 如果追加后生成的新字符串大于1M,则新空间为扩展后字符串长度+1M+1,我们成为内存预分配。
这里之所以需要预分配是因为当我们申请内存的时候,需要进行用户态和内核态的切换。应用程序默认运行的环境为用户态,当应用程序需要进行一些特殊操作(例如读取文件,申请空间,杀死其他进程等)时,由于用户态默认是不安全的,因此需要进行用户态到内核态的切换,然后进入内核态之后在进行内存空间的分配。而申请内存这一过程极度消耗资源,因此需要尽量减少多次申请内存空间。而内存的预分配能尽可能的减少这一情况。
这是扩展后sdshdr8中的内容。
总结
Redis实现的SDS有如下优点:
- 获取字符串长度的时间复杂度为O(1)
- 支持动态扩容
- 内存预分配,减少内存分配次数
- 二进制安全,不会由于遇到结束符而提前结束读取