我们一直说Redis的性能很快,那为什么快?Redis为了达到性能最大化,做了哪些方面的优化呢?在深度解析Redis的数据结构这篇文章中,其实从数据结构上分析了Redis性能高的一方面原因。
在目前的k-v数据库的技术选型中,Redis几乎是首选的用来实现高性能缓存的方案,它的性能有多快呢?
根据官方的基准测试数据,一台普通硬件配置的Linux机器上运行单个Redis实例,处理简单命令(O(n)或者O(logn)),QPS可以达到8W,如果使用pipeline批处理功能,QPS最高可以达到10W。
Redis为什么那么快
Redis的高性能主要依赖于几个方面。
C语言实现,C语言在一定程度上还是比Java语言性能要高一些,因为C语言不需要经过JVM进行翻译。
纯内存I/O,内存I/O比磁盘I/O性能更快
I/O多路复用,基于epoll的I/O多路复用技术,实现高吞吐网络I/O
单线程模型,单线程无法利用到多核CPU,但是在Redis中,性能瓶颈并不是在计算上,而是在I/O能力,所以单线程能够满足高并发的要求。从另一个层面来说,单线程可以避免多线程的频繁上下文切换以及同步锁机制带来的性能开销。
下面我们分别从上述几个方面进行展开说明,先来看网络I/O的多路复用模型。
从请求处理开始分析
当我们在客户端向RedisServer发送一条指令,并且得到Redis回复的整个过程中,Redis做了什么呢?
图4-1
要处理命令,则redis必须完整地接收客户端的请求,并将命令解析出来,再将结果读出来,通过网络回写到客户端。整个工序分为以下几个部分:
接收,通过TCP接收到命令,可能会历经多次TCP包、ack、IO操作
解析,将命令取出来
执行,到对应的地方将value读出来
返回,将value通过TCP返回给客户端,如果value较大,则IO负荷会更重
其中解析和执行是纯cpu/内存操作,而接收和返回主要是IO操作,首先我们先来看通信的过程。
网络IO的通信原理
同样,我也画了一幅图来描述网络数据的传输流程
首先,对于TCP通信来说,每个TCPSocket的内核中都有一个发送缓冲区和一个接收缓冲区
接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区。
read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。
进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端。
网卡中的缓冲区既不属于内核空间,也不属于用户空间。它属于硬件缓冲,允许网卡与操作系统之间有个缓冲;内核缓冲区在内核空间,在内存中,用于内核程序,做为读自或写往硬件的数据缓冲区;用户缓冲区在用户空间,在内存中,用于用户程序,做为读自或写往硬件的数据缓冲区
网卡芯片收到网络数据会以中断的方式通知CPU,我有数据了,存在我的硬件缓冲里了,来读我啊。CPU收到这个中断信号后,会调用相应的驱动接口函数从网卡的硬件缓冲里把数据读到内核缓冲区,正常情况下会向上传递给TCP/IP模块一层一层的处理。
NIO多路复用机制
Redis的通信采用的是多路复用机制,什么是多路复用机制呢?
由于Redis是C语言实现,为了简化大家的理解,我们采用Java语言来描述这个过程。
在理解多路复用之前,我们先来了解一下BIO。
BIO模型
在Java中,如果要实现网络通信,我们会采用Socket套接字来完成。
Socket这不是一个协议,而是一个通信模型。其实它最初是BSD发明的,主要用来一台电脑的两个进程间通信,然后把它用到了两台电脑的进程间通信。所以,可以把它简单理解为进程间通信,不是什么高级的东西。主要做的事情不就是:
A发包:发请求包给某个已经绑定的端口(所以我们经常会访问这样的地址.13.15.16:,就是端口);收到B的允许;然后正式发送;发送完了,告诉B要断开链接;收到断开允许,马上断开,然后发送已经断开信息给B。
B收包:绑定端口和IP;然后在这个端口监听;接收到A的请求,发允许给A,并做好接收准备,主要就是清理缓存等待接收新数据;然后正式接收;接受到断开请求,允许断开;确认断开后,继续监听其它请求。
可见,Socket其实就是I/O操作,Socket并不仅限于网络通信,在网络通信中,它涵盖了网络层、传输层、会话层、表示层、应用层——其实这都不需要记,因为Socket通信时候用到了IP和端口,仅这两个就表明了它用到了网络层和传输层;而且它无视多台电脑通信的系统差别,所以它涉及了表示层;一般Socket都是基于一个应用程序的,所以会涉及到会话层和应用层。
构建基础的BIO通信模型
BIOServerSocket
publicclassBIOServerSocket{//先定义一个端口号,这个端口的值是可以自己调整的。staticfinalintDEFAULT_PORT=;publicstaticvoidmain(String[]args)throwsIOException{//先定义一个端口号,这个端口的值是可以自己调整的。//在服务器端,我们需要使用ServerSocket,所以我们先声明一个ServerSocket变量ServerSocketserverSocket=null;//接下来,我们需要绑定监听端口,那我们怎么做呢?只需要创建使用serverSocket实例//ServerSocket有很多构造重载,在这里,我们把前边定义的端口传入,表示当前//ServerSocket监听的端口是serverSocket=newServerSocket(DEFAULT_PORT);System.out.println("启动服务,监听端口:"+DEFAULT_PORT);//回顾一下前面我们讲的内容,接下来我们就需要开始等待客户端的连接了。//所以我们要使用的是accept这个函数,并且当accept方法获得一个客户端请求时,会返回//一个socket对象,这个socket对象让服务器可以用来和客户端通信的一个端点。//开始等待客户端连接,如果没有客户端连接,就会一直阻塞在这个位置Socketsocket=serverSocket.accept();//很可能有多个客户端来发起连接,为了区分客户端,咱们可以输出客户端的端口号System.out.println("客户端:"+socket.getPort()+"已连接");//一旦有客户端连接过来,我们就可以用到IO来获得客户端传过来的数据。//使用InputStream来获得客户端的输入数据//bufferedReader大家还记得吧,他维护了一个缓冲区可以减少数据源读取的频率BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));StringclientStr=bufferedReader.readLine();//读取一行信息System.out.println("客户端发了一段消息:"+clientStr);//服务端收到数据以后,可以给到客户端一个回复。这里咱们用到BufferedWriterBufferedWriterbufferedWriter=newBufferedWriter(newOutputStreamWriter(socket.getOutputStream()));bufferedWriter.write("我已经收到你的消息了\n");bufferedWriter.flush();//清空缓冲区触发消息发送}}
BIOClientSocket
publicclassBIOClientSocket{staticfinalintDEFAULT_PORT=;publicstaticvoidmain(String[]args)throwsIOException{//在客户端这边,咱们使用socket来连接到指定的ip和端口Socketsocket=newSocket("localhost",);//使用BufferedWriter,像服务器端写入一个消息BufferedWriterbufferedWriter=newBufferedWriter(newOutputStreamWriter(socket.getOutputStream()));bufferedWriter.write("我是客户端Client-01\n");bufferedWriter.flush();BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));StringserverStr=bufferedReader.readLine();//通过bufferedReader读取服务端返回的消息System.out.println("服务端返回的消息:"+serverStr);}}
上述代码构建了一个简单的BIO通信模型,也就是服务端建立一个监听,客户端向服务端发送一个消息,实现简单的网络通信,那BIO有什么弊端呢?
我们通过对BIOServerSocket进行改造,