Redis线程模型的前世今生

文章讲述了Java网络编程中的阻塞、非阻塞模式以及IO多路复用原理,通过实例展示了如何使用NIO和多线程解决服务器性能问题,以及Redis采用的IO多路复用策略。最后讨论了Reactor模式在单线程和多线程模型的应用及其优缺点。
摘要由CSDN通过智能技术生成

e.printStackTrace();

} finally {

try {

socket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

我们准备用两个客户端同时发起连接请求、来模拟单线程阻塞模式的现象。同时发起连接,通过服务端日志,我们发现此时服务端只接受了其中一个连接,主线程被阻塞在上一个连接的read方法上。

我们尝试关闭第一个连接,看第二个连接的情况,我们希望看到的现象是,主线程返回,新的客户端连接被接受。

从日志中发现,在第一个连接被关闭后,第二个连接的请求被处理了,也就是说第二个连接请求在排队,直到主线程被唤醒,才能接收下一个请求,符合我们的预期。

此时不仅要问,为什么呢?

主要原因在于accept、read、write三个函数都是阻塞的,主线程在系统调用的时候,线程是被阻塞的,其他客户端的连接无法被响应。

通过以上流程,我们很容易发现这个过程的缺陷,服务器每次只能处理一个连接请求,CPU没有得到充分利用,性能比较低。如何充分利用CPU的多核特性呢?自然而然的想到了——多线程逻辑

2.1.2 多线程阻塞

对工程师而言,代码解释一切,直接上代码。

BIO多线程

package net.io.bio;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.ServerSocket;

import java.net.Socket;

public class BioTest {

public static void main(String[] args) throws IOException {

final ServerSocket server=new ServerSocket(8081);

while(true) {

new Thread(new Runnable() {

public void run() {

Socket socket=null;

try {

socket = server.accept();

System.out.println(“accept port:”+socket.getPort());

BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));

String inData=null;

while ((inData = in.readLine()) != null) {

System.out.println(“client port:”+socket.getPort());

System.out.println(“input data:”+inData);

if(“close”.equals(inData)) {

socket.close();

}

}

} catch (IOException e) {

e.printStackTrace();

} finally {

}

}

}).start();

}

}

}

同样,我们并行发起两个请求;

两个请求,都被接受,服务端新增两个线程来处理客户端的连接和后续请求。

我们用多线程解决了,服务器同时只能处理一个请求的问题,但同时又带来了一个问题,如果客户端连接比较多时,服务端会创建大量的线程来处理请求,但线程本身是比较耗资源的,创建、上下文切换都比较耗资源,又如何去解决呢?

2.2 非阻塞


如果我们把所有的Socket(文件句柄,后续用Socket来代替fd的概念,尽量减少概念,减轻阅读负担)都放到队列里,只用一个线程来轮训所有的Socket的状态,如果准备好了就把它拿出来,是不是就减少了服务端的线程数呢?

一起看下代码,单纯非阻塞模式,我们基本上不用,为了演示逻辑,我们模拟了相关代码如下;

package net.io.bio;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.ServerSocket;

import java.net.Socket;

import java.net.SocketTimeoutException;

import java.util.ArrayList;

import java.util.List;

import org.apache.commons.collections4.CollectionUtils;

public class NioTest {

public static void main(String[] args) throws IOException {

final ServerSocket server=new ServerSocket(8082);

server.setSoTimeout(1000);

List sockets=new ArrayList();

while (true) {

Socket socket = null;

try {

socket = server.accept();

socket.setSoTimeout(500);

sockets.add(socket);

System.out.println(“accept client port:”+socket.getPort());

} catch (SocketTimeoutException e) {

System.out.println(“accept timeout”);

}

//模拟非阻塞:轮询已连接的socket,每个socket等待10MS,有数据就处理,无数据就返回,继续轮询

if(CollectionUtils.isNotEmpty(sockets)) {

for(Socket socketTemp:sockets ) {

try {

BufferedReader in=new BufferedReader(new InputStreamReader(socketTemp.getInputStream()));

String inData=null;

while ((inData = in.readLine()) != null) {

System.out.println(“input data client port:”+socketTemp.getPort());

System.out.println(“input data client port:”+socketTemp.getPort() +“data:”+inData);

if(“close”.equals(inData)) {

socketTemp.close();

}

}

} catch (SocketTimeoutException e) {

System.out.println(“input client loop”+socketTemp.getPort());

}

}

}

}

}

}

系统初始化,等待连接;

发起两个客户端连接,线程开始轮询两个连接中是否有数据。

两个连接分别输入数据后,轮询线程发现有数据准备好了,开始相关的逻辑处理(单线程、多线程都可)。

再用一张流程图辅助解释下(系统实际采用文件句柄,此时用Socket来代替,方便大家理解)。

服务端专门有一个线程来负责轮询所有的Socket,来确认操作系统是否完成了相关事件,如果有则返回处理,如果无继续轮询,大家一起来思考下?此时又带来了什么问题呢。

CPU的空转、系统调用(每次轮询到涉及到一次系统调用,通过内核命令来确认数据是否准备好),造成资源的浪费,那有没有一种机制,来解决这个问题呢?

2.3 IO多路复用


server端有没专门的线程来做轮询操作(应用程序端非内核),而是由事件来触发,当有相关读、写、连接事件到来时,主动唤起服务端线程来进行相关逻辑处理。模拟了相关代码如下;

IO多路复用

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.nio.charset.Charset;

import java.util.Iterator;

import java.util.Set;

public class NioServer {

private static Charset charset = Charset.forName(“UTF-8”);

public static void main(String[] args) {

try {

Selector selector = Selector.open();

ServerSocketChannel chanel = ServerSocketChannel.open();

chanel.bind(new InetSocketAddress(8083));

chanel.configureBlocking(false);

chanel.register(selector, SelectionKey.OP_ACCEPT);

while (true){

int select = selector.select();

if(select == 0){

System.out.println(“select loop”);

continue;

}

System.out.println(“os data ok”);

Set selectionKeys = selector.selectedKeys();

Iterator iterator = selectionKeys.iterator();

while (iterator.hasNext()){

SelectionKey selectionKey = iterator.next();

if(selectionKey.isAcceptable()){

ServerSocketChannel server = (ServerSocketChannel)selectionKey.channel();

SocketChannel client = server.accept();

client.configureBlocking(false);

client.register(selector, SelectionKey.OP_READ);

//继续可以接收连接事件

selectionKey.interestOps(SelectionKey.OP_ACCEPT);

}else if(selectionKey.isReadable()){

//得到SocketChannel

SocketChannel client = (SocketChannel)selectionKey.channel();

//定义缓冲区

ByteBuffer buffer = ByteBuffer.allocate(1024);

StringBuilder content = new StringBuilder();

while (client.read(buffer) > 0){

buffer.flip();

content.append(charset.decode(buffer));

}

System.out.println(“client port:”+client.getRemoteAddress().toString()+",input data: "+content.toString());

//清空缓冲区

buffer.clear();

}

iterator.remove();

}

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

同时创建两个连接;

两个连接无阻塞的被创建;

无阻塞的接收读写;

再用一张流程图辅助解释下(系统实际采用文件句柄,此时用Socket来代替,方便大家理解)。

当然操作系统的多路复用有好几种实现方式,我们经常使用的select(),epoll模式这里不做过多的解释,有兴趣的可以查看相关文档,IO的发展后面还有异步、事件等模式,我们在这里不过多的赘述,我们更多的是为了解释Redis线程模式的发展。

三、NIO线程模型解释

===========

我们一起来聊了阻塞、非阻塞、IO多路复用模式,那Redis采用的是哪种呢?

Redis采用的是IO多路复用模式,所以我们重点来了解下多路复用这种模式,如何在更好的落地到我们系统中,不可避免的我们要聊下Reactor模式。

首先我们做下相关的名词解释;

Reactor:类似NIO编程中的Selector,负责I/O事件的派发;

Acceptor:NIO中接收到事件后,处理连接的那个分支逻辑;

Handler:消息读写处理等操作类。

3.1 单Reactor单线程模型


处理流程

  • Reactor监听连接事件、Socket事件,当有连接事件过来时交给Acceptor处理,当有Socket事件过来时交个对应的Handler处理。

优点

  • 模型比较简单,所有的处理过程都在一个连接里;

  • 实现上比较容易,模块功能也比较解耦,Reactor负责多路复用和事件分发处理,Acceptor负责连接事件处理,Handler负责Scoket读写事件处理。

缺点

  • 只有一个线程,连接处理和业务处理共用一个线程,无法充分利用CPU多核的优势。

  • 在流量不是特别大、业务处理比较快的时候系统可以有很好的表现,当流量比较大、读写事件比较耗时情况下,容易导致系统出现性能瓶颈。

怎么去解决上述问题呢?既然业务处理逻辑可能会影响系统瓶颈,那我们是不是可以把业务处理逻辑单拎出来,交给线程池来处理,一方面减小对主线程的影响,另一方面利用CPU多核的优势。这一点希望大家要理解透彻,方便我们后续理解Redis由单线程模型到多线程模型的设计的思路。

3.2 单Reactor多线程模型


这种模型相对单Reactor单线程模型,只是将业务逻辑的处理逻辑交给了一个线程池来处理。

处理流程

  • Reactor监听连接事件、Socket事件,当有连接事件过来时交给Acceptor处理,当有Socket事件过来时交个对应的Handler处理。

  • Handler完成读事件后,包装成一个任务对象,交给线程池来处理,把业务处理逻辑交给其他线程来处理。
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

最新整理面试题
在这里插入图片描述

上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题

最新整理电子书

在这里插入图片描述

最新整理大厂面试文档

在这里插入图片描述

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

最新整理面试题
[外链图片转存中…(img-PwAugUc5-1711809585533)]

上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题

最新整理电子书

[外链图片转存中…(img-cyuNRe55-1711809585533)]

最新整理大厂面试文档

[外链图片转存中…(img-byr0Mozf-1711809585533)]

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值