Redis IO多路复用

0、前言

本文所有代码可见 => 【gitee code demo】

本文涉及的主题:

1、BIO、NIO的业务实践和缺陷

2、Redis IO多路复用:redis快的主要原因

3、epoll 架构

部分图片 via 【epoll 原理分析】

1、BIO单线程版

1.1 业务代码

client client代码相同 启动多个即可

java 复制代码
public class RedisClient1 {
	
	public static void main(String[] args) throws IOException {
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		Socket socket = new Socket("127.0.0.1", 6300);
		log("{} {}> 尝试连接服务 {}", sdf.format(new Date()) ,socket.getLocalPort(), socket.getPort());
		OutputStream outputStream = socket.getOutputStream();
		
		
		while (true) {
			Scanner scanner = new Scanner(System.in);
			log("{} {}> ", sdf.format(new Date()) ,socket.getLocalPort());
			String string = scanner.nextLine();
			if (string.equalsIgnoreCase("quit")) {
				break;
			}
			socket.getOutputStream().write(string.getBytes());
			log("{} {}> 发送数据:{}", sdf.format(new Date()) ,socket.getLocalPort(), string);
		}
		outputStream.close();
		socket.close();
	}
	
}

server

java 复制代码
public class RedisServerBIO {
	public static void main(String[] args) throws IOException {
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		
		ServerSocket serverSocket = new ServerSocket(6300);
		while (true) {
			log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());
			Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
			log("{} {}> {} 连接到服务", sdf.format(new Date()), socket.getLocalPort(), socket.getPort());
			InputStream inputStream = socket.getInputStream();
			int length = -1;
			byte[] bytes = new byte[1024];
			log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());
			while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
			{
				log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), serverSocket.getLocalPort(), socket.getPort(), new String(bytes, 0, length));
			}
			inputStream.close();
			socket.close();
		}
	}
}

1.2 结果演示

现象:

1、client1 连接到server,client2尝试连接被阻塞

2、client2 先发送的消息未被server接受,client1后发送的repeat消息被server接受

结论:

BIO会一直阻塞,单线程下只能处理一个socket连接

存在的问题:

多 client 访问时效率低

2、BIO多线程版

2.1 业务代码

java 复制代码
public class RedisServerBIOMultiThread {
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(6300);
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());
		
		while (true) {
			Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
			log("{} {}> {} 连接到服务", sdf.format(new Date()), socket.getLocalPort(), socket.getPort());
			
			new Thread(() -> {
				try {
					InputStream inputStream = socket.getInputStream();
					int length = -1;
					byte[] bytes = new byte[1024];
					while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
					{
						log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), serverSocket.getLocalPort(), socket.getPort(), new String(bytes, 0, length));
					}
					inputStream.close();
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}, Thread.currentThread().getName()).start();
			
			System.out.println(Thread.currentThread().getName());
			
		}
	}
}

2.1 结果演示

现象:

client1 、client2 都能正常连接到 server且正常发送、接受消息

结论:

BIO多线程提高处理能力,可以同时处理多个socket连接

存在的问题:

每个线程只能处理一个socket,当client数量大时,需要消耗大量线程资源

3、NIO

3.1 业务代码

当一个客户端与服务端进行连接,这个socket就会加入到一个容器中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了

java 复制代码
public class RedisServerNIO {
	static ArrayList<SocketChannel> socketList = new ArrayList<>();
	static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
	
	public static void main(String[] args) throws IOException {
		ServerSocketChannel serverSocket = ServerSocketChannel.open();
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		serverSocket.bind(new InetSocketAddress("127.0.0.1", 6300));
		serverSocket.configureBlocking(false);//设置为非阻塞模式
			
		while (true) {
			for (SocketChannel element : socketList) {
				int read = element.read(byteBuffer);
				if (read > 0) {
					byteBuffer.flip();
					byte[] bytes = new byte[read];
					byteBuffer.get(bytes);
					//System.out.println(JSONUtil.toJsonStr(element));
					log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), element.socket().getLocalPort(), element.socket().getPort(), new String(bytes, 0, read));
					byteBuffer.clear(); 
				}
			}
			
			SocketChannel socketChannel = serverSocket.accept();
			if (socketChannel != null) {
				log("{} {}> {} 连接到服务", sdf.format(new Date()), socketChannel.socket().getLocalPort(), socketChannel.socket().getPort());
				socketChannel.configureBlocking(false);//设置为非阻塞模式
				socketList.add(socketChannel);
				log("{} {}> socket 数量: {} ", sdf.format(new Date()), socketChannel.socket().getLocalPort(), socketList.size());
			}
		}
	}
}

3.2 结果演示

现象:

1、client1 、client2 都能正常连接到 server且正常发送、接受消息

2、server 没有创建额外线程

结论:

NIO 可以实现一个线程处理多个 socket 连接

存在的问题:

1、每次遍历所有socket,有很多无用功

2、遍历过程在用户态,还需要将数据从内核态读取到用户态

4、IO多路复用

1、使用 epoll() 实现,多个网络连接 socket 复用同一个线程

2、基于事件驱动机制,socket 中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有 socket

3、减少了内核态和用户态的切换

Redis IO多路复用实现

4.1 epoll_create()

创建内核中的fd容器

4.2 epoll_ctl()

epoll_ctl函数用于增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中

4.3 epoll_wait

用于监听套接字事件,可以通过设置超时时间timeout来控制监听的行为为阻塞模式还是超时模式

4.4 epoll 软件架构

相关推荐
喵叔哟1 小时前
ASP.NET Core 中的缓存
缓存·asp.net·wpf
S-X-S3 小时前
Gateway实现Redis拉取信息+用户模块开发
数据库·redis·gateway
Onlooker1294 小时前
Redis2-Redis常见命令
数据库·redis·缓存
Amd7944 小时前
清除 Nuxt 数据缓存:clearNuxtData
缓存·组件·路由·nuxt·数据·清除·刷新
debugBiubiubiu20006 小时前
Redis02——缓存(缓存更新策略、缓存穿透、缓存雪崩、缓存击穿、缓存工具封装)
java·redis·学习·缓存
北顾丶6 小时前
Redis作为缓存,如何与MySql的数据进行同步?
java·开发语言·redis·mysql·缓存·java基础·java面试
一眼万里*e9 小时前
skynet 连接redis
redis
z355190694714 小时前
缓存一致性问题
数据库·缓存·golang
叫我OldFe16 小时前
【从0到1进阶Redis】Jedis 理解事务
java·redis·springboot
蜗牛 | ICU16 小时前
【redis 第八篇章】链表结构
数据库·redis·链表