文章目录
- 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 高性能的核心原理,在正文开始之前我们先思考几个问题:
- Redis 为什么节省内存又高效?
- 同步/异步、阻塞/非阻塞有什么区别?
- 删除一个百万级数据的 hash 字典会阻塞吗?
- Redis6.0 的多线程需要考虑并发问题吗?
如果你对以上问题了然于胸,这篇文章读起来很容易,权当帮你串联、回顾知识点,如果不是很清楚也没关系,且听我细细道来!
一、请求
我画了一条完整的请求处理流程,你可以参考下:
从用户发出请求到接收到响应这过程大概会经历以上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 6.2
2. 为什么节省内存又高效?
1)节省内存:
- 压缩: 如ziplist、listpack …
- 前缀树:rax
- 概率算法:HyperLogLog
- …
我们以 ziplist 压缩列表为例:
最开始 redis 使用双向链表(支持向前、向后遍历),每个节点都存储两个指针,也就是保存的是地址,一个指针占 4 字节,共 8 字节,如何更节省?
我们先去掉每个节点的两个指针,那如何后向遍历呢?使用 encoding 字段编码,可根据数据类型、长度选择 1、2、3 … 等编码字节表示数据的真实长度,后向遍历时挨个处理即可。
如何向前遍历?使用 prevlen 记录前一个元素的大小,也就能很方便定位前一个元素。
2)高效:
- 空间换时间: 如跳表 …
- SDS: 空间预分配、惰性释放…
- 组合: 如 hash、zset …
- …
三、网络模型
1、四种常见IO模型
1)同步、异步:
内核行为:如果内核主动发起数据拷贝(从内核到用户态)则为异步,反之为同步。
2)阻塞、非阻塞:
用户态行为:用户态发起系统调用,如果阻塞读,则为阻塞;如果立即返回(即使数据未准备就绪)则为非阻塞。
1.1 同步阻塞
用户态线程(进程)发起调用,此时内核数据未准备就绪,用户态会一直阻塞知道数据拷贝完成。
这个过程中,用户态主动发起调用,所以是「同步」,同时,用户态阻塞等待数据完成,因此,这个过程是「阻塞」的。
阻塞是啥意思?用户态线程在此期间,一直等待数据就绪,空闲状态、不做任何事情。
是不是很浪费资源?通常情况下,用户态应用会限制每个应用的资源数(线程),「干等」期间就是浪费资源!
1.2 同步非阻塞
为了解决「干等」浪费资源的情况,我们通过用户态轮询的方式来解决,具体是
- 通过 read 系统调用,如果内核数据未就绪,直接返回,用户态控制过一会再来
- 多轮操作,直到真正 read 数据
值得注意的是:数据准备就绪后,read 读取数据期间,该操作仍是阻塞。
1.3 IO多路复用
同步非阻塞 解决了「干等」问题,但效率还不算高,毕竟每个用户态线程都要亲自执行 「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 调用,这个操作类似于「注册」操作,内核收到这个操作后,先记下来,等到数据真正准备就绪的时候,然后找到这条记录,主动
发起拷贝(类似于回调)。
我画了张图,你可以参考下:
这种是真正做到了异步,将更多的动作交给了系统内核,也就是说需要操作系统来提供更多的支持,工作量其实不小,目前 Windows 操作系统提供了真正的异步能力,而 Linux 类则还没支持上。
2、事件驱动
2.1 引子
客户端、服务端通信的那张经典的模型图你肯定还记得,这里我就不再罗列了,我们聊聊服务端如何演进,一步步实现高性能的原因。
最初,我们的要求很简单,只要客户端、服务端能正常通行即可,于是,我们用一个线程来处理所有连接:
慢慢的发现这种串行处理太慢了,于是改进了一版,每一个新连接都用新开一个线程去处理,如下:
速度上快了很多,但线程数没法控制了,过多的线程消耗还会导致服务端资源枯竭、线程上下文频繁切换带来了严重的性能损耗。
你也想到了,我们可以继续改进,使用线程池:
看起来完美了,但仔细一想,还是有问题,对于一些长连接或者耗时久的连接还是会很快耗尽线程池资源,服务端能力同样受限。
问题出在哪里?大家想想,我们的业务操作(内存操作)和网络IO操作,谁更耗时?
问题就在这里,通常情况下,网络IO 才是瓶颈点,一方面,网络的不确定性,等待网络数据包的就绪实际也具有不确定性,另一方面,read/write 等调用涉及与内核的交互,进程上下文的切换等都是开销。
总结来说,我们的线程大部分时机都在「等待」网络数据包的就绪,也就是说,在等待中,浪费了我们线程池中的线程资源。
如何解决?拆分
2.2 事件驱动模型
事件驱动的核心是将 连接
与 工作线程
分离,避免工作线程将过多的时间花在 I/O 等待上,以 事件
驱动工作线程,即,专人做专事(想想现代人的分工)
当有事件准备就绪时,再交给工作线程去处理,这样一来,我们的工作线程就节省了大部分的等待时间,全方位投入到实际工作中,彻底释放工作线程!!!
那负责处理连接的线程如何达到高性能?IO 多路复用
,通常情况下,一个线程就能达到要求!
3、Reactor 模型
前面我们提了「事件驱动」,是一种抽象、指导思想,而 Reactor 模型则是一种具体的实现,换句话说,Reactor模型是事件驱动的一种实现
。
Reactor 模式由 Reactor 线程、Handlers 处理器两大角色组成:
- Reactor 线程:主要负责连接建立、监听IO事件、IO事件读写以及将事件分发到Handlers 处理器
- Handlers 处理器:非阻塞的执行业务处理逻辑
3.1 单线程模型
当我们的业务操作、网络IO 两部分能力都非常高效时,我们用一个线程是不是就可以搞定?是的,这就是单线程模型。
图中,acceptor 负责建立连接,利用「IO多路复用」可以达到一个线程应对成千上万连接的能力。
值得一提的是,本文的主角 — redis
就是采用这种模式,从 redis 单机几万的 QPS 也可以看出这种模式同样不可小觑,关键还得看场景!
3.2 多线程模型
不是所有的业务操作都是基于内存的,比如我们的 web 应用,很多可能需要操作 MySQL
这样的数据库,耗时就增加了,几十、上百毫秒都是很正常的事。
这样情况下,如果还是单线程处理业务,那这块的能力将会直线下降:
- 我们可以考虑将业务这块的使用线程池
- 同时,acceptor 也采用分离的线程处理
acceptor 还是利用 IO 多路复用处理连接,当监听到有就绪的 IO 事件时,就交给业务线程池去处理,分工明确、各司其职。
3.3 主从多线程模型
说到底,还是要在「网络IO」「业务操作」两部分进行 PK,瓶颈在哪里,就优化哪里。
当我们的业务是纯内存操作时,那一般 「网络 IO 」就是瓶颈,怎么优化?还得搞多线程,只不过这里还有些讲究。
网络IO 涉及两部分:建立新连接、监听/处理已建立连接的IO事件,这两部分我们也可以分开处理:
- 建立新连接:这里叫主 reactor,负责建立新的连接,然后交给子 reactor 处理
- 监听/处理IO事件:也叫子 reactor,负责从建立的连接中监听IO事件,将就绪读取的事件交给业务线程池去处理。
网络这块说了这么多,相信你也清楚了,优化思路是:对于长的链路,先拆分、然后逐个击破
。
四、线程模型
你可能会经常听到这几个问题:
- redis 是单线程的?
- keys 这类命令要避免在线上使用?
- 删除一个百万级的 hash 字典有阻塞风险?
- redis6.0 已经有了多线程了,需要考虑并发问题吗?
通过这个模块,你会对这几个问题了然于胸~
1、单线程模型
redis 是单线程的,这个问题从某种层面来看是没有任何问题的,到目前为止(redis6.2),redis 的 命令执行仍然是单线程串行处理
。
Redis 是典型的事件驱动服务,内部有文件事件和时间事件两类:
- 文件事件:对外 - 处理用户请求,即 连接、I/O读写以及命令处理等
- 时间事件:对内 - 定时任务、各类指标/监控,比如内存驱逐、过期key、关闭超时客户端等
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 表示惰性释放空间
3、多线程模型
Redis 在6.0版本新增多线程实现,主要针对IO读、写使用多线程,命令执行仍然是单线程处理:
命令执行为什么不使用多线程?
- redis 性能主要在于网络和内存,而不是 CPU
- 另外,使用单线程处理命令可以避免并发控制问题。
总结
我们从一条请求出发,然后深入剖析其背后的原理,也清楚的看到了哪些关键点能影响到 redis 的高性能。
redis 是基于内存的,这是其 快
的核心原因,我们先介绍了 redis 的数据结构、类型以及一些典型的算法,透视 redis 对数据的组织、对内存的掌控以及对空间/时间的权衡。
然后进入网络模块,这是重中之重,是任何需要网络交互并且渴望拥有高性能的组件必须考虑的问题,redis 实现了自己的轻量级网络库,采用的是 单线程版
的 Reactor 模型。
最后是其线程模型,命令处理始终沿用单线程串行执行,随着版本迭代,引入了后台线程处理慢操作
,redis6.0 还提供了多线程来解决网络 IO 瓶颈。