吊打面试官之 Redis 底层数据结构详解

1、前置知识

我们知道,Redis 很快,但是 Redis 为啥能这么快呢,首先,因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快。另一方面,就要归功于它的数据结构了。Redis 是一个键值对数据库,键值对是按照一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。

那 Redis 中的数据结构是啥呢?你可能会说 String(字符串)、List(列表)、Hash(哈希表)、Set(集合)和 Sorted Set(有序集合)。其实这知识 Redis 键值对中值的数据类型,也就是数据的保存形式。而我们说的数据结构是它们的底层实现。

简单来说,底层数据结构一共有6中,在 Redis 3.2版本后,增加了一种,下面我通过图表的方式展示:

吊打面试官之 Redis 底层数据结构详解_第1张图片
数据类型 底层数据结构
String sds
List 3.2之前:ziplist/linkedlist 3.2之后:quicklist
Hash ziplist/hashtable
Set intset/hashtable
Sorted Set ziplist/zskiplist

可以看到,String 类型的底层实现只有一种数据结构,也就是sds(简单动态字符串),而其他的四种数据类型都有两种底层实现结构,通常会把这四种类型称为集合类型,他们的特点是一个键对应一个集合的数据。

这里提一下 Redis 对象,干嘛用的,想象成对象头,不管你什么类型,都必须要带的,里面包含数据类型等等相关信息:由下面代码可以知道,一个 RedisObject 占用的字节数:4 + 4 + 24 + 32 + 64 = 128 位 / 8 = 16 字节。

/*
* Redis 对象
*/
typedef struct redisObject {
   
    // 类型 4bits,即上面[String,List,Hash,Set,Zset]中的一个
    unsigned type:4;
    // 编码方式 4bits,encoding表示对象底层所使用的编码。
    unsigned encoding:4;
    // LRU 时间(相对于 server.lruclock) 24bits
    unsigned lru:22;
    // 引用计数 Redis里面的数据可以通过引用计数进行共享 32bits
    int refcount;
    // 指向对象的值 64-bit
    void *ptr;
} robj;// 16bytes

下面我们就来具体讲讲这些底层的数据结构。

2、底层数据结构

2、简单动态字符串

SDS(simple dynamic string):简单动态字符串。SDS 中包含了 free(当前可用空间大小),len(当前存储字符串长度),buf[](存储的字符串内容),下面是 SDS 源码:

struct sdshdr{
   
    //记录buf数组中已使用字节的数量
    //等于 SDS 保存字符串的长度 4byte
    int len;
    //记录 buf 数组中未使用字节的数量 4byte
    int free;
    //字节数组,用于保存字符串 字节\0结尾的字符串占用了1byte
    char buf[];
}

包含了 len、free、buf[] 三个属性,占用的字节数最少是:4 + 4 + 1 = 9 byte。(仅限 Redis3.2 之前,3.2版本之后 SDS 结构发生了变化)

比如你执行 set hello world ,会创建出两个 SDS,一个存 Key:hello,一个存 Value:word,比如如下:

吊打面试官之 Redis 底层数据结构详解_第2张图片

说了这么多,那为什么要用 SDS 呢,Redis 是 C 编写的,为啥没有使用 C 中的字符串呢?

优化获取字符串长度

C 语言要想获取字符串长度必须遍历整个字符串的每一个字符,然后自增做累加,时间复杂度是O(n),SDS 直接维护了一个 len 变量,时间复杂度为 O(1)。

减少内存分配

当我们对一个字符串类型进行追加的时候,可能会发生两种情况:

  • 当剩余空间(free)足够容纳追加内容时,就不需要再去分配内存空间,这样可以减少内存分配次数。
  • 当前剩余空间不足以容纳追加内容,需要重新为其申请内存空间。

而 C 语言字符串在进行字符串的扩充和收缩的时候,都会面临内存空间的重新分配问题,如果忘记分配或者分配大小不合理还会造成数据污染。

那么 SDS 的 free 值哪来的呢?也就是字符串扩容策略。

  • 当给 SDS 的值追加一个字符串,而当前剩余空间不够时,就会触发 SDS 的扩容机制,扩容采用了空间预分配的策略,即分配空间的时候:如果 SDS 值大小 < 1M,则增加一倍;反之如果 > 1M,则当前空间增加 1M 作为新空间。
  • 当 SDS 的字符串缩短了,SDS 的 buf 会多出来一些空间,这个空间并不会马上被回收,而是暂时留着以防再用的时候进行多余的内存分配,这个是惰性空间释放的策略。

惰性释放空间

当我们截断字符串时,Redis 会把截断部分置空,只保留剩余部分,且不立即释放阶段部分的内存空间,这样做的好处就是当下次再对这个字符串追加的时候,如果当前剩余空间足以容纳追加内容时,就不需要再去重新申请空间,避免了频繁的内存申请。暂时用不到的空间可以被 Redis 定时删除或惰性删除。

防止缓冲区溢出

其实和减少内存分配是成套的,都是因为 SDS 会预先检查内存自动分配来做到防止缓冲区溢出的。当需要对一个 SDS 进行修改的时候,Redis 会在执行拼接操作之前,预先检查给定 SDS 空间是否足够,如果不够,会先拓展 SDS 的空间,然后再执行拼接操作。

二进制安全

在 C 语言中通过判断当前字符是否是’\0’来确定字符串是否结束,对于一些二进制文件(如图片等),内容可能包含空字符串,因此无法正确读取。而在 SDS 结构中,只要遍历长度没有达到 len,即使遇到’\0’也不会认为字符串结束,不会发生上述问题。

兼容部分 C 字符串函数

虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用一部分 C 语言库()的函数

你可能感兴趣的:(Redis,redis,数据结构,数据库)