聊聊redis单线程为何能作到高性能和io多路复用究竟是个什么鬼

1:io多路复用epoll 
io多路复用简单来讲就是一个线程处理多个网络请求。
咱们知道epoll in 的事件触发是可读了,这个比较好理解,好比一个链接过来,或者一个数据发送过来了,那么in事件就触发了,那么out事件是如何触发的呢?缓冲区可写(有空的区域),就能够触发,epoll有两种模式LT(水平触发)和ET(边缘触发),LT模式下,主要缓冲区数据一次没有处理完,那么下次epoll_wait返回时,还会返回这个句柄;而ET模式下,缓冲区数据处理一次就结束,下次是不会再通知了,只在第一次返回.因此在ET模式下,通常是经过while循环,一次性读彻底部数据.epoll默认使用的是LT。
socket的缓冲区已经满了,此时没法继续send。此时异步程序的正确处理流程是调用epoll_wait,当socket缓冲区中的数据被对方接收以后,缓冲区就会有空闲空间能够继续往里面写数据,此时epoll_wait就会返回这个socket的EPOLLOUT事件,得到这个事件时,你就能够继续往socket中写出数据。
redis的epoll使用的是默认的LT模式,只要写缓冲区可写时,就会不断的触发可写事件,为了不一直触发可写事件,redis是在有数据可写的时候注册写事件,写完以后就取消写事件的注册
epoll内部数据结构为红黑树和链表,红黑树保存了全部socket和监听的事件信息,链表保存的是就绪的socket信息,就是那些就绪socket已经帮你整理好了。
那么,这个准备就绪list链表是怎么维护的呢?当咱们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上以外,还会给内核中断处理程序注册一个回调函数,告诉内核,若是这个句柄的中断到了,就把它放到准备就绪list链表里。因此,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少许的内核cache,就帮咱们解决了大并发下的socket处理问题。执行epoll_create时,建立了红黑树和就绪链表,执行epoll_ctl时,若是增长socket句柄,则检查在红黑树中是否存在,存在当即返回,不存在则添加到树干上,而后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时马上返回准备就绪链表里的数据便可。
 
2:读写事件的注册与删除
当一个新的链接创建后,redis会建立一个redisClient对象,而后为这个socket向epoll注册一个读事件,直到RedisClient对象销毁时才删除读事件,当redis读到一个完整的命令并解析完成后,就会为socket向epoll注册写事件,将回复信息发给client以后,就会从epoll删除刚注册的写事件,下个命令来了以后又会重复这个增删写事件的动做。
因此每一个socket向epoll注册销毁一次读事件,屡次注册销毁写事件,这样作的目的:在我没什么可写的状况下你就别叫我写了,我知道何时可写 
 
3:redis单线程是怎么作到高性能的呢?
之前我一直在想一个问题:若是一个redis命令很长,redis接收处理这个命令就要100毫秒,那么别的命令会不会延迟100毫秒呢?后续命令处理会不会像消息队列同样积压呢?
答案:不会。
上面咱们已经说了epoll的原理,它不是让咱们一次处理完一个命令后,再去处理另外一个命令,epoll是帮咱们一次接收多个命令的部分数据(若是命令很短则是完整的数据),每一个socket都有一个缓冲区,写满了就不能写了,须要读出来后才能继续往里面写,redis为每一个client分配了一个变长缓冲区,从socket中读出后存在缓冲区中,当接收到一个完整的命令,就解析并执行这个命令,而后把缓冲区后面的数据往前移动,反复利用这块内存,当这块内存超过必定值后就会释放,在须要的时候从新分配一块内存
也就是说epoll的水平触发模式将一个较长的命令请求分红了屡次接收,一次能接收多个命令的请求,天生就只支持高并发的,加上redis会将耗时的命令会分屡次处理,保证了咱们的读写操做都很快。
综述单线程高性能的缘由:
  • 1:纯内存操做原本就很快
  • 2:redis使用epoll支持io多路复用,天生支持高并发请求
  • 3:redis将耗时的操做分屡次处理,保证每次处理的时间都很短,保证了读写性能,若是数据很长的话处理时间就会变长,因此redis不建议保存太长的数据
还有redis6.0实现了多线程的功能,性能至少翻倍,那你还要问题单线程为何性能高吗?并且仍是在数据的接收解析和数据的发送使用多线程的状况下,性能就至少翻倍了。多是为了保证代码的简洁性,做者不肯意使用多线程,为了提高性能用了多线程,也是部分功能使用多线程,操做redis数据库的逻辑仍是单线程,若是数据是写少读多的状况下,采用多线程读写锁性能会不会提高不少呢?
因此redis一开始采用单线程的缘由:
  • 1:代码简洁又简单 
  • 2:性能已经很好了
  • 3:性能不够我再搞多线程吗
 
4:redis单线程是怎么同时处理文件事件和时间事件
文件事件主要是网络I/O的读写,请求的接收和回复。时间事件就是单次/屡次执行的定时器,如主从复制、定时删除过时数据、字典rehash等。
redis全部核心功能都是跑在主线程中的,像aof文件落盘操做是在子线程中执行的,那么在高并发状况下它是怎么作到高性能的呢?
因为这两种事件在同一个线程中执行,就会出现互相影响的问题,如时间事件到了还在等待/执行文件事件,或者文件事件已经就绪却在执行时间事件,这就是单线程的缺点,因此在实现上要将这些影响降到最低。那么redis是怎么实现的呢?
定时执行的时间事件保存在一个链表中,因为链表中任务没有按照执行时间排序,因此每次须要扫描单链表,找到最近须要执行的任务,时间复杂度是O(N),redis敢这么实现就是由于这个链表很短,大部分定时任务都是在serverCron方法中被调用。从如今开始到最近须要执行的任务的开始时间,时长定位T,这段时间就是属于文件事件的处理时间,以epoll为例,执行epoll_wait最多等待的时长为T,若是有就绪任务epoll会返回全部就绪的网络任务,存在一个数组中,这时咱们知道了全部就绪的socket和对应的事件(读、写、错误、挂断),而后就能够接收数据,解析,执行对应的命令函数。
若是最近要执行的定时任务时间已通过了,那么epoll就不会阻塞,直接返回已经就绪的网络事件,即不等待。
总之单线程,定时事件和网络事件仍是会互相影响的,正在处理定时事件网络任务来了,正在处理网络事件定时任务的时间到了。因此redis必须保证每一个任务的处理时间不能太长。