Redis学习——高级篇①
- = = = = = = = = = Redis7高级之单线程和多线程(一) = = = = = = = = =
- 一、Redis单线程VS多线程
- 1.Redis的单线程部分
- 1.1 Redis为什么是单线程?
- 1.2 Redis所谓的“单线程”
- 1.3 Redis演进变化
- 1.3.1 Redis 3.x 单线程时代性能很快的原因
- 1.3.2 Redis 4.0 之前一直采用单线程的主要原因有三个
- 2. Redis单线程为什么加了多线程特性
- 3.Redis 6/7的多线程特性和IO多路复用入门篇
- 3.1 Redis的瓶颈初步定为 网络IO
- 3.1.1 Redis 6/7 真正的多线程
- 3.1.2 主线程和IO线程的四个阶段
- 3.2 Unix网络编程的五种IO模型
- Blocking IO 阻塞IO
- NoneBlocking IO 非阻塞IO
- IO multiplexing IO多路复用
- Signal driven IO 信号驱动IO
- Asynchronous IO 异步IO
- 3.3 Redis7开启多线程
- 4. IO多路复用
- 1、IO多路复用-select方式
- 2、IO多路复用模型-poll模式
- 3、IO多路复用模型-epoll函数
- 4、网络模型-epoll中的ET和LT
- 5、网络模型-基于epoll的服务器端流程
- 6、网络模型-Redis是单线程的吗?为什么使用单线程
在这章 小编没听懂,因为是非科班 但是我找到了一个小视频 大家可以看完笔记 不理解 看一下这个 小白也看得懂的 I/O 多路复用解析(超详细案例)
= = = = = = = = = Redis7高级之单线程和多线程(一) = = = = = = = = =
一、Redis单线程VS多线程
1.Redis的单线程部分
1.1 Redis为什么是单线程?
- Redis的版本很多3.x、4.x、6.x,版本不同架构也是不同的,不限定版本问是否单线程也不太严谨。
- 版本3.x ,最早版本,也就是大家口口相传的redis是单线程
- 版本4.x,严格意义来说也不是单线程,而是负责处理客户端请求的线程是单线程,但是开始加了点多线程的东西(异步删除)。
- 2020年5月版本的6.0.x后及2022年出的7.0版本后,告别了大家印象中的单线程,用一种全新的多线程来解决问题。
1.2 Redis所谓的“单线程”
主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。
Redis命令工作线程是单线程的,但是,对整个Redis来说,是多线程的;
1.3 Redis演进变化
1.3.1 Redis 3.x 单线程时代性能很快的原因
- 基于内存操作
- 所有Redis的数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能高
- 数据结构简单
- Redis的数据结构是专门设计的,这些简单的数据结构的查找和操作时间大部分复杂度都是O(1),性能高
- 多路复用和非阻塞IO
- Redis使用I/O多路复用功能来监听多个socket连接客户端,这样可以使用一个线程来处理多个请求,减少线程切换带来额开销,同时也避免了I/O阻塞操作
- 避免上下文切换
- 因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
1.3.2 Redis 4.0 之前一直采用单线程的主要原因有三个
- 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
- 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;
- 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。
2. Redis单线程为什么加了多线程特性
-
因为单线程有单线程的问题,比如我要删除一个比较大的key
del bigkey
会一直阻塞,等待删除完成,才能继续操作,会导致Redis主线程卡顿
-
所以引入了 惰性删除 可以有效避免Redis主线程卡顿
- 在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。
unlink key |
---|
flushdb async |
flushall async |
把删除工作交给了后台的小弟(子线程)异步来删除数据了。 |
-
因为Redis是单个主线程处理,redis之父antirez一直强调"Lazy Redis is better Redis".
- 而lazy free的本质就是把某些cost(主要时间复制度,占用主线程cpu时间片)较高删除操作,从redis主线程剥离让BIO子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
虽然引入了多个线程来实现数据的异步惰性删除等功能,但其处理读写请求的仍然只有一个线程,所以仍然算是狭义上的单线程
3.Redis 6/7的多线程特性和IO多路复用入门篇
对于Redis 主要的性能瓶颈是内存或者网络带宽 而并非CPU
听不懂是正常的,先耐心看下去
3.1 Redis的瓶颈初步定为 网络IO
3.1.1 Redis 6/7 真正的多线程
- 在Redis6/7中,非常受关注的第一个新特性就是多线程。
- 这是因为,Redis一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
- 随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络IO的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度
- 采用多个IO线程来处理网络请求,提高网络请求处理的并行度,Redis6/7就是采用的这种方法。
- 但是,Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制了(不管加锁操作处理),这样一来,Redis线程模型实现就简单了
- Redis 只是将 I/O 读写变成了多线程,而命令的执行依旧是由主线程串行执行的
3.1.2 主线程和IO线程的四个阶段
- 阶段一:服务端和客户端建立Socket连接,并分配处理线程
- 首先,主线程负责接收建立连接请求。当有客户端请求和实例建立Socket连接时,主线程会创建和客户端的连接,并把Socket放入全局等待队列中。紧接着,主线程通过轮询方法把Socket连接分配给IO线程。
- 阶段二: IO线程读取井解析请求
- 主线程一旦把Socket分配给I0线程,就会进入阻塞状态,等待IO线程完成客户端请求读取和解析。因为有多个线程在并行处理,所以,这个过程很快就可以完成。
- 阶段三:主线程执行请求操作
- 等到IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。
- 阶段四: IO线程回写Socket和主线程清空全局队列
- 当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待IO线程,把这些结果回写到Socket中,并返回给客户端。和I0线程读取和解析请求一样, IO线程回写Socke时,也是有多个线程在并发执行,所以回写Socket的速度也很快。等到0线程回写Socket完毕,主线程会清空全局队列,等待客户端的后续请求。
IO多路复用和Epoll函数的使用
一种同步的IO模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪就能够通知到对应应用程序进行相应的读写操作,没有文件句柄就绪时就会阻塞应用程序,从而释放CPU资源
- Redis6→7将网络数据读写、请求协议解析通过多个IO线程的来处理 ,对于真正的命令执行来说,仍然使用主线程操作
3.2 Unix网络编程的五种IO模型
在linux中一切都是文件
- 文件描述符(简称FD,句柄)
- 文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
- IO模型通过监视文件描述符来实现操作
Blocking IO 阻塞IO
- 使用系统调用,并一直阻塞直到内核将数据准备好,之后再由内核缓冲区复制到用户态,在等待内核准备的这段时间什么也干不了
- 下图函数调用期间,一直被阻塞,直到数据准备好且从内核复制到用户程序才返回,这种IO模型为阻塞式IO
- 阻塞式IO式最流行的IO模型
NoneBlocking IO 非阻塞IO
- 内核在没有准备好数据的时候会返回错误码,而调用程序不会休眠,而是不断轮询询问内核数据是否准备好
- 下图函数调用时,如果数据没有准备好,不像阻塞式IO那样一直被阻塞,而是返回一个错误码。数据准备好时,函数成功返回。
- 应用程序对这样一个非阻塞描述符循环调用成为轮询。
- 非阻塞式IO的轮询会耗费大量cpu,通常在专门提供某一功能的系统中才会使用。通过为套接字的描述符属性设置非阻塞式,可使用该功能
IO multiplexing IO多路复用
- IO
- 网络IO,尤其在操作系统层面指数据在内核态和用户态之间的读写操作
- 多路
- 多个客户端连接(连接就是套接字描述符,即socket 或者 channel)
- 复用
- 复用一个或几个线程
PS:套接字描述符是访问套接字的一种路径,套接字对唯一标识一个网络上的每个TCP连接。
IO多路复用: 也就是一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程
- 一个服务端进程可以同时处理多个套接字描述符
- 实现IO多路复用的模型有3种: select -> poll -> epoll 三个阶段
IO多路复用模型,简单明了版理解
将用户socket对应的文件描述符(FileDescriptor)注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、 epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
在单个线程通过记录跟踪每–个Socket(I/O流)的状态来同时管理多个I/O流.一个服务端进程可以同时处理多个套接字描述符。
-
目的是尽量多的提高服务器的吞吐能力。
-
大家都用过nginx, nginx使用epolI接收请求, ngnix会 有很多链接进来,epoll会把他们都监视起来, 然后像拨开关一样, 谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理,这就是IO多路复用原理,有请求就响应,没请求不打扰。
只使用一个服务端进程可以同时处理多个套接字描述符连接
客户端请求服务端时,实际就是在服务端的Socket文件中写入客户端对应的文件描述符FileDescriptor,如果有多个客户端同时请求服务端,为每次请求分配一个线程,类似每次来都new一个如此就会比较耗费服务端资源…因此,我们只使用一个线程来监听多个文件描述符,这就是IO多路复用
采用多路I/O复用技术可以让单个线程高效的处理多个连接请求一个服务端进程可以同时处理多个套接字描述符。
所以Redis为什么快
- IO多路复用+epoll函数使用,才是redis为什么这么快的直接原因,而不是仅仅单线程命令+redis安装在内存中。
Signal driven IO 信号驱动IO
首先开启套接字的信号驱动式IO功能,并且通过sigaction(信号处理程序) 系统调用安装一个信号处理函数 ,该函数调用将立即返回,当前进程没有被阻塞 ,继续工作;当数据报准备好的时候,内核为该进程产生SIGIO 的信号,随后既可以在信号处理函数中调用recvfrom 读取数据报,并且通知主循环数据已经准备好等待处理;也可以直接通知主循环让它读取数据报;(其实就是一个待读取的通知和待处理的通知),基本不会用到。
Asynchronous IO 异步IO
线程和多进程的模型虽然解决了并发的问题,但是系统不能无限的增加线程,由于系统的切换线程的开销恒大,所以,一旦线程数量过多,CPU的时间就花在线程的切换上,正真运行代码的时间就会减少,结果导致性能严重下降
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一个问题的一种方法。
- 另一种解决IO问题的方法是异步IO,当代码需要执行一个耗时的IO操作时,他只发出IO指令,并不等待IO结果然后就去执行其他代码,一段时间后,当IO返回结果是,在通知CPU进行处理
我们调用aio_read函数,给内核传递描述符,缓冲区指针,缓冲区大小,和文件偏移量,并且告诉内核当整个操作完成时如何通知我们,该函数调用后,立即返回,不会被阻塞 - 另一方面:从kernel的角度,当他收到一个aio_read之后,首先它立即返回,所以不会对用户进程产生block,然后kernel会等待数据准备完成,然后将数据拷贝到用户内存(copy由内核完成),当着一切完成后,kernel会给用户进程发送一个singal或者执行下一个基于线程回调函数来完成此次IO处理过程,告诉他read操作完成
3.3 Redis7开启多线程
在单机模式下,可以开启多线程,但是在其他模式,最好不开启
Redis实例的 CPU开销不大但吞吐量却没有提升,可以考虑使用Redis7的多线程机制,加速网络处理,进而提升实例的吞吐量
- 注意线程数
- 官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
- 设置io-thread-do-reads配置项为yes,表示启动多线程。
最后用一幅图,来说明他们之间的区别
4. IO多路复用
1、IO多路复用-select方式
select是Linux最早是由的I/O多路复用技术:
简单说,就是我们把需要处理的数据封装成FD,然后在用户态时创建一个fd的集合(这个集合的大小是要监听的那个FD的最大值+1,但是大小整体是有限制的 ),这个集合的长度大小是有限制的,同时在这个集合中,标明出来我们要控制哪些数据
|
|
|
|
比如要监听的数据,是1,2,5三个数据,此时会执行select函数,然后将整个fd发给内核态,内核态会去遍历用户态传递过来的数据,如果发现这里边都数据都没有就绪,就休眠,直到有数据准备好时,就会被唤醒,唤醒之后,再次遍历一遍,看看谁准备好了,然后再将处理掉没有准备好的数据,最后再将这个FD集合写回到用户态中去,此时用户态就知道了,奥,有人准备好了,但是对于用户态而言,并不知道谁处理好了,所以吧 用户态也需要去进行遍历,然后找到对应准备好数据的节点,再去发起读请求,我们会发现,这种模式下他虽然比阻塞IO和非阻塞IO好,但是依然有些麻烦的事情, 比如说频繁的传递fd集合,频繁的去遍历FD等问题
2、IO多路复用模型-poll模式
poll模式对select模式做了简单改进,但性能提升不明显,部分关键代码如下:
IO流程:
- 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
- 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
- 内核遍历fd,判断是否就绪
- 数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
- 用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd
与select对比:
- select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
- 监听FD越多,每次遍历消耗时间也越久,性能反而会下降
2个问题
- 一个就是说每次调用的时候需要将fd从用户态拷贝到内核态,这个会涉及到一个用户态内容它的切换。当fd大,需要开销大
- poll跟这个select的其实是不知道具体是哪个f d有事件就绪的。他们需要去进行一个O(n)的遍历,就遍历所有传入的这个f d就是说去找到那个呃具体发生事件的那个fd 开销大!
所以epoll函数来辽
3、IO多路复用模型-epoll函数
|
|
|
|
|
|
epoll模式是对select和poll的改进,它提供了三个函数:
第一个是:eventpoll的函数,他内部包含两个东西
一个是:
1、红黑树-> 记录的事要监听的FD
2、一个是链表->一个链表,记录的是就绪的FD
紧接着调用epoll_ctl操作,将要监听的数据添加到红黑树上去,并且给每个fd设置一个监听函数,这个函数会在fd数据就绪时触发,就是准备好了,现在就把fd把数据添加到list_head中去
3、调用epoll_wait函数
就去等待,在用户态创建一个空的events 数组,当就绪之后,我们的回调函数会把数据添加到list_head中去,当调用这个函数的时候,会去检查list_head,当然这个过程需要参考配置的等待时间,可以等一定时间,也可以一直等, 如果在此过程中,检查到了list_head中有数据会将数据添加到链表中,此时将数据放入到events数组中,并且返回对应的操作的数量,用户态的此时收到响应后,从events中拿到对应准备好的数据的节点,再去调用方法去拿数据。
小总结:
select模式存在的三个问题:
- 能监听的FD最大不超过1024
- 每次select都需要把所有要监听的FD都拷贝到内核空间
- 每次都要遍历所有FD来判断就绪状态
poll模式的问题:
- poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式中如何解决这些问题的?
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
- 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
- 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
4、网络模型-epoll中的ET和LT
当FD有数据可读时,我们调用epoll_wait(或者select、poll)可以得到通知。但是事件通知的模式有两种:
- LevelTriggered:简称LT,也叫做水平触发。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。
- EdgeTriggered:简称ET,也叫做边沿触发。只有在某个FD有状态变化时,调用epoll_wait才会被通知。
举个栗子:
- 假设一个客户端socket对应的FD已经注册到了epoll实例中
- 客户端socket发送了2kb的数据
- 服务端调用epoll_wait,得到通知说FD就绪
- 服务端从FD读取了1kb数据回到步骤3(再次调用epoll_wait,形成循环)
结论
如果我们采用LT模式,因为FD中仍有1kb数据,则第⑤步依然会返回结果,并且得到通知
如果我们采用ET模式,因为第③步已经消费了FD可读事件,第⑤步FD状态没有变化,因此epoll_wait不会返回,数据无法读取,客户端响应超时。
5、网络模型-基于epoll的服务器端流程
我们来梳理一下这张图
服务器启动以后,服务端会去调用epoll_create,创建一个epoll实例,epoll实例中包含两个数据
1、红黑树(为空):rb_root 用来去记录需要被监听的FD
2、链表(为空):list_head,用来存放已经就绪的FD
创建好了之后,会去调用epoll_ctl函数,此函数会会将需要监听的数据添加到rb_root中去,并且对当前这些存在于红黑树的节点设置回调函数,当这些被监听的数据一旦准备完成,就会被调用,而调用的结果就是将红黑树的fd添加到list_head中去(但是此时并没有完成)
3、当第二步完成后,就会调用epoll_wait函数,这个函数会去校验是否有数据准备完毕(因为数据一旦准备就绪,就会被回调函数添加到list_head中),在等待了一段时间后(可以进行配置),如果等够了超时时间,则返回没有数据,如果有,则进一步判断当前是什么事件,如果是建立连接时间,则调用accept() 接受客户端socket,拿到建立连接的socket,然后建立起来连接,如果是其他事件,则把数据进行写出
6、网络模型-Redis是单线程的吗?为什么使用单线程
Redis到底是单线程还是多线程?
- 如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程
- 如果是聊整个Redis,那么答案就是多线程
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
- Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink
- Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。
为什么Redis要选择单线程?
- 抛开持久化不谈,Redis是纯 内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
- 多线程会导致过多的上下文切换,带来不必要的开销
- 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣