我们平常应该会遇到一个redis的面试题
Redis 的线程模型是什么?
简单来说就是内部采用的是reactor单线程模型,它内部用的是一个叫做文件事件处理器的东西,这个文件事件处理器这个东西就是单线程的,所以说redis也是一个单线程的模型
这个可能涉及到一些网络编程的知识:
什么是BIO,NIO,两者的区别是什么
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式
正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
这是抄来的定义,怎么理解呢?
看了很多视频,自己也敲了敲代码,说下我自己的理解
首先说说BIO,同步阻塞机制,我写了一个小Demo
服务端代码
public class RedisServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9090);
while (true){
//阻塞的方法
Socket socket = serverSocket.accept();
System.out.println("XXXXX来连接了");
//在这里是错误的方法,因为我们的BIO不把我们的socket放入线程的话是不能支持并发的
//因为我们的read方法是会阻塞的,我们另一个客户端进来会在这个地方阻塞掉,是不会回去接收我们的客户端的socket的,所以只能有一个客户端连接
byte[] bytes = new byte[1024];
//阻塞的方法 没有消息可读会阻塞
socket.getInputStream().read(bytes);
System.out.println("收到客户端的消息" + new String(bytes));
}
}
}
客户端代码
public class RedisClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",9090);
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
socket.getOutputStream().write(s.getBytes());
socket.close();
}
}
试想一个客户端去连接,应该是没啥问题的,但是如果两个以上的客户端去连接这个服务端就会出现问题了,因为第二个客户端来连接的时候服务器端的read()方法是同步阻塞机制的,也就是说我们的方法会阻塞到这个地方,第二个客户端是没办法进行操作的。
这也是我们为啥要将服务端这部分代码放到Thread去执行的原因,单线程这样操作是不支持并发的,但是问题又来了,我们都知道线程占资源是很宝贵的,如果我们的服务端创建了大量的线程就会很不好(资源,CPU占用之类的),用线程池来解决呢?确实可以解决一部分问题,但是并不能很完美的解决这个问题,怎么办?就是我们的NIO,他就是致力于单线程下处理多请求并发问题的
补充一些知识
画图理解,在别人的基础上自己理解的,有雷同就是借鉴别人的
一个请求连接共用了几个socket
单线程怎么处理并发请求呢?
NIO包提供了一个叫Selector选择器集合的东西
我贴一部分代码
selector = Selector.open();//创建选择器对象
ssc = ServerSocketChannel.open();//打开服务端socket
InetSocketAddress addr = new InetSocketAddress(port);
ssc.socket().bind(addr);//在我们的ServerSockerChanel绑定端口
ssc.configureBlocking(false);//设置ServerSockerChanel为非阻塞
//
SelectionKey sk = ssc.register(selector,SelectionKey.OP_ACCEPT);//
//给定key一个附加的Acceptor对象,我们可以通过这个来判断是什么事件
sk.attach(new Acceptor(selector,ssc));
还有一部分代码是这样,至于什么业务我们不用关心,我只是来理解这个思想的
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
//遍历已经发生的事件
while (it.hasNext()){
SelectionKey selectionKey = it.next();
//根据事件的Key去调度
dispatch(selectionKey);
it.remove();
}
流程图
也就是说,单线程的情况下,我们用了一个selector的集合来存储我们的socket,这里还附加了一个Acceptor的对象,
流程就是:
如果我们是第一次连接,将我们的accept请求注册到这个集合中,附加一些关于他的信息,然后遍历这个时候我们的selectkeys集合,看看里面存放了什么感兴趣的事,如果是accept那我就调用它的run方法,如果是read方法,我就调用read的run方法,同时好几个请求,既有链接,也有读请求,那我就一起放入我的selectKeys中,轮询一个一个操作,一个一个的删除,这就是我理解的NIO的大致思想
然后我们再来理解redis的线程模型