redis单线程模型

1. 理解单线程模型

  Redis基于Reactor模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Hanlder)。文件事件处理器由SocketIO多路复用程序、文件事件分派器(dispather),事件处理器(handler)四部分组成。IO多路复用程序会同时监听多个socket,当被监听的socket准备好执行acceptreadwriteclose等操作时,与这些操作相对应的文件事件就会产生。IO多路复用程序会把所有产生事件的socket压入一个队列中,然后有序地每次仅一个socket的方式传送给文件事件分派器,文件事件分派器接收到socket之后会根据socket产生的事件类型调用对应的事件处理器进行处理。

  文件事件处理器分为几种:

  • 连接应答处理器:用于处理客户端的连接请求;
  • 命令请求处理器:用于执行客户端传递过来的命令,比如常见的setlpush等;
  • 命令回复处理器:用于返回客户端命令的执行结果,比如setget等命令的结果;

事件种类:

  • AE_READABLE:与两个事件处理器结合使用。
    • 当客户端连接服务器端时,服务器端会将连接应答处理器socketAE_READABLE事件关联起来;
    • 当客户端向服务端发送命令的时候,服务器端将命令请求处理器AE_READABLE事件关联起来;
  • AE_WRITABLE:当服务端有数据需要回传给客户端时,服务端将命令回复处理器与socketAE_WRITABLE事件关联起来。
Redis的客户端与服务端的交互过程:

2. 为什么redis使用单线程模型还能保证高性能?

(1) 纯内存访问

redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。

(2) 非阻塞式IO

  • 什么是阻塞式 IO

当我们调用 Scoket 的读写方法,默认它们是阻塞的。

read() 方法要传递进去一个参数 n,表示读取这么多字节后再返回,如果没有读够 n 字节线程就会阻塞,直到新的数据到来或者连接关闭了, read 方法才可以返回,线程才能继续处理。

write() 方法会首先把数据写到系统内核为 Scoket 分配的写缓冲区中,当写缓存区满溢,即写缓存区中的数据还没有写入到磁盘,就有新的数据要写道写缓存区时,write() 方法就会阻塞,直到写缓存区中有空闲空间。

  • 什么是非阻塞式 IO

非阻塞 IO 在 Scoket 对象上提供了一个选项Non_Blocking ,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。

能读多少取决于内核为 Scoket 分配的读缓冲区的大小,能写多少取决于内核为 Scoket 分配的写缓冲区的剩余空间大小。读方法和写方法都会通过返回值来告知程序实际读写了多少字节数据。

有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。

(3) IO多路复用

文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

  多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

   IO复用只需要阻塞在selectpoll或者epoll,可以同时处理和管理多个连接。缺点是当selectpoll或者epoll 管理的连接数过少时,这种模型将退化成阻塞IO 模型。并且还多了一次系统调用:一次selectpoll或者epoll 一次recvfrom

select、poll、epoll 区别:
  最大连接数 FD剧增后带来的IO效率问题 消息传递方式
select 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 内核需要将消息传递到用户空间,都需要内核拷贝动作
poll 基于链表来存储的,没有最大连接数的限制 同上 内核需要将消息传递到用户空间,都需要内核拷贝动作
epoll 接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销

(4)单线程避免了线程切换和竞态产生的消耗。

单线程能带来几个好处:

  • 第一,单线程可以简化数据结构和算法的实现。并发数据结构实现不但困难而且开发测试比较麻
  • 第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
  • 单线程的问题:对于每个命令的执行时间是有要求的。如果 某个命令执行过长,会造成其他命令的阻塞,所以 redis 适用于那些需要快速执行的场景。

3.epoll模型

  epoll是Linux提供的系统实现,核心方法只有三个epoll_create、epollctl、epollwait。epoll效率高,是因为基于红黑树、双向链表、事件回调机制。

  epoll_create

epoll_create(int size)
核心功能:
1.创建一个epoll文件描述符
2.创建eventpoll,其中包含红黑树cache和双向链表

  参数size并不是限制了epoll所能监听的文件描述符最大个数,只是对内核初始分配内部数据结构的一个建议。在Linux 2.6.8后,size 参数被忽略,但是必须传一个比 0 大的数。调用epoll_create后,会占用一个fd值。在Linux下可以查看/proc/$$/fd/ 文件描述符。使用完,需要调用close关闭。

  epollctl

int epollctl(int epfd, int op, int fd, struct epollevent *event);
核心功能:
1.对指定描述符fd执行op的绑定操作
2.把fd写入红黑树,同时在内核注册回调函数

  op操作类型,用三个宏EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD,来分别表示增删改对fd的监听。

  epollwait

int epollwait(int epfd, struct epollevent *events, int maxevents, int timeout);
核心功能:
1.获取epfd上的io事件

  参数events是就绪事件,用来得到想要获得的事件集合。maxevents表示的events有多大,maxevents的值必须大于0,参数timeout是超时时间。epollwait会阻塞,直到一个文件描述符触发了事件,或者被一个信号处理函数打断,或者timeout超时。返回值是需要处理的fd数量。

  执行流程

 

  优点

  • epoll创建的红黑树保存所有fd,没有大小限制,且增删查的复杂度O(logN)
  • 基于callback,利用系统内核触发感兴趣的事件
  • 就绪列表为双线链表时间复杂度O(1)
  • 应用获取到的fd都是真实发生IO的fd,与select 和 poll 需要不断轮询判断是否可用相比,能避免无用的内存拷贝

 

参考:https://www.cnblogs.com/reecelin/p/13538382.html

   https://blog.csdn.net/klarclm/article/details/8828486

   https://blog.csdn.net/m0_48071146/article/details/106319454

   

posted @ 2021-01-12 17:27  鄙人取个名字好难  阅读(306)  评论(0编辑  收藏  举报