Redis 为什么这么快?(详解版)

Redis 为什么这么快?(详解版)

前言

这是Redis官网提供的性能基准测试,Y轴是QPS,X轴是连接数。我们可以看到连接数小于10000时,QPS可每秒达到10-12万左右。

在这里插入图片描述

主要原因

  1. 内存存储: Redis将数据存储在内存中,因此读写速度非常快。此外,Redis还可以将数据持久化到磁盘,以防止数据丢失。
  2. 单线程模型: Redis采用单线程模型,避免了多线程的上下文切换开销,同时利用了现代CPU的多核优势,通过多路复用技术实现高并发。
  3. 非阻塞IO: Redis 基于 IO 多路复用实现了非阻塞式 IO,可以在IO操作完成之前继续执行其他操作,提高了系统的并发能力。
  4. 数据结构丰富: Redis支持丰富的数据结构,如字符串、哈希、列表、集合、有序集合等,可以满足不同场景的需求,提高了数据操作的效率。

内存存储

Redis 是基于内存操作的数据库,不论读写操作都是在内存上完成的,直接访问内存的速度远比访问磁盘的速度要快多个数量级。

由下图可知:内存访问一次大概120ns(微秒),SSD 硬盘访问一次50-150us(纳秒),如果按照访问一次150us来算,性能差距在1000倍。这是因为1us等于1000ns。所以,150微秒是150,000纳秒,而120纳秒只是150微秒的一千分之一。

在这里插入图片描述

单线程模型

Redis使用单线程的主要原因是为了避免多线程带来的复杂性和线程安全性的问题。

以下是一些原因:

  • 简化并发控制: 使用单线程可以避免多线程并发控制的复杂性,如锁、同步和线程安全等问题。这样可以减少开发和维护的复杂性。
  • 避免上下文切换开销: 多线程在切换上下文时会有一定的开销,而单线程可以避免这种开销,提高系统的性能和响应速度。
  • 内存和CPU效率: 单线程可以更好地利用CPU缓存,避免多线程间的竞争和冲突,提高内存和CPU的效率。
  • 事件驱动模型: Redis使用事件驱动模型,通过非阻塞的IO和事件循环来处理并发请求,这种模型更适合单线程。

虽然Redis使用单线程,但它通过非阻塞IO、事件驱动和多路复用等技术来实现高并发和高性能。因此,即使是单线程,Redis仍然能够处理大量的并发请求。当然,在某些特定场景下,如需要大量计算的情况下,单线程可能会成为性能瓶颈,但在大多数情况下,Redis的单线程模型能够提供高性能和稳定性。

非阻塞IO

Redis 内部使用文件事件处理器 file event handler ,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
在这里插入图片描述

首先,Redis 服务端进程初始化的时候,会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。

客户端 socket01 向 Redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。

假设此时客户端发送了一个 set key value 请求,此时 Redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。

如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok ,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

redis 6.0开始引入多线程
注意! Redis 6.0 之后的版本抛弃了单线程模型这一设计,原本使用单线程运行的 Redis 也开始选择性地使用多线程模型。

前面还在强调Redis 单线程模型的高效性,现在为什么又要引入多线程?这其实说明 Redis 在有些方面,单线程已经不具有优势了。因为读写网络的 Read/Write 系统调用在 Redis 执行期间占用了大部分 CPU 时间,如果把网络读写做成多线程的方式对性能会有很大提升。

Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。 之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务、LPUSH/LPOP 等等的并发问题。

非阻塞IO这一块知识点是直接引用互联网 Java 工程师进阶知识完全扫盲 - Doocs 技术社区,讲的实在是太棒了👍

数据结构

SDS简单动态字符串

字符串是 Redis 中最常用的数据结构,不过作者并没有使用 C 标准款的实现,而是自己实现了一套简单动态字符串(SDS)作为替代。
在这里插入图片描述

Redis 中定义动态字符串的结构:

struct sdshdr{
    int len;//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
    int free;//记录buf数组中未使用字节的数量
    char buf[];//字符数组,用于保存字符串
}

在这里插入图片描述

SDS相对C字符串的优点:

  1. 获取字符串的长度复杂度为常数: len属性记录字符串的长度,从C字符串的复杂度C(N)降到C(1)。
  2. 杜绝缓冲区的溢出: 对SDS进行修改时,先判断buf[]空间是否足够,如果空间不满足则自动分配,而C字符串需要手动分配空间,一旦用户忘记就会发生缓冲区溢出。
  3. 减少修改字符串时带来的内存重分配次数: a.空间预分配:对SDS进行修改后,如果len的空间小于1MB,则free会分配和len相同的空间;如果len的空间大于1MB,则free会分配1MB的空间。对SDS的N次修改,从必定分配N次到最多分配N次。b.惰性空间释放:对SDS进行缩短,不被使用的空间被free记录,等待再次被使用,再次避免内存重分配。
  4. 二进制安全: SDS的API都是二进制安全,并以处理二进制方式处理数据。
  5. 兼容部分C字符串的函数: SDS和C字符串都会在字符串最后留一个空间放’/0’,这保证了C字符串的函数可以被调用,避免不必要的代码。

ZipList压缩列表

ZipList是List 、Hash、Sorted Set三种数据类型底层实现之一。
ZipList的结构:

struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为0xFF
}

在这里插入图片描述

ZipList的特性:

  • 压缩列表是一种结构类似数组的顺序结构。相比起传统的链表,它节点连续的内存块组成,每个节点都不需要指向前驱接点、后继节点以及存储数据的指针,不容易产生内存碎片,内存利用率高。
  • 适用情景:一个列表只有少量数据,并且每个列表项是小整数或短字符串ZipList的结构。
  • 查找第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,时间复杂度是O(1)。而查找其他元素时,只能逐个查找,此时的时间复杂度是O(N)。

ZipList的缺点:

插入和删除操作需要频繁的申请和释放内存,同时会发生内存拷贝,数据量大时内存拷贝开销较大。

3.2 以后的版本版本,Redis 又逐渐引入了QuickList数据结构来替代ZipList压缩列表。
在这里插入图片描述

redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

字典/哈希表

HashTable 是Set 、Hash、Sorted Set三种数据类型底层实现之一。
在这里插入图片描述

HashTable的特性:

Redis 的哈希表与 Java 中相似,也是基于 key 得到的哈希值计算桶下标,再采用拉链法解决冲突,并在装载因子超过预定值时自动扩容。

它的特殊之处在于,当扩容的时候,它会基于扩容后的大小创建一张新的哈希表,然后在访问旧表的时候,每次将访问到的桶中的链表转移到新表中。

在这个过程中,每次操作的时候都会先访问旧表,然后再访问新表,直到旧表的数据组件的全部转移到新表以后,旧表会被回收,只留下新表。

这个过程被称为渐进式哈希,它巧妙地避免的在一次操作中大批量的进行数据迁移,而是将其分摊到多次请求中。

跳表

SkipList是Sorted Set的底层实现结构之一。

SkipList的特性:

  • SkipList是一种有序数据结构,它通过在每个节点中存储着多个指向其他节点的指针,从而达到快速访问节点的目的。
  • SkipList支持平均O(logN)、最坏O(N)时间复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
  • SkipList在LinkedList的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位。
    首先从考虑一个有序表开始:

在这里插入图片描述

从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 < 2, 4, 6 >,总共比较的次数为 2 + 4 + 6 = 12 次。有没有优化的算法吗? 链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:
在这里插入图片描述

这里我们把 < 14, 34, 50, 72 > 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:
在这里插入图片描述
这里元素不多,体现不出优势,如果元素足够多,这种索引结构就能体现出优势来了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值