3:高性能IO模型:为什么单线程Redis能那么快?
2021-04-27 本文已影响0人
_River_
1:Redis单线程的概念?
Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。
但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
2:Redis使用单线程和多线程对比?
先看一下多线程的作用:
在有合理的资源分配的情况下, 可以增加系统中处理同时多个请求操作的资源实体,
进而提升系统能够同时处理的请求数,即吞吐率。
但如果没有一个良好的设计,实际上会导致进一步新增线程时,系统吞吐率就增长缓慢了。
原因主要归结于:多线性对共同资源执行操作时,需要解决并发的问题,
比如说加锁,这也就导致了实际上多线程也会变成串行执行。
3:Redis使用单线程为什么这么快?
第一:Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
第二:就是 Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
以一个SimpleKV(简陋版本的Redis)为例子:
以 Get 请求为例,SimpleKV 为了处理一个 Get 请求,
1:需要监听客户端请求(bind/listen),
2:客户端建立连接(accept),
3:从 socket 中读取请求(recv),
4:解析客户端发送请求(parse),
5:根据请求类型读取键值数据(get),
6:最后给客户端返回结果,即向 socket 中写回数据(send)。
由于在这里的网络 IO 操作中,有潜在的阻塞点,分别是 accept() 和 recv()。
1:当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,
会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。
2:当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,
Redis 也会一直阻塞在 recv()。
4:网络IO操作的解决方案(Socket的NIO)
Socket 网络模型的非阻塞模式设置,主要体现在三个关键的函数调用上。
三个函数的调用返回类型和设置模式概念
在 socket 模型中,不同操作调用后会返回不同的套接字类型。
1:socket() 方法会返回主动套接字(生成主动套字节)
2:然调用 listen() 方法,将主动套接字转化为监听套接字,
此时,可以监听来自客户端的连接请求。
3:调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。
理解:
一个线程可以生成多个套接字 下面实际上只有套接字A 与套接字B
一个线程生成的套接字会经过 请求事件的转换 分别经历下面的阶段
主动套接字(A) 监听套接字(A) 已连接套接字(A)
主动套接字(B) 监听套接字(B) 已连接套接字(B)
针对监听套接字,我们可以设置非阻塞模式:
当 Redis 调用 accept() 但一直未有连接请求到达时,
Redis 线程可以返回处理其他操作,而不用一直等待。
特别注意:
调用 accept() 时,已经存在监听套接字了。
因为虽然 Redis 线程可以不用继续等待,
但是总得有机制继续在监听 监听套接字 等待后续连接请求并在有请求时通知 Redis。
调用 recv() 后,如果已连接套接字上一直没有数据到达,
Redis 线程同样可以返回处理其他操作。
我们也需要有机制继续监听该 已连接套接字 并在有数据达到时通知 Redis。
这样才能保证 Redis 线程,既不会像基本 IO 模型中一直在阻塞点等待,
也不会导致 Redis 无法处理实际到达的连接请求或数据。
5:基于多路复用的高性能 I/O 模型
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。
简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。
内核会一直监听这些套接字上的连接请求或数据请求。
一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
图中的多个 FD 就是刚才所说的多个套接字。
Redis 网络框架调用 epoll 机制,让内核监听这些套接字。(由于sokcet产生的套接字)
总点:
当这些套接字发生事件之后存入队列中 同时有可能该套接字的本身会变化类型
Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,
正因为此,可以同时和多个客户端连接并处理请求,从而提升并发性。
为了在请求到达时能通知到 Redis 线程(通过回调机制重新通知Redis)
select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
那么,回调机制是怎么工作的呢?
当select/epoll 监听到事件之后 这些事件会被放进一个事件队列;
Redis单线程 无需一直轮询是否有请求实际发生,只需要对该事件队列不断进行处理。
这就可以避免造成 CPU 资源浪费。
然后Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,
这就实现了基于事件的回调,能及时响应客户端请求,提升 Redis 的响应性能。
6:基于多路复用的高性能 I/O 模型 小总结
关键点在于accpet和recv时可能会阻塞线程
1:使用IO多路复用技术可以让线程先处理其他事情,等需要accpet资源或者recv资源到位
2:Redis运行在其内核中的epoll会去监听多个套接字
2:epoll会调用回调函数通知线程,然后线程再去处理存/取数据;
redis的单线程既生成socket(主动套接字) 又处理 事件处理队列
redis的内核使用 epoll_wait 来进行套接字的监听 当事件发生时把事件存入 事件处理队列
6: 简单介绍下select poll epoll的区别
select和poll本质上没啥区别,就是文件描述符数量的限制,select根据不同的系统,文件描述符限制为1024或者2048,poll没有数量限制。
他两都是把文件描述符集合保存在用户态,每次把集合传入内核态,内核态返回ready的文件描述符。
epoll是通过epoll_create和epoll_ctl和epoll_await三个系统调用完成的,
每当接入一个文件描述符,通过ctl添加到内核维护的红黑树中,通过事件机制,
当数据ready后,从红黑树移动到链表,通过await获取链表中准备好数据的fd,程序去处理。