都要2023年了,不会还有人不知道redis为什么快吧

        redis作为一种常见的kv数据库,在实际中使用非常广泛,其最大的特点就是"快",在系统中常被用来当做缓存快速获取想要数据。我们也会经常被问到,redis为什么这么快呢?兄弟们常常都是以下的回答:

  • redis基于内存
  • redis是单线程
  • redis采用阻塞式io和多路io复用
  • 优化了数据结构

        相信许多的兄弟在刚刚毕业的时候都是这个回答,我当初也是如此。随着我们年龄的增长,以及对技术的不断了解。我们不能只局限于此,我将把这几点具体展开。

redis基于内存

        这一点想必是大家最耳熟能详的一句话了。redis为什么快?因为基于内存!

        不过也确实如此,MySQL的数据和索引都是持久化保存在磁盘上的,因此当我们使用SQL语句执行一条查询命令时,如果目标数据库的索引还没被加载到内存中,那么首先要先把索引加载到内存,再通过若干寻址定位和磁盘I/O,把数据对应的磁盘块加载到内存中,最后再读取数据。Redis把数据存在内存中,读写都直接对数据库进行操作,天然地就比硬盘数据库少了到磁盘读取数据的这一步。

        不过redis快,基于内存是一个很重要的因素。不过不是全部的因素。

redis是单线程

        在很久以前,当时秋招的我在背到这一条的时候其实是纳闷的。

        redis单线程所以快?不应该是多线程来的快些吗?

        多线程有时候确实比单线程快,但也有很多时候没有单线程那么快。比如并发时,并发是指多个进程指令在一个cpu中运行在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

        在这种并发的情况下,就涉及到上下文切换的问题了。在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。redis的单线程模式就不需要上下文切换,提升了速度。避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

redis采用非阻塞式io和多路io复用

        非阻塞式io当用户进程发出 read 操作时,如果内核中的数据还没有准备好,那么它不会阻塞用户进程,而是立刻返回一个错误。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。当用户进程判断结果是一个错误时,它就知道数据还没有准备好,于是它可以再次发送read操作。

        多路IO复用,有时也称为事件驱动IO。它的基本原理就是有个函数会不断地轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

优化了数据结构

        redis实现的数据结构,使得我们对数据进行增删查改操作时,Redis 能更加高效的处理。具体的数据结构主要有以下这些:



SDS

        Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS。

        为啥要封装这样应该数据结构呢?

        首先,之前c语音的char的数据结构有缺陷!char类型的数据结构在读取到\0的时候就看做字符串结束,这样会有两个问题:

  1. 要是字符串中含有\0时会提前结束。
  2. 每次查询char类型的长度时,时间复杂度位0(n)
  3. C 语言的字符串是不会记录自身的缓冲区大小的,字符串操作函数不高效且不安全,比如可能会发生缓冲区溢出,从而造成程序运行终止。

        Redis封装的SDS数据结构如下:

 

  •  len记录所保存字符串的长度
  • alloc,分配给字符数组的空间长度
  • flags,SDS 类型,用来表示不同类型的 SDS
  • buf[],字节数组,用来保存实际数据

        因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而且 SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。

        不会发生缓冲区溢出,Redis 的 SDS 结构里引入了 alloc 和 leb 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用 。不够用时自动扩大空间大小。

        节省内存空间,通过不同类型的sds可以节省内存空间。

双向链表

         listnode节点是每一个双向链表节点的结构:

typedef struct listNode {
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点的值
    void *value;
} listNode;

        Redis 在 listNode 结构体基础上又封装了 list 这个数据结构,这样操作起来会更方便,链表结构如下:

  • listNode 链表节点带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;

  • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);

  • list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);

  • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;

         链表的缺陷也是有的,链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。

        这时便设计出来了压缩链表来更好的利用CPU 缓存来加速访问。

压缩列表

压缩列表是 Redis 数据类型为 list 和 hash 的底层实现之一。

  • 当一个列表键(list)只包含少量的列表项,并且每个列表项都是小整数值,或者长度比较短的字符串,那么 Redis 就会使用压缩列表作为列表键(list)的底层实现。

  • 当一个哈希键(hash)只包含少量键值对,并且每个键值对的键和值都是小整数值,或者长度比较短的字符串,那么 Redis 就会使用压缩列表作为哈希键(hash)的底层实现。 

 压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数;

  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;

  • zllen,记录压缩列表包含的节点数量;

  • zlend,标记压缩列表的结束点,特殊值 OxFF(十进制255)。

哈希表 

         哈希表是一种保存键值对(key-value)的数据结构。

        哈希表中的每一个 key 都是独一无二的,程序可以根据 key 查找到与之关联的 value,或者通过 key 来更新 value,又或者根据 key 来删除整个 key-value等等。

        Hash 表优点在于,它能以 O(1) 的复杂度快速查询数据。主要是通过 Hash 函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快。

        但是存在的风险也是有,在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高。

        解决哈希冲突的方式,有很多种。Redis 采用了链式哈希,在不扩容哈希表的前提下,将具有相同哈希值的数据链接起来,以便这些数据在表中仍然可以被查询到。

猜你喜欢

转载自blog.csdn.net/m0_58366209/article/details/127994149