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 软件架构

相关推荐
信仰_2739932438 小时前
Redis红锁
数据库·redis·缓存
爬山算法11 小时前
Redis(120)Redis的常见错误如何处理?
数据库·redis·缓存
野生技术架构师11 小时前
盘一盘Redis的底层数据结构
数据结构·数据库·redis
Feng.Lee11 小时前
聊聊缓存测试用例设计方案
缓存·测试用例
R.lin12 小时前
memcached 的核心工作机制、优缺点、适用场景以及常见问题的处理方式
数据库·缓存·memcached
bug总结12 小时前
更新原生小程序封装(新增缓存订阅)完美解决
前端·缓存·小程序
笃行客从不躺平14 小时前
CPU 缓存 高并发探索
缓存
freedom_1024_18 小时前
LRU缓存淘汰算法详解与C++实现
c++·算法·缓存
羊锦磊19 小时前
[ 项目开发 1.0 ] 新闻网站的开发流程和注意事项
java·数据库·spring boot·redis·spring·oracle·json
wddblog19 小时前
多级缓存体系与热点对抗术--速度是用户体验的王道,而缓存是提升速度的银弹
缓存·ux