思考并回答以下问题:
今天我们来探讨一下Redis高性能的原因。
这个问题在面试中还是很常见的,原因也很简单,除了Redis你基本上没有听过其他采用单线程模型的中间件,所以这就凸显了Redis的与众不同。
而且这个问题也很有现实意义。大部分时候对Redis的一些高级应用,比如前面提到的利用Redis实现一个分布式锁,其中有一个很重要的假设就是Redis是线程安全的,没有并发问题。而Redis是单线程的这一点就保证了线程安全的特性。
那么今天我就带你来看一下,Redis的高性能究竟是怎样做到的。
Redis是单线程的含义
你在学习Redis的时候,肯定听过一句话,Redis是单线程的。而实际上,Redis并不是单线程的。业界说Redis是单线程的,是指它在处理命令的时候,是单线程的。在Redis6.0之前,Redis的IO也是单线程的,但是在6.0之后也改成了多线程。
但是其他部分,比如说持久化、数据同步之类的功能,都是由别的线程来完成的。因此严格来说,Redis其实是多线程的。
面试准备
这一部分的面试内容基本上都是纯理论的,所以你需要做几件事情。
- 了解你使用的其他中间件,在IO上是否使用了epoll,以及是否使用了Reactor模式。
- 了解你们公司有没有使用Redis的多线程,如果用了,那么弄清楚最开始的决策理由以及相比单线程性能究竟提升了多少。
- 了解清楚你使用的Redis的性能瓶颈。
如果你用的Redis真的启用了多线程模式,你就可以将这一点纳入到你的性能优化方案中。有关Redis的线程模型面试是纯理论面试,所以你需要记忆的东西很多。有时间的话可以把Redis的源码下载下来,看看和网络IO处理有关的部分,加深印象。
当你和面试官聊到了这些话题的时候,你就可以用这节课的知识来回答。
- 网络IO问题。
- 其他也用epoll的中间件。
- 多线程的Memcache,Memcache用了多线程,但是Redis用了单线程。
- Redis的性能问题。
面试思路
一般来说,面试官都会问你“为什么Redis是单线程的,但是又能做到高性能?”很多人会下意识地回答:“因为Redis是完全内存操作的。”这个理由很关键,但是这并不是面试官想要的答案,他希望你回答的是Redis的IO模型。
所以要回答这个问题,你首先要澄清Redis单线程的含义。
我们通常说的Redis单线程,其实是指处理命令的时候Redis是单线程的。但是Redis的其他部分,比如说持久化其实是另外的线程在处理。因此本质上,Redis是多线程的。特别是Redis在6.0之后,连IO模型都改成了多线程的模型,进一步发挥了多核CPU的优势。
然后你先点明两个关键点:内存操作和高效IO模型。
Redis的高性能源自两方面,一方面是Redis处理命令的时候,都是纯内存操作。另外一方面,在Linux系统上Redis采用了epoll和Reactor结合的IO模型,非常高效。
这个时候他肯定就会问你,什么是epoll,什么是Reactor模式。
epoll模型
简单来说就是epoll会帮你管着一大堆的套接字。每次你需要做啥的时候,就问问哪些套接字可用。读数据,就是找出那些已经收到了数据的套接字;写数据,就是找出那些可以写入数据的套接字。
而在Linux系统里面,套接字就是一个普通的文件描述符,因此epoll本质上是管着一堆文件描述符。
你记住:epoll = CRUD文件描述符。
Redis使用的是epoll来处理IO。在Linux里面,万物都是文件,和网络IO有关的套接字也是文件。所以epoll要做的事情,就是管理这些文件描述符。或者用一句话来描述:epoll就是增删改查文件描述符。
你再介绍一下epoll的基本结构和系统调用。
epoll里面有两个关键结构。一个是红黑树,每一个节点都代表了一个文件描述符。另外一个是双向链表,也叫做就绪列表。
为了维护epoll的结构,有三个关键的系统调用。
1,epoll_create:也就是创建一个epoll结构
2,epoll_ctl:管理epoll里面的那些文件描述符,简单说就是增删改epoll里面的文件描述符。
3,epoll_wait:根据你的要求,返回符合条件的文件描述符,也就是查。
那么显然你可以猜到,如果你写一个从网络中读取数据的程序,看起来应该是这样的。1
2
3
4
5
6
7
8
9epoll = epoll_create();
// fd 是一个套接字对应的文件描述符,并且告诉它,你关心读事件
// 你可以加很多个
epoll_ctl(epoll, ADD, fd, READ)
while true {
// 你需要可读数据的套接字,等待时间是 1000 毫秒
fds = epoll_wait(epoll, READ, 1000)
// 一步步处理
}
你可以进一步补充epoll是怎么把文件描述符挪到就绪列表的。
需要注意的是,epoll并不是在我发起epoll调用的时候才把文件描述符挪到就绪列表的。而是在epoll创建之后,不管你有没有发起epoll_wait调用,只要文件描述符满足条件了,就会被挪到就绪列表。
发起epoll调用
因此,当你发起epoll_wait的时候,有两种情况。第一种情况是就绪列表里面有符合条件的套接字,直接给你。
第二种情况就是就绪列表里面没有符合条件的套接字,这时候传入不同的超时时间,会有不同的响应。记住关键词,-1永远阻塞,0立刻返回,正数等待直到超时。
如果在发起超时调用的时候,传入的超时时间是-1,那么调用者会被阻塞,直到有满足条件的文件描述符。如果传入的超时时间是0,那么会立刻返回,不管有没有满足条件的文件描述符。如果传入的是正数N,那么就会等待最多N毫秒,直到有数据或者超时。
亮点:epoll与中断
紧接着,你可以刷一个亮点,就是epoll怎么知道数据来了?又或者epoll怎么知道超时了?答案是中断,也就是你在操作系统基本原理里面学到的中断。
每一个和IO有关的文件描述符都有一个对应的驱动,这个驱动会告诉epoll发生了什么。比如说,当有数据发送到网卡的时候,会触发一个中断。借助这个中断,网卡的驱动会告诉epoll,这里有数据了。而超时也是利用了中断,不过是时钟中断。时钟中断之后,内核会去检查发起epoll_wait的线程有没有超时,如果超时了就会唤醒这个线程。调用者就会得到超时响应。
这部分内容我建议你主动提起,因为很少有面试官会在面试中追问到这里。而且,你可以将比较上层的应用原理和底层的中断机制关联在一起,能体现你计算机基础很扎实。
epoll、poll和select
在面试中,epoll、poll和select有时候会一起问。也就是让你分析这三种模型,并且解释三者的优劣。
前面你已经掌握了最难的epoll了,而poll和select比epoll简单多了。
我们先来看select。你发起select调用的时候会传给select一堆代表连接的文件描述符,内核会帮你检查这些文件描述符。
它和epoll的区别是,你必须要发起select调用,内核才会一个个帮你问。也就是说,select调用缺乏epoll那种即便你不调用epoll_wait,epoll也会把准备好的文件描述符放到就绪列表的机制。一句话来说,就是epoll会提前帮你准备好符合条件的文件描述符,但是select不会。
select用起来的伪代码就像这样:1
2
3
4readfds = [] // 一堆文件描述符,作为候选
writefds = [] // 也是一堆文件描述符,作为候选
execpfds = [] // 还是一堆文件描述符,作为候选
select(readfds, writefds, excepfds) // 从这些描述符里面挑出符合条件的
在select方法内部,内核会遍历你传入的这些候选文件描述符,找出你要的。poll和select的基本原理一样。
三者的区别我列了一个表格,你在面试的时候可以强调一下和性能有关的几个点。
在面试中你主要面epoll的细节,poll和select你大概提一下就可以。一般情况下你能解释清楚epoll,就能赢得竞争优势了。
在搞清楚了Redis使用的系统调用之后,还有一个面试的点,就是Redis使用的Reactor模式。
Reactor模式
Reactor模式也是广泛使用的IO模式,它的性能很好,Redis也用了Reactor模式。
用一句话来说明Reactor模式:一个分发器 + 一堆处理器。
一般来说,客户端和服务端的IO交互主要有两类事件:连接事件和读写事件。那么Reactor里面的分发器就是把连接事件交给Acceptor,把读写事件交给对应的Handler。这些Handler最终会调用到你真正需要读写数据的业务代码。
结合前面讲的epoll,你基本上就能猜到,Redis的Reactor就是调用了epoll,拿到创建连接的套接字,或者可读写的套接字,转发给后面的Acceptor或者Handler。
在搞清楚这一点之后,接下来你就能够理解各种Reactor的变种了。变种基本上可以分成三类。
- 把Accetor做成多线程。
- 把Handler做成多线程。
- 把Reactor做成多线程。Reactor的主线程只监听连接创建的事件,监听到了就交给其他线程处理。其他线程则是监听读写事件,然后调用对应的Handler处理。
Redis的特殊之处在于,Redis是单线程的。也就是说,Reactor、Handler、Acceptor都只是一个逻辑上的区分,实际上都是同一个线程。
所以当面试官问到的时候,你就把这两者结合在一起回答。
为了保证性能最好,Redis使用的是基于epoll的Reactor模式。
Reactor模式可以看成是一个分发器+一堆处理器。Reactor模式会发起epoll之类的系统调用,如果是读写事件,那么就交给Handler处理;如果是连接事件,就交给Acceptor处理。
然后强调一下单线程的Redis是怎么使用这个Reactor模式的。
Redis是单线程模型,所以Reactor、Handler和Acceptor其实都是这个线程。
整个过程是这样的:
1,Redis中的Reactor调用epoll,拿到符合条件的文件描述符。
2,假如说Redis拿到了可读写的描述符,就会执行对应的读写操作。
3,如果Redis拿到了创建连接的文件描述符,就会完成连接的初始化,然后准备监听这个连接上的读写事件。
后面在6.0的时候,Redis改成了多线程模型,但是基本原理还是Reactor+epoll。
最后,你提到了Redis的6.0新模型,那么面试官就可能会问你两个问题。
- 同样是基于内存的缓存中间件,为什么Memcache用的是多线程模型,而Redis用的是单线程模型?
- Redis为什么最终又引入了多线程模型?和原本的单线程模型比起来,区别在哪里?
亮点一:为什么Memcache使用多线程?
坦白来说,我个人认为这就是一个设计者的偏好问题,因为两者的优缺点都是很明显的。
你在回答的时候,也就是回答两者的优缺点,再随便补充一点个人理解就可以了。你可以先回答Redis使用单线程模式的原因。
Redis使用单线程模式的理由有很多。首先有两个显著的优点:不会引入上下文切换的开销,也没有多线程访问资源的竞争问题。其次Redis是一个内存数据库,操作很快,所以它的性能瓶颈只可能出现在网络IO和内存大小上,是不是多线程影响不大。最后,单线程模式比较好理解,调试起来也容易。
紧接着回答Memcache的设计。
Memcache采用了多线程设计,那么带来的后果就是会有线程上下文切换的开支,并且多线程模式下需要引入锁来保护共享资源。优点则是Memcache会比Redis更充分地利用多核CPU的性能。
毕竟我们都不是Redis的设计者,也难以说清楚究竟是什么因素促使设计者下定决心使用单线程模型。下面的这一段话是我的个人理解,你可以替换成你自己的思考。
我之前注意到有些人在网上发布了Redis和Memcache的性能对比。基本上就是有些时候Redis好一点,有些时候Memcache好一点。
我就觉得既然Redis用单线程模型都能取得这种不相上下的性能表现,就说明Redis的选择还是很正确的。即便如此,后面Redis在6.0的时候,还是引入了多线程模型,期望进一步利用多核CPU的优势。
亮点二:Redis为什么引入多线程?
引入多线程的原因只有一个,那就是性能。
Redis在6.0引入多线程的原因只有一个,在高并发场景下可以利用多个线程并发处理IO任务、命令解析和数据回写。这些线程也被叫做IO线程。默认情况下,多线程模式是被禁用了的,需要显式地开启。
当然要想答好这个问题,你不能只解释为什么引入多线程,还要结合Reactor和epoll来解释在启用了多线程模型之后,Redis是如何运作的。
如果刨除Redis主线程和IO线程之间的交互细节,那么多线程模式下的Redis的Reactor和epoll结合在一起的形式就是像图片里展示的这样。
你这样介绍整个设计。
当Redis启用了多线程之后,里面的主线程就要负责接收事件、创建连接、执行命令。Redis的IO线程就负责读写数据。
我用一个请求的处理过程来解释一下整个设计。当客户端发出请求的时候,主线程会收到一个可读的事件,于是它把对应的客户端丢到可读的客户端列表。一个IO线程会被安排读写这个客户端发过来的命令,并且解析好。紧接着主线程会执行IO线程解析好的命令,并且把响应放回到可写客户端列表里面。IO线程负责写回响应。整个过程就结束了。
所以整个Redis在多线程模式下,可以看作是单线程Reactor、单线程Acceptor和多线程Handler的Reactor模式。只不过Redis的主线程同时扮演了Reactor中分发事件的角色,也扮演了接收请求的角色。同时多线程Handler在Redis里面仅仅是读写数据,命令的执行还是依赖于主线程来进行的。
紧接着你要补充一个业界比较多人认同的观点,就是不到逼不得已不要启用Redis多线程模型。
虽然说现在Redis的IO改成多线程之后能够有效利用多核性能,但是大部分情况下都是不推荐使用多线程模式的。道理很简单,Redis在单线程模式下的性能就足以满足绝大多数使用场景了,那么用不用多线程已经无所谓了。
接下来,你根据你们公司的实际情况来选择一个回答。
第一个回答是介绍你们公司使用了多线程模型。
我司的Redis早期的时候就触及了单线程的性能瓶颈,后来在开启了多线程之后,能支撑的QPS大概提升了50%,效果还是很不错的。
你最好在自己公司里面测试一下性能提升的幅度。有些时候面试官可能会问你用了几个线程,你回答公司的实际情况就行。
另外一个回答是你们公司没有用多线程模型。
早期我们公司虽然遇到过Redis的性能瓶颈,但还是没有启用多线程模型,而是改成了使用RedisCluster(或者扩大了RedisCluster规模)。我个人认为,RedisCluster一样能够解决性能瓶颈问题,而且相比多线程模式,RedisCluster的可用性更好,解决性能问题的效果也更好。
面试思路总结
这一节课,我首先澄清了一个观点“Redis是单线程的”,实际上强调的是命令的执行过程是单线程的。
在面试过程中,Redis的高性能主要分成两个部分:epoll和Reactor模式。你要注意在回答的时候解释清楚Redis究竟是怎么把这两者结合在一起的。简单来说就是Reactor通过epoll这个调用知道发生了什么,然后分发给后面的Acceptor和Handler。
记住,在单线程里面,Reactor模式里面各个组件实际上都是同一个线程。那么相应的epoll结构、基本原理以及和中断的关系,你也需要记住。
最后,我们讨论了两个问题。
- 为什么Memcache使用多线程?虽然明面上你可能也认为就是设计者的偏好而已,但是你在回答的时候还是要分析单线程多线程的优劣,最后讲一下自己的体会。
- Redis为什么引入多线程?核心还是为了充分利用多核性能。要想在这个问题里面赢得优势,你需要说清楚在多线程模式下,Redis中的Reactor模式是如何运作的。
要强调的一点就是,今天这部分内容用于面试应该是够了,毕竟在面试的时候很少有面试官会要求你答出Redis的源码究竟是怎么实现的。不过我还是建议你如果有时间的话,去看看Redis的底层源码,这样会对这节课的内容有更深刻的理解。
思考题
- 我在Memcache里面吐槽说为什么它用多线程而Redis用单线程,可能就是设计者的偏好问题,你是如何看待这个问题的?
- 你认为在Redis遇到性能瓶颈的时候,是应该优先考虑使用多线程模式还是应该考虑RedisCluster增加新节点来解决?