前言
当别人问我们Redis这么快的时候,很多小白都只会简简单单的回答,因为Redis它是基于内存存储的,使用内存存储数据,可以避免频繁的进行写盘操作,大大降低响应时间。这个确实是一个原因,但回答的还是不够面。起码在这里还得回答上高效的数据结构以及IO网络多路复用的设计架构。
多路复用概述
Redis 采用网络 I/O 多路复用技术,要想理解IO多路复用的话,还得去理解下阻塞 I/O 模型,非阻塞 I/O 模型。
阻塞 I/O 模型
当用户线程发出 I/O 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态(block),用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。
非阻塞 I/O 模型
当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
用户线程需要不断地询问内核数据是否就绪,非阻塞 I/O 不会交出 CPU,而会一直占用 CPU,从而导致 CPU 占用率非常高。
多路复用
在多路复用 I/O 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 I/O 读写操作。因为在多路复用 I/O 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有 socket 读写事件进行时,才会使用 I/O 资源,所以它大大减少了资源占用(如 CPU)。
“多路”指的是多个网络连接客户端,“复用”指的是复用同一个线程(单进程)。
select / poll / epoll 就是三个内核提供给用户态的多路复用调用接口
select
select函数仅仅知道有几个I/O事件发生了,但并不知道具体是哪几个socket连接有I/O事件,还需要轮询去查找,时间复杂度为O(n),处理的请求数越多,所消耗的时间越长。
poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll
epoll可以理解为event pool,不同与select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多
select | poll | epoll | |
操作方式 | 遍历 | 遍历 | 回调 |
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024(x86)或者2048(x64) | 无上限 | 无上限 |
最大支持文件描述符数 | 有最大值限制 | 65535 | 65535 |
fd拷贝 | 每次调用,都需要把fd结合从用户态拷贝到内核态 | 每次调用,都需要把fd结合从用户态拷贝到内核态 | 只有首次调用的时候拷贝 |
工作效率 | 每次都要遍历所有文件描述符,时间复杂度O(n) | 每次都要遍历所有文件描述符,时间复杂度O(n) | 每次只用遍历需要遍历的文件描述符,时间复杂度O(1) |
为什么redis一定要部署在Linux机器上才能发挥出该性能?
因为只有linux有epoll函数,其它系统会自动降级成select函数。
redis为什么是单线程及为什么快的总结
1、Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
2、Redis使用的是非阻塞IO、IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
3、Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
4、Redis避免了多线程的锁的消耗。
5、Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。