Redis高性能原理探秘-IO模型

Redis的性能由哪些因素决定?

  • 内存

    由于Redis是基于内存的操作,因此内存大小是决定其性能的一个重要因素。

  • CPU

    CPU 是另一个重要的影响因素,由于是单线程模型,Redis 更喜欢大缓存快速 CPU, 而不是多核。

  • 网络通信

    网络带宽和延迟通常是最大短板。

网络通信模型

最终目标: 增加客户端的访问连接数量

  • BIO(阻塞IO模型)
    • ServerSocket

    • Socket

    • 阻塞体现在两个地方:

      连接阻塞

      IO阻塞

    • 使用场景

      zookeeper的leader选举(3个节点, 5个节点)
      nacos的注册地址信息同步

  • NIO(非阻塞IO)

    把连接阻塞和IO阻塞改成非阻塞

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的多路复用模型。

从请求处理开始分析

当我们在客户端向Redis Server发送一条指令,并且得到Redis回复的整个过程中,Redis做了什么呢?

redis-req.png

要处理命令,则redis必须完整地接收客户端的请求,并将命令解析出来,再将结果读出来,通过网络回
写到客户端。整个工序分为以下几个部分:

  • 接收,通过TCP接收到命令,可能会历经多次TCP包、ack、IO操作
  • 解析,将命令取出来
  • 执行,到对应的地方将value读出来
  • 返回,将value通过TCP返回给客户端,如果value较大,则IO负荷会更重

其中解析执行是纯cpu/内存操作,而接收和返回主要是IO操作,首先我们先来看通信的过程。

网络IO的通信原理

网络通信.png

同样,用一幅图来描述网络数据的传输流程

  • 首先,对于TCP通信来说,每个TCP Socket的内核中都有一个发送缓冲区和一个接收缓冲区接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区。
  • read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。
  • 进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端。
  • 网卡中的缓冲区既不属于内核空间,也不属于用户空间。它属于硬件缓冲,允许网卡与操作系统之间有
    个缓冲; 内核缓冲区在内核空间,在内存中,用于内核程序,做为读自或写往硬件的数据缓冲区; 用
    户缓冲区在用户空间,在内存中,用于用户程序,做为读自或写往硬件的数据缓冲区。
  • 网卡芯片收到网络数据会以中断的方式通知CPU,我有数据了,存在我的硬件缓冲里了,来读我啊。
    CPU收到这个中断信号后,会调用相应的驱动接口函数从网卡的硬件缓冲里把数据读到内核缓冲区,正
    常情况下会向上传递给TCP/IP模块一层一层的处理。

IO多路复用机制

Redis的通信采用的是多路复用机制,什么是多路复用机制呢?

  • 由于Redis是C语言实现,为了方便理解,我们采用Java语言来描述这个过程。

在理解多路复用之前,我们先来了解一下BIO。

BIO模型

在Java中,如果要实现网络通信,我们会采用Socket套接字来完成。
Socket不是一个协议,而是一个通信模型。其实它最初是BSD发明的,主要用于一台电脑的两个进程间通信,后来把它用到了两台电脑的进程间通信。所以,可以把它简单理解为进程间通信,不是什么高级的东西。主要做的事情不就是:

  • A发包:发请求包给某个已经绑定的端口(所以我们经常会访问这样的地址182.13.15.16:1235,1235就是端口);收到B的允许;然后正式发送;发送完了,告诉B要断开链接;收到断开允许,马上断开,然后发送已经断开信息给B。
  • B收包:绑定端口和IP;然后在这个端口监听;接收到A的请求,发允许给A,并做好接收准备,主要就是清理缓存等待接收新数据;然后正式接收;接收到断开请求,允许断开;确认断开后,继续监听其它请求。

可见,Socket其实就是I/O操作,Socket并不仅限于网络通信,在网络通信中,它涵盖了网络层、传输层、会话层、表示层、应用层——其实这都不需要记,因为Socket通信时候用到了IP和端口,仅这两个就表明了它用到了网络层和传输层;而且它无视多台电脑通信的系统差别,所以它涉及了表示层;一般Socket都是基于一个应用程序的,所以会涉及到会话层和应用层。

构建基础的BIO通信模型
@Slf4j
public class BIOServerSocket {
    public static final int PORT = 8080;
    public static void main(String[] args) {
        try(ServerSocket serverSocket = new ServerSocket(PORT)) {
            log.info("启动服务:监听端口:{}",PORT);
            //阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
            Socket socket = serverSocket.accept();
            log.info("客户端:{}连接成功",socket.getPort());
            //阻塞(InputStream是阻塞的)等待获取客户端请求报文
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String clientStr = bufferedReader.readLine();
            log.info("收到客户端发送的消息:{}",clientStr);
            //构建输出流,写回客户端
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("server return message:" + clientStr + "\n");
            //清空缓冲区,发送消息
            bufferedWriter.flush();
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

测试效果:

bio-1.png

我们通过对BIOServerSocket进行改造,关注case1和case2部分。

  • case1: 增加了while循环,实现重复监听
  • case2: 当服务端收到客户端的请求后,不直接返回,而是等待20s。
@Slf4j
public class BIOServerSocket2 {
    public static final int PORT = 8080;
    public static void main(String[] args) {
        try(ServerSocket serverSocket = new ServerSocket(PORT)) {
            log.info("启动服务:监听端口:{}",PORT);
            //循环接收请求
            while (true){
                //阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
                Socket socket = serverSocket.accept();
                log.info("客户端:{}连接成功",socket.getPort());
                //阻塞(InputStream是阻塞的)等待获取客户端请求报文
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String clientStr = bufferedReader.readLine();
                log.info("收到客户端发送的消息:{}",clientStr);
                //等待20秒
                Thread.sleep(20*1000);
                //构建输出流,写回客户端
                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                bufferedWriter.write("server return message:" + clientStr + "\n");
                //清空缓冲区,发送消息
                bufferedWriter.flush();
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }
    }
}

客户端代码:BIOClientSocket

@Slf4j
public class BIOClientSocket {
    public static final String HOST = "127.0.0.1";
    public static final int PORT = 8080;
    public static void main(String[] args) {
        try(Socket socket = new Socket(HOST,PORT)) {
            log.info("客户端连接端口:{}:{}",HOST,PORT);
            //构建输出流,请求服务端
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("client1 msg: hello \n");
            //清空缓冲区,发送消息
            bufferedWriter.flush();
            //阻塞(InputStream是阻塞的)等待获取客户端请求报文
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String clientStr = bufferedReader.readLine();
            log.info("收到服务端返回的消息:{}",clientStr);

        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

接着,把BIOClientSocket复制两份(client1、client2),同时向BIOServerSocket发起请求。

现象: client1先发送请求到Server端,由于Server端等待20s才返回,导致client2的请求一直被阻塞。

bio-2.png

这个情况会导致一个问题,如果服务端在同一个时刻只能处理一个客户端的连接,而如果一个网站同时有1000个用户访问,那么剩下的999个用户都需要等待,而这个等待的耗时取决于前面的请求的处理时长,如图所示。

bio-req.png
基于多线程优化BIO

为了让服务端能够同时处理更多的客户端连接,避免因为某个客户端连接阻塞导致后续请求被阻塞,于是引入多线程技术,代码如下。

BIOServerSocketWithThread

@Slf4j
public class BIOServerSocketWithThread {
    public static TaskExecutor taskExecutor = new TaskExecutor() {
        ExecutorService executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(),
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(), Executors.defaultThreadFactory());
        @Override
        public void execute(Runnable task) {
            executorService.execute(task);
        }
    };
    public static final int PORT = 8080;
    public static void main(String[] args) {
        try(ServerSocket serverSocket = new ServerSocket(PORT)) {
            log.info("启动服务:监听端口:{}",PORT);
            //循环接收请求
            while (true){
                //阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
                Socket socket = serverSocket.accept();
                log.info("客户端:{}连接成功",socket.getPort());
                // I/O异步执行
                taskExecutor.execute(new SocketThread(socket));
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

SocketThread

@Slf4j
public class SocketThread implements Runnable{
    private Socket socket;

    public SocketThread(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            //阻塞(InputStream是阻塞的)等待获取客户端请求报文
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String clientStr = bufferedReader.readLine();
            log.info("收到客户端发送的消息:{}",clientStr);
            //等待20秒
            Thread.sleep(20*1000);
            //构建输出流,写回客户端
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("server return message:" + clientStr + "\n");
            //清空缓冲区,发送消息
            bufferedWriter.flush();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //TODO 关闭IO流
        }
    }
}

结果:

bio-3.png

当引入了多线程之后,每个客户端的链接(Socket),我们可以直接给到线程池去执行,而由于这个过程是异步的,所以并不会同步阻塞影响后续链接的监听,因此在一定程度上可以提升服务端链接的处理数量。

bi-req-2.png
NIO非阻塞IO
  • 使用多线程的方式来解决这个问题,仍然有一个缺点,线程的数量取决于硬件配置,所以线程数量是有限的,如果请求量比较大的时候,线程本身会收到限制从而并发量也不会太高。那怎么办呢,我们可以采用非阻塞IO。

  • NIO 从JDK1.4 提出的,本意是New IO,它的出现为了弥补原本IO的不足,提供了更高效的方式,提出一个通道(channel)的概念,在IO中它始终以流的形式对数据的传输和接收,下面我们演示一下NIO的使用。

    NIOServerSocket

    @Slf4j
    public class NIOServerSocket {
        public static final int PORT = 8080;
        public static void main(String[] args) {
            try {
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                //设置连接非阻塞
                serverSocketChannel.configureBlocking(false);
                serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
                while(true){
                    //获得一个客户端连接,非阻塞
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    if(socketChannel != null){
                        log.info("客户端连接:{}",socketChannel.getRemoteAddress());
                        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                        socketChannel.read(byteBuffer);
                        log.info("Server Receive Msg:"+new String(byteBuffer.array()));
                        Thread.sleep(10000);
                        //反转
                        byteBuffer.flip();
                        socketChannel.write(byteBuffer);
                    }else{
                        Thread.sleep(1000);
                        log.info("客户端未连接");
                    }
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    NIOClientSocket

    @Slf4j
    public class NIOClientSocket {
        public static final String HOST = "127.0.0.1";
        public static final int PORT = 8080;
        public static void main(String[] args) {
            try(SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT))) {
                socketChannel.configureBlocking(false);
                log.info("客户端连接端口:{}:{}",HOST,PORT);
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                String hello = "NIOClient msg: hello \n";
                byteBuffer.put(hello.getBytes());
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                while(true) {
                    byteBuffer.clear();
                    int i = socketChannel.read(byteBuffer);
                    if (i > 0) {
                        log.info("收到服务端的数据:{}",new String(byteBuffer.array()));
                    } else {
                        log.info("服务端数据未准备好");
                        Thread.sleep(1000);
                    }
                }
    
            } catch (IOException | InterruptedException ioException) {
                ioException.printStackTrace();
            }
        }
    }
    

    演示效果:

    服务端

nio-1.png

客户端

nio-2.png
  • 所谓的NIO(非阻塞IO),其实就是取消了IO阻塞和连接阻塞,当服务端不存在阻塞的时候,就可以不断轮询处理客户端的请求,如图所示,表示NIO下的运行流程。
nio-loop.png

上述这种NIO的使用方式,仍然存在一个问题,就是客户端或者服务端需要通过一个线程不断轮询才能
获得结果,而这个轮询过程中会浪费线程资源。

多路复用IO
  • 大家站在全局的角度再思考一下整个过程,有哪些地方可以优化呢?

  • 我们回到NIOClientSocket中下面这段代码,当客户端通过 read 方法去读取服务端返回的数据时,如果此时服务端数据未准备好,对于客户端来说就是一次无效的轮询。

    while(true) {
                    byteBuffer.clear();
                    int i = socketChannel.read(byteBuffer);
                    if (i > 0) {
                        log.info("收到服务端的数据:{}",new String(byteBuffer.array()));
                    } else {
                        log.info("服务端数据未准备好");
                        Thread.sleep(1000);
                    }
                }
    
  • 我们能不能够设计成,当客户端调用 read 方法之后,不仅仅不阻塞,同时也不需要轮询。而是等到服务端的数据就绪之后, 告诉客户端。然后客户端再去读取服务端返回的数据呢?所以为了优化这个问题,引入了多路复用机制。

  • I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

    什么是文件描述符(fd):在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。

  • 常见的IO多路复用方式有【select、poll、epoll】,都是Linux API提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型。

    • select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点

      • 由于他能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。

      • 同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的TCP连接来说确实有点少。

    • epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024。

      由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行。

  • I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图所示

多路复用.png
  • 客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。代码如下:

    @Slf4j
    public class NIOSelectorServerSocket implements Runnable{
        public static final int PORT = 8080;
        Selector selector;
        ServerSocketChannel serverSocketChannel;
    
        public NIOSelectorServerSocket(int port) throws IOException {
            selector = Selector.open();
            serverSocketChannel=ServerSocketChannel.open();
            //如果采用selector模型,必须要设置非阻塞
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        }
    
        @Override
        public void run() {
            while(!Thread.interrupted()){
                try {
                    //阻塞等待事件就绪
                    selector.select();
                    //事件列表
                    Set selected = selector.selectedKeys();
                    Iterator it = selected.iterator();
                    while(it.hasNext()){
                        //说明有连接进来
                        dispatch((SelectionKey) it.next());
                        //移除当前就绪的事件
                        it.remove();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        private void dispatch(SelectionKey key) throws IOException {
            //是连接事件?
            if(key.isAcceptable()){
                log.info("客户端注册事件:{}",key);
                register(key);
            }else if(key.isReadable()){
                log.info("读事件:{}",key);
                //读事件
                read(key);
            }else if(key.isWritable()){
                log.info("写事件:{}",key);
                //写事件
                write(key);
            }
        }
        private void register(SelectionKey key) throws IOException {
            //客户端连接
            ServerSocketChannel channel= (ServerSocketChannel) key.channel();
            //获得客户端连接
            SocketChannel socketChannel = channel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector,SelectionKey.OP_READ);
        }
        private void read(SelectionKey key) throws IOException {
            //得到的是socketChannel
            SocketChannel channel= (SocketChannel) key.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            channel.read(byteBuffer);
            log.info("Server Receive Msg:{}",new String(byteBuffer.array()));
            //反转
            byteBuffer.flip();
            channel.write(byteBuffer);
        }
        private void write(SelectionKey key) throws IOException {
            //得到的是socketChannel
         
        }
    
        public static void main(String[] args) throws IOException {
            NIOSelectorServerSocket selectorServerSocket=new NIOSelectorServerSocket(PORT);
            new Thread(selectorServerSocket).start();
        }
    }
    
  • 事实上NIO已经解决了上述BIO暴露的下面两个问题:

    • 同步阻塞IO,读写阻塞,线程等待时间过长。
    • 在制定线程策略的时候,只能根据CPU的数目来限定可用线程资源,不能根据连接并发数目来制定,也就是连接有限制。否则很难保证对客户端请求的高效和公平。
  • 到这里为止,通过NIO的多路复用机制,解决了IO阻塞导致客户端连接处理受限的问题,服务端只需要一个线程就可以维护多个客户端,并且客户端的某个连接如果准备就绪时,会通过事件机制告诉应用程序某个channel可用,应用程序通过select方法选出就绪的channel进行处理。

单线程Reactor 模型(高性能I/O设计模式)

  • 了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去异步非阻塞处理,如图所示。

    reactor-1.png

Reactor模型有三个重要的组件:

  • Reactor :将I/O事件发派给对应的Handler

  • Acceptor :处理客户端连接请求

  • Handlers :执行非阻塞读/写

  • 一个单线程的Reactor模型,代码如下:

    ReactorMain

    public class ReactorMain {
        public static final int PORT = 8080;
        public static void main(String[] args) throws IOException {
            new Thread(new Reactor(PORT),"Main-Thread").start();
        }
    }
    

    Reactor

    @Slf4j
    public class Reactor implements Runnable{
        private final Selector selector;
        private final ServerSocketChannel serverSocketChannel;
    
        public Reactor(int port) throws IOException {
            selector=Selector.open();
            serverSocketChannel= ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new Acceptor(selector,serverSocketChannel));
        }
    
        @Override
        public void run() {
            while(!Thread.interrupted()){
                try {
                    selector.select();
                    Set selectionKeys = selector.selectedKeys();
                    Iterator iterator = selectionKeys.iterator();
                    while(iterator.hasNext()){
                        dispatch(iterator.next());
                        iterator.remove();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        private void dispatch(SelectionKey key){
            //可能拿到的对象有两个
            // Acceptor
            // Handler
            Runnable runnable = (Runnable)key.attachment();
            if(runnable != null){
                runnable.run();
            }
        }
    }
    
    

    Acceptor

    @Slf4j
    public class Acceptor implements Runnable{
    
        private final Selector selector;
        private final ServerSocketChannel serverSocketChannel;
    
        public Acceptor(Selector selector, ServerSocketChannel serverSocketChannel) {
            this.selector = selector;
            this.serverSocketChannel = serverSocketChannel;
        }
    
        @Override
        public void run() {
            SocketChannel channel;
            try {
                //得到一个客户端连接
                channel = serverSocketChannel.accept();
                log.info("{}:收到一个客户端连接",channel.getRemoteAddress());
                channel.configureBlocking(false);
                channel.register(selector, SelectionKey.OP_READ,new Handler(channel));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    Handler

    @Slf4j
    public class Handler implements Runnable{
        SocketChannel channe;
    
        public Handler(SocketChannel channe) {
            this.channe = channe;
        }
    
        @Override
        public void run() {
            log.info("{}------",Thread.currentThread().getName());
            ByteBuffer buffer=ByteBuffer.allocate(1024);
            /*try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            int len=0,total=0;
            String msg="";
            try {
                do {
                    len = channe.read(buffer);
                    if(len>0){
                        total+=len;
                        msg+=new String(buffer.array());
                    }
                } while (len > buffer.capacity());
                log.info("total:{}",total);
    
                //msg=表示通信传输报文
                //耗时2s
                //登录: username:password
                //ServetRequets: 请求信息
                //数据库的判断
                //返回数据,通过channel写回到客户端
    
                log.info("{}: Server receive Msg:{}",channe.getRemoteAddress(),msg);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if(channe!=null){
                    try {
                        channe.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    代码是最基本的单Reactor单线程模型(整体的I/O操作是由同一个线程完成的)。

    其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。
    Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,由Reactor分发)。

    Reactor 模式本质上指的是使用 I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(nonblocking I/O) 的模式。

多线程单Reactor模型

  • 单线程Reactor这种实现方式有存在着缺点,从实例代码中可以看出,handler的执行是串行的,如果其中一个handler处理线程阻塞将导致其他的业务处理阻塞。由于handler和reactor在同一个线程中的执行,这也将导致新的无法接收新的请求,我们做一个小实验:

    • 在上述Reactor代码的DispatchHandler的run方法中,增加一个Thread.sleep()。
    • 打开多个客户端窗口连接到Reactor Server端,其中一个窗口发送一个信息后被阻塞,另外一个窗口再发信息时由于前面的请求阻塞导致后续请求无法被处理。
  • 为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步处理,将reactor和handler在不同的线程来执行,如图所示。

reactor-2.png

多线程改造-MultiDispatchHandler,我们直接将前面的Reactor单线程模型改成多线程,其实我们就是把IO阻塞的问题通过异步的方式做了优化,源码如下:

@Slf4j
public class MutilDispatchHandler implements Runnable{

    SocketChannel channel;

    private Executor executor= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public MutilDispatchHandler(SocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void run() {
        processor();
    }
    private void processor(){
        executor.execute(new ReaderHandler(channel));
    }
    static class ReaderHandler implements Runnable{
        private SocketChannel channel;

        public ReaderHandler(SocketChannel channel) {
            this.channel = channel;
        }

        @Override
        public void run() {
            log.info("{}------",Thread.currentThread().getName());
            ByteBuffer buffer=ByteBuffer.allocate(1024);
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int len=0,total=0;
            String msg="";
            try {
                do {
                    len = channel.read(buffer);
                    if(len>0){
                        total+=len;
                        msg+=new String(buffer.array());
                    }
                } while (len > buffer.capacity());
                System.out.println("total:"+total);

                //msg=表示通信传输报文
                //耗时2s
                //登录: username:password
                //ServetRequets: 请求信息
                //数据库的判断
                //返回数据,通过channel写回到客户端
                log.info("{}: Server receive Msg:{}",channel.getRemoteAddress(),msg);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if(channel!=null){
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
  • 在多线程Reactor模型中,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。

你可能感兴趣的:(Redis高性能原理探秘-IO模型)