系列文章目录
Redis总结(1)——基本介绍
Redis总结(2)——Java客户端,Jedis与Lettuce
Redis总结(3)——基本原理,线程与I/O
前言
在生产中我们选择使用Redis通常都是因为其优异的性能,天下武功,唯快不破。那么Redis快的原因是什么,最明显的答案是它是基于内存实现的,但是这不是全部答案,其背后还有数据模型、线程模型和I/O模型的支撑才能使得Redis能达到10w级别的QPS。数据结构涉及到底层代码,暂不介绍,下面着重看线程模型和I/O模型。一、单线程模型
一般认为多线程能够提升CPU的利用率从而提升程序的效率,但是对于Redis这种基于内存且I/O密集型的应用而言,CPU并不是它的效率瓶颈,反而是内存和网络I/O决定了它的效率,因此没有必要使用多线程模型。
于此同时,多线程模型在以下几个地方反而会有增加额外的开销:
- CPU的上下文切换,CPU在切换线程时需要保存上一个线程任务的状态,同时要加载当前线程需要处理的任务状态,这个过程非常消耗资源。
- 多线程并行时为了保证正确性,必须对共享的数据进行加锁(线程安全),锁的添加、释放、竞争也一定会影响性能。
- 线程本身的创建和销毁也会消耗资源
因此对于Redis多线程可能会适得其反,而单线程反而有便于开发维护的优点,选择单线程模型就顺理成章了。
此处需要额外强调一点的是,Redis的单线程模型是指其网络IO和键值对读写是由一个线程完成的,而它的持久化、集群数据同步、异步删除等都是其他线程执行的,所以整个Redis服务还是多线程的。而在最新的Redis6.0中网络请求过程也是多线程了。
二、多路复用I/O
虽然Redis网络IO和键值对读写用的是简单的单线程模型,但是其I/O模型却不那么简单。在说多路复用之前,我们先看一下服务端处理一个客户端的请求有哪几个过程:
- 与客户端建立连接
- 从连接(Socket)中读取请求
- 处理请求,执行相应的Redis指令
- 发送结果,即向Socket写回结果
在高并发的场景下,可能会有多个客户端同时发送请求,对于单线程的Redis来说,最简单的处理方式就是依次执行每一个请求,即对一个请求完成上面的所有步骤,再处理下一个客户端请求,这也就是单线程的阻塞I/O模型。但是这个模型的效率很低,原因在于步骤1和2会阻塞,这两步依赖于网络I/O,延时很高。当 Redis 监听到一个客户端想要连接时,它需要等待连接的建立,需要等到数据全部读取完成,才能开始执行命令,而执行命令的过程反而很快,大量的资源被浪费在等待中。
如何提高高并发场景下的处理效率呢?显然多线程模型可以解决这个问题,每一个请求都创建一个线程,但是多线程由于自身的弊端(见上一节),Redis没有使用,转而使用另一种I/O技术即多路复用,Redis的多路复用模型如下:
模型分为多路复用程序,文件事件分派器,文件事件处理器三个部分。多路复用程序会监听Socket,将准备好的Socket产生的文件事件放入一个消息队列中,然后以每次一个的方式向文件事件分派器发送Socket,文件事件分派器根据事件类型选择相应的处理器,当这个Socket处理完成后多路复用程序会发送下一个。这样的模式实现了多路Socket复用一个处理线程,故名多路复用。
打个比方,一个餐厅只有一个服务员,但是顾客有很多,如果服务员从顾客进店、点菜、下单都一直跟随服务这个顾客,直到顾客离开,然后再服务下一位顾客,那么效率就会很低。但是换一种方式,当顾客需要服务员服务时才呼叫服务员,那么效率就高多了。Redis就是这个服务员,前者就是阻塞模型,后者就是多路复用。当然上面只是对多路复用做了一个简单的讲解,想要深入的理解,需要网络编程相关知识以及阅读Redis源码。