Redis 核心原理串讲(上),从一条请求透视高性能的本质

文章目录

  • Redis 核心原理总览(全局篇)
  • 前言
  • 一、请求
  • 二、数据结构
    • 1. 有哪些?
    • 2. 为什么节省内存又高效?
  • 三、网络模型
    • 1、四种常见IO模型
      • 1.1 同步阻塞
      • 1.2 同步非阻塞
      • 1.3 IO多路复用
      • 1.4 异步IO
    • 2、事件驱动
      • 2.1 引子
      • 2.2 事件驱动模型
    • 3、Reactor 模型
      • 3.1 单线程模型
      • 3.2 多线程模型
      • 3.3 主从多线程模型
  • 四、线程模型
    • 1、单线程模型
    • 2、后台线程
    • 3、多线程模型
  • 总结


Redis 核心原理总览(全局篇)

正文开始之前,我们先思考下「如何造一个缓存组件?」

该片段是 Redis 原理知识地图,请仔细阅读!(基于 redis6.2

1)最小可用版:

  • 要快:缓存最核心的目的是支持快速访问,硬件层面一般选择「内存」
  • 远程访问:作为缓存组件,要支持单独部署,可以利用现有开源网络库,也可以自己实现。

大部分语言都提供了内存操作,条件 1 很容易满足,条件 2 要支持远程访问,就要和 TCP 连接打交道,我们可以利用开源的网络库,比如 C 版的 libc 等。

原理篇第一部分:将围绕一条请求探索 redis 高性能的核心原理。

2)进阶版:

第一步我们已经有了缓存组件最基本的雏形,并且已经达到了高性能的处理能力,这个时候我们可能有更多的诉求:

  • 稳定:即 尽可能不丢数据、无故障或故障后快速恢复、自动处理故障的能力
  • 可扩展:单机容量或者 QPS 达到上限,支持水平扩展能力。

原理篇第二部分:将围绕 redis 架构演进进行剖析。

全局知识地图:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第1张图片

前言

本文围绕「请求主线」透视 Redis 高性能的核心原理,在正文开始之前我们先思考几个问题:

  • Redis 为什么节省内存又高效?
  • 同步/异步、阻塞/非阻塞有什么区别?
  • 删除一个百万级数据的 hash 字典会阻塞吗?
  • Redis6.0 的多线程需要考虑并发问题吗?

如果你对以上问题了然于胸,这篇文章读起来很容易,权当帮你串联、回顾知识点,如果不是很清楚也没关系,且听我细细道来!


一、请求

我画了一条完整的请求处理流程,你可以参考下:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第2张图片

从用户发出请求到接收到响应这过程大概会经历以上7个阶段,你可以思考下,redis 服务端高性能主要处理哪些阶段?

你应该猜到了,是 3、4、5 阶段。

  • IO 模型:阶段 3、5
  • 线程模型、数据结构:阶段 4

我们先尝试回答这个问题:「Redis 高性能的原因?」

内存 + 优秀的数据结构/算法 + 高性能网络I/O

其中,最根本原因是基于「内存」,你可以想想你的应用,如果只操作内存而不是数据库,是不是非常快?

另外,我们还可以设计一些优秀的算法、数据结构让这些基于内存的操作 更快!

当整个 “业务” 模块已经非常高效时,前置模块 ------ 「网络 IO」可能成为瓶颈,我们得想办法让它达到高配模式,即 高性能网络 IO 模型

请求主线的整块内容就串起来了,我们小结下涉及到的核心知识点:

  • 数据类型 + 数据结构
  • 高性能网络I/O:I/O多路复用、事件驱动、Reactor模型
  • 线程模型:单线程模型、后台线程、多线程模型

值得一提的是,对于长链路优化终极秘法是「拆分」,然后逐个击破。

二、数据结构

1. 有哪些?

数据类型:

  • 5 常见数据类型:string、list、set、hash、zset
  • 其他数据类型:stream、hyperloglog、bitmap、geo

数据结构:

  • sds、ziplist、quicklist、listpack、hash、skiplist、rax、intset

数据类型,属于更上层、直接提供对外使用的类型,而数据结构则是数据真正的组织方式。在 Redis 中,大家最好区分开来,避免混淆。

我们看看常见数据结构全景图:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第3张图片

注:参考版本 redis 6.2

2. 为什么节省内存又高效?

1)节省内存:

  • 压缩: 如ziplist、listpack …
  • 前缀树:rax
  • 概率算法:HyperLogLog

我们以 ziplist 压缩列表为例:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第4张图片

最开始 redis 使用双向链表(支持向前、向后遍历),每个节点都存储两个指针,也就是保存的是地址,一个指针占 4 字节,共 8 字节,如何更节省?

我们先去掉每个节点的两个指针,那如何后向遍历呢?使用 encoding 字段编码,可根据数据类型、长度选择 1、2、3 … 等编码字节表示数据的真实长度,后向遍历时挨个处理即可。

如何向前遍历?使用 prevlen 记录前一个元素的大小,也就能很方便定位前一个元素。

2)高效:

  • 空间换时间: 如跳表 …
  • SDS: 空间预分配、惰性释放…
  • 组合: 如 hash、zset …

三、网络模型

1、四种常见IO模型

1)同步、异步:

内核行为:如果内核主动发起数据拷贝(从内核到用户态)则为异步,反之为同步。

2)阻塞、非阻塞:

用户态行为:用户态发起系统调用,如果阻塞读,则为阻塞;如果立即返回(即使数据未准备就绪)则为非阻塞。

1.1 同步阻塞

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第5张图片
用户态线程(进程)发起调用,此时内核数据未准备就绪,用户态会一直阻塞知道数据拷贝完成。

这个过程中,用户态主动发起调用,所以是「同步」,同时,用户态阻塞等待数据完成,因此,这个过程是「阻塞」的。

阻塞是啥意思?用户态线程在此期间,一直等待数据就绪,空闲状态、不做任何事情。

是不是很浪费资源?通常情况下,用户态应用会限制每个应用的资源数(线程),「干等」期间就是浪费资源!

1.2 同步非阻塞

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第6张图片

为了解决「干等」浪费资源的情况,我们通过用户态轮询的方式来解决,具体是

  • 通过 read 系统调用,如果内核数据未就绪,直接返回,用户态控制过一会再来
  • 多轮操作,直到真正 read 数据

值得注意的是:数据准备就绪后,read 读取数据期间,该操作仍是阻塞。

1.3 IO多路复用

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第7张图片

同步非阻塞 解决了「干等」问题,但效率还不算高,毕竟每个用户态线程都要亲自执行 「read 轮询」这个操作,100个并发连接就需要 100 个线程来完成。

为了解决这个问题,IO 多路复用登场了 …

IO 多路复用的目的是用 1 个线程解决 100、1000 … 个并发连接,具体怎么做的?

  • 首先,通过 select(epoll …) 操作,查询监听的 N 个连接,是否数据有准备就绪的。
  • 对数据准备就绪的连接,则挨个轮训调用其 read 方法读取对应数据。

为什么叫 I/O 多路复用?

因为一个专用线程轮询发起 select 调用可以向内核查「多个」数据通道(Channel)的状态,所以叫多路复用。

内核支持

I/O多路复用需要操作系统内核支持,比如 Windows下使用select、Linux下使用 poll/epoll、Mac下使用 kqueue。

值得一提的是,IO 多路复用是目前最为流行的 IO 模型,常见的 tomcat、nginx、kafka、netty 以及我们今天的 redis 都是这种 IO模型

1.4 异步IO

前面介绍了三种网络 IO 模型,你也看到了,不管是阻塞还是非阻塞,都需要用户态线程「主动」发起操作,有没有一种用户态被动接收的模型呢?

当然有,这就是 异步IO 模型,工作原理大概是这样的:当我们首次发起 read 调用,这个操作类似于「注册」操作,内核收到这个操作后,先记下来,等到数据真正准备就绪的时候,然后找到这条记录,主动 发起拷贝(类似于回调)。

我画了张图,你可以参考下:
Redis 核心原理串讲(上),从一条请求透视高性能的本质_第8张图片

这种是真正做到了异步,将更多的动作交给了系统内核,也就是说需要操作系统来提供更多的支持,工作量其实不小,目前 Windows 操作系统提供了真正的异步能力,而 Linux 类则还没支持上。

2、事件驱动

2.1 引子

客户端、服务端通信的那张经典的模型图你肯定还记得,这里我就不再罗列了,我们聊聊服务端如何演进,一步步实现高性能的原因。

最初,我们的要求很简单,只要客户端、服务端能正常通行即可,于是,我们用一个线程来处理所有连接:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第9张图片

慢慢的发现这种串行处理太慢了,于是改进了一版,每一个新连接都用新开一个线程去处理,如下:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第10张图片

速度上快了很多,但线程数没法控制了,过多的线程消耗还会导致服务端资源枯竭、线程上下文频繁切换带来了严重的性能损耗。

你也想到了,我们可以继续改进,使用线程池:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第11张图片

看起来完美了,但仔细一想,还是有问题,对于一些长连接或者耗时久的连接还是会很快耗尽线程池资源,服务端能力同样受限。

问题出在哪里?大家想想,我们的业务操作(内存操作)和网络IO操作,谁更耗时?

问题就在这里,通常情况下,网络IO 才是瓶颈点,一方面,网络的不确定性,等待网络数据包的就绪实际也具有不确定性,另一方面,read/write 等调用涉及与内核的交互,进程上下文的切换等都是开销。

总结来说,我们的线程大部分时机都在「等待」网络数据包的就绪,也就是说,在等待中,浪费了我们线程池中的线程资源。

如何解决?拆分

2.2 事件驱动模型

事件驱动的核心是将 连接工作线程 分离,避免工作线程将过多的时间花在 I/O 等待上,以 事件 驱动工作线程,即,专人做专事(想想现代人的分工)

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第12张图片

当有事件准备就绪时,再交给工作线程去处理,这样一来,我们的工作线程就节省了大部分的等待时间,全方位投入到实际工作中,彻底释放工作线程!!!

那负责处理连接的线程如何达到高性能?IO 多路复用,通常情况下,一个线程就能达到要求!

3、Reactor 模型

前面我们提了「事件驱动」,是一种抽象、指导思想,而 Reactor 模型则是一种具体的实现,换句话说,Reactor模型是事件驱动的一种实现

Reactor 模式由 Reactor 线程、Handlers 处理器两大角色组成:

  • Reactor 线程:主要负责连接建立、监听IO事件、IO事件读写以及将事件分发到Handlers 处理器
  • Handlers 处理器:非阻塞的执行业务处理逻辑

3.1 单线程模型

当我们的业务操作、网络IO 两部分能力都非常高效时,我们用一个线程是不是就可以搞定?是的,这就是单线程模型。

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第13张图片

图中,acceptor 负责建立连接,利用「IO多路复用」可以达到一个线程应对成千上万连接的能力。

值得一提的是,本文的主角 — redis 就是采用这种模式,从 redis 单机几万的 QPS 也可以看出这种模式同样不可小觑,关键还得看场景!

3.2 多线程模型

不是所有的业务操作都是基于内存的,比如我们的 web 应用,很多可能需要操作 MySQL 这样的数据库,耗时就增加了,几十、上百毫秒都是很正常的事。

这样情况下,如果还是单线程处理业务,那这块的能力将会直线下降:

  • 我们可以考虑将业务这块的使用线程池
  • 同时,acceptor 也采用分离的线程处理

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第14张图片

acceptor 还是利用 IO 多路复用处理连接,当监听到有就绪的 IO 事件时,就交给业务线程池去处理,分工明确、各司其职。

3.3 主从多线程模型

说到底,还是要在「网络IO」「业务操作」两部分进行 PK,瓶颈在哪里,就优化哪里。

当我们的业务是纯内存操作时,那一般 「网络 IO 」就是瓶颈,怎么优化?还得搞多线程,只不过这里还有些讲究。

网络IO 涉及两部分:建立新连接、监听/处理已建立连接的IO事件,这两部分我们也可以分开处理:

  • 建立新连接:这里叫主 reactor,负责建立新的连接,然后交给子 reactor 处理
  • 监听/处理IO事件:也叫子 reactor,负责从建立的连接中监听IO事件,将就绪读取的事件交给业务线程池去处理。

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第15张图片

网络这块说了这么多,相信你也清楚了,优化思路是:对于长的链路,先拆分、然后逐个击破

四、线程模型

你可能会经常听到这几个问题:

  • redis 是单线程的?
  • keys 这类命令要避免在线上使用?
  • 删除一个百万级的 hash 字典有阻塞风险?
  • redis6.0 已经有了多线程了,需要考虑并发问题吗?

通过这个模块,你会对这几个问题了然于胸~

1、单线程模型

redis 是单线程的,这个问题从某种层面来看是没有任何问题的,到目前为止(redis6.2),redis 的 命令执行仍然是单线程串行处理

Redis 是典型的事件驱动服务,内部有文件事件和时间事件两类:

  • 文件事件:对外 - 处理用户请求,即 连接、I/O读写以及命令处理等
  • 时间事件:对内 - 定时任务、各类指标/监控,比如内存驱逐、过期key、关闭超时客户端等

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第16张图片

redis 服务端的运行就依赖于主线程轮询交替执行「文件事件」和「时间事件」,换句话说,整个 redis 服务端大厦由主线程来驱动完成。

值得注意的是,redis 服务端大部分工作都是由主线程来完成,地位不可撼动

前面我们也提到过,redis 属于 Reactor 模型中的单线程模型,也就是说 「网络IO」和「命令执行」都是一个线程完成。

我们这里的单线程模型也主要指正常请求中的 网络IO + 命令处理。

:单线程模型一定要有自己的认识,要知道,redis 也在不断对网络IO模块进行优化,升级一下版本、或者调整一些参数,实际运行的模型可能就变了,需要你辩证的来看!!!

2、后台线程

删除一个百万级的 hash 字典有阻塞风险?姿势用对了还真就可以!!!redis 提供了一些后台线程,来专门应对这种 慢操作

截止到目前,redis 共有三个后台线程,分别是 close_file、aof_fsync和lazy_free:

  • close_file 表示关闭相应文件描述符对应的文件(释放套接字、数据空间等)
  • aof_fsync 表示 AOF 刷盘
  • lazy_free 表示惰性释放空间

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第17张图片

3、多线程模型

Redis 在6.0版本新增多线程实现,主要针对IO读、写使用多线程,命令执行仍然是单线程处理:

Redis 核心原理串讲(上),从一条请求透视高性能的本质_第18张图片

命令执行为什么不使用多线程?

  • redis 性能主要在于网络和内存,而不是 CPU
  • 另外,使用单线程处理命令可以避免并发控制问题。

总结

我们从一条请求出发,然后深入剖析其背后的原理,也清楚的看到了哪些关键点能影响到 redis 的高性能。

redis 是基于内存的,这是其 的核心原因,我们先介绍了 redis 的数据结构、类型以及一些典型的算法,透视 redis 对数据的组织、对内存的掌控以及对空间/时间的权衡。

然后进入网络模块,这是重中之重,是任何需要网络交互并且渴望拥有高性能的组件必须考虑的问题,redis 实现了自己的轻量级网络库,采用的是 单线程版的 Reactor 模型。

最后是其线程模型,命令处理始终沿用单线程串行执行,随着版本迭代,引入了后台线程处理慢操作,redis6.0 还提供了多线程来解决网络 IO 瓶颈。

你可能感兴趣的:(缓存,#,redis,MQ,redis,java,数据库)