1)性能瓶颈
首先明确Redis的性能瓶颈是在网络IO而不是CPU,因为它的操作都是基于内存的,受限程度很小,而命令读取和回复的传输是通过网络IO的方式,这里的受限程度会比较大。
2)单线程OR多线程
Redis在执行指令时是单线程的,但并不意味着它没有多线程的应用,这部分主要体现在过期键清除、持久化、网络IO(Redis6之后)等方面,因此常说的单线程是个“局部概念”。
要解释为什么是单线程,不妨看看多线程会有什么问题?
① 线程上下文切换的开销,多线程就必定会有线程上下文的切换,就必定有产生一定的开销;
② 同步开销,对于数据库来说,多线程意味着在操作时难免会进行同步,这一同步就需要引入锁等同步机制,这样一来反而得不偿失了;
③ 线程安全问题,Redis中可不止简单的k-v数据,还有众多的数据结构存储数据,如果引入多线程,那么这些底层的数据结构就必须设计成线程安全的,这又会拉低一定的性能值;
为啥命令执行是单线程,还那么快?
①其采用内存存储,读写速度快;
②使用上了高效的数据结构,例如跳表、字典、快表等;
③高效的线程IO模型,IO多路复用;
3)IO模型
Redis中使用的是非阻塞的多路复用IO模型,底层基于Reactor模式的epoll实现,首先看一下epoll是什么东西。
① 单Socket实现
在OS中,网络上的两个实体可以通过Socket进行连接,这样的连接是最简单的实现,来一个请求就给创建一个Socket,这样的缺点也是显而易见的,假设上一个Socket任务还没执行完,那么后面来的任务都需要阻塞住,会造成很多请求在排队的情况,因此不能采用这种Socket与实体连接一一对应的方式。
② 多Socket实现
接着考虑基于多进程/多线程的IO模型,对于一个请求就给分配一个进程/线程去处理,之后由各自的进程/线程去与请求对接完成读写操作,这样做的好处是他们可以并行/并发执行了。
对于进程来说,每次分配一个进程去处理都需要从主线程fork出去,这期间需要进行多数据的拷贝,这带来的开销是巨大的。
对于线程来说,由于是并发进行的,期间可能会有共享数据的读写,这就意味着必须使用锁来保证线程安全,其次线程上下文切换的成本也不小。
③ IO多路复用
既然多线程也不行,那么就只能从IO身上动手脚了。这时候就有了IO多路复用模型的概念了,简单来说,就是类似于CPU调度的时分复用,对于一个IO操作保证执行时间短一些,每S处理的请求多一些,接着循环操作,直到完成,时间刻度拉长了看这就是多个请求在一个进程的复用。实现IO多路复用的有三个方法,select/poll 和 epoll。
关于select:
select 的做法是将Socket存在一个集合中,调用select的时候会将集合拷贝一份到内核中,由内核对集合进行顺序遍历后标记出可读/写的Socket,之后再从内核拷贝回用户态,用户态再去遍历集合,找到可读/写的Socket进而处理。【整个过程发生两次拷贝、两次遍历】
关于poll:
poll和select的做法类似,不过是将Socket存入一个动态数组(链表)中,唯一的好处就是可以突破select对于Socket个数的限制,但依旧会受到系统上限的限制。其他操作基本同select。
关于epoll:
epoll的做法是将需要监控(感兴趣)的Socket存入内核中的红黑树,之后一旦有事件发生就在红黑树中找到那个Socket,后将其加入就绪事件链表,待用户调用epoll_wait的时候只会将链表(这些已经可读/写的事件)返回给用户态,而不是全量的遍历,这就大大提升了效率。
此外epoll还支持边缘触发和水平触发两种方式,边缘触发下当有事件发生时服务端只会从epoll_wait苏醒一次,所以我们必须一次性将内容读出;水平触发下当有事件发生时服务端会不断从epoll_wait苏醒,直到内核缓冲区的数据被读取完毕。边缘触发搭配非阻塞IO使用效率会比较高。