Redis 为什么这么快?

前言

我们都知道 Redis 是使用内存来进行数据的存储,这也是为什么 Redis 的访问速度要远远快于 MySQL 的主要原因,因为是使用内存存储数据,可以避免频繁的进行写盘操作,大大降低响应时间,我们仅仅知道因为它是基于内存实现的,对于其它原因为什么快是一概不知。

接下来就以为什么 Redis 会这么快的原因分成几部分来讲解。

基于内存实现

这点在上面就已经说过了,这里在简单说一下。

Redis 是基于内存的数据库,不可避免的要和磁盘数据库做对比,比如 MySQL 数据库。

MySQL 是关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中,读取速度较慢,每次请求访问数据库时,都存在着 I/O 操作,如果反复频繁的访问数据库,会在数据库中花费过多的时间,并且反复访问数据库会导致数据库的负载过高。

而 Redis 是基于内存的缓存数据库,用于存储使用频繁的数据,这样减少访问数据库的次数,提高运行效率。

高效的数据结构

我们都知道,MySQL 索引为了提高效率,选择了 B+ 树的数据结构,对于一个应用场景来说合理的数据结构可以让你的应用或者程序更快。

接下来我们就说说 Redis 的底层数据结构,底层数据结构一共有6种,分别是,简单动态字符串,双向链表,压缩列表,哈希表,跳表和整数数组,它们和数据类型的对应关系如下图所示:

在这里插入图片描述

下面我们来详细介绍一下 Redis 中 5 大数据结构的底层实现。

SDS 简单动态字符串

Redis 是用 C 语言开发完成的,但在 Redis 字符串中,并没有使用 C 语言中的字符串,而是用一种称为 SDS(Simple Dynamic String)的结构体来保存字符串。

在这里插入图片描述
例如:执行命令 set key value,key 和 value 都是一个 SDS 类型的结构存储在内存中。

SDS 与 C 字符串的区别:

字符串长度

C 字符串本身不记录长度信息,每次获取长度信息都需要遍历整个字符串,复杂度为 O(n);C 字符串遍历时遇到 ‘\0’ 时结束。
在这里插入图片描述

SDS 中 len 字段保存着字符串的长度,所以总能在常数时间内获取字符串长度,复杂度是 O(1)。

在这里插入图片描述

拒绝缓冲区溢出

当 SDS API 需要对 SDS 进行修改时,首先会检查空间是否足够,若不充足则会分配新空间,避免了缓冲区溢出问题。

减少字符串修改时带来的内存重新分配的次数

在 C 中,当我们频繁的对一个字符串进行修改(append 或 trim)操作的时候,需要频繁的进行内存重新分配的操作,十分影响性能。如果不小心忘记,有可能会导致内存溢出或内存泄漏,对于 Redis 来说,本身就会很频繁的修改字符串,所以使用 C 字符串并不合适。而 SDS 实现了空间预分配和惰性空间释放两种优化策略:

  • 空间预分配: 字符串修改越频繁的话,内存分配就越频繁,就会很消费性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗
  • 惰性空间释放: SDS缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配

二进制安全

在 Redis 中不仅可以存储 String 类型的数据,也可能存储一些二进制数据,但二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 ‘\0’ 等。前面我们提到过,C 中字符串遇到 ‘\0’ 会结束,那 ‘\0’ 之后的数据就读取不上了。但在 SDS 中,是根据 len 长度来判断字符串结束的。

哈希表(字典)

Redis 作为 k-v 型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如 HashMap,通过 key 就可以直接获取到对应的value。而哈希表的特性,在O(1)时间复杂度就可以获得对应的值。

跳表

作为 Redis 中特有的数据结构-跳跃表,其在链表的基础上增加了多级索引来提升查找效率。

在这里插入图片描述

这是跳跃表的简单原理图,每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在 O(logN) 的时间复杂度里查找到对应的节点。

下面这张是跳表真实的存储结构图:
在这里插入图片描述
网上对跳表的各种理论讲解都比较多,这里分享一篇我的文章 Redis-跳跃表(skip List).。其基本原理为添加多级索引来加快查找速度实现O(logN)的时间复杂度, 通过随机函数确定节点插入到几级索引中来防止跳表退化为单链表。

双向链表

列表 List 更多是被当作队列或栈来使用的。队列和栈的特性一个先进先出,一个先进后出。双向链表很好的支持了这些特性。

在这里插入图片描述
以上是双端链表图结构。

链表里每个节点都带有两个指针,prev 指向前节点,next 指向后节点。这样在时间复杂度为 O(1) 内就能获取到前后节点。

在这里插入图片描述

这里小伙伴可能注意到了,头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点。这样设计能对双向节点的处理时间复杂度降至 O(1),对于队列和栈来说再适合不过。同时链表迭代时从两端都可以进行。

在这里插入图片描述
而且,头节点里同时还有一个参数 len 3 ,和上边说到的 SDS 里类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到 len 值就可以了,这个时间复杂度是O(1),我们可以看出,这些特性都降低了 List 使用时的时间开销。

压缩列表

ziplist 是 Redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构 ,如下图:
在这里插入图片描述

压缩列表是经过特殊编码,专门为了提升内存使用效率设计的。所有的操作都是通过指针与解码出来的偏移量进行的。并且压缩列表的内存是连续分配的,遍历的速度很快。

I/O 多路复用模型

Redis 采用网络 IO 多路复用技术,来保证在多连接的时候系统的高吞吐量。 多路-指的是多个 socket 网络连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的、也是目前最好的多路复用技术。 采用多路 I/O 复用技术,可以让单个线程高效处理多个连接请求(尽量减少网络 IO 的时间消耗)。并且 Redis 在内存中操作数据的速度非常快(因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈)。主要以上两点造就了 Redis 具有很高的吞吐量。 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求。

避免上下文切换

那么单线程的 Redis 为什么会快呢?

因为多线程在执行过程中需要进行 CPU 的上下文切换,需要完成一系列工作,这是非常消耗资源的操作,Redis 又是基于内存实现的,对于内存来说,没有上下文切换效率就是最高的。多次读写都在一个 CPU 上,对于内存来说就是最佳方案。

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值