文章目录

  • 前言
  • 源码
  • 数据安全
  • 动态
  • 总结


前言

首先,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中具体的数据

c语言 redis 订阅分发_字符串


c语言 redis 订阅分发_面试_02

数据安全

例如一个字符串name,其SDS结构如下:

c语言 redis 订阅分发_redis_03


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中的内容。

c语言 redis 订阅分发_数据库_04

总结

Redis实现的SDS有如下优点:

  • 获取字符串长度的时间复杂度为O(1)
  • 支持动态扩容
  • 内存预分配,减少内存分配次数
  • 二进制安全,不会由于遇到结束符而提前结束读取