Redis网络模型
1. 用户空间和内核空间
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的
- 进程的寻址空间划分为:内核空间和用户空间
- Linux系统为了提高IO效率,会在用户空间和内核空间都会加入缓冲区
2. 五种网络模型
a. 阻塞IO
用户进程在两个阶段都是阻塞的
b. 非阻塞IO
用户进程在等待数据时是非阻塞的,会一直发送recvfrom系统调用,处于盲等状态;而在拷贝数据阶段依旧是阻塞的
c. IO多路复用
用户应用在一阶段都需要调用recvfrom来获取数据,非阻塞IO和阻塞IO的差别在于
- 如果调用recvfrom时,恰好没有数据,阻塞IO会阻塞用户进程,而非阻塞IO会使CPU发生空转
- 如果调用recvfrom时,恰好有数据,用户进程可以直接进入二阶段,读取并处理数据
文件描述符(FD):从0开始递增的无符号整数,用来关联Linux中的一个文件
IO多路复用:利用单个线程来同时监听多个FD,某一个FD可读可写(数据就绪)时得到通知,从而避免无效的等待,充分利用CPU资源
监听FD的常见方式有
-
select:select和poll只会通知用户进程有FD就绪,但不确定具体是哪一个,需要用户进程逐个遍历就绪的FD来确认
- 需要将整个fd_set从用户空间拷贝到内核空间,select结束再次将fd_set拷贝回用户空间
- select无法知道具体是哪一个fd就绪,需要遍历整个fd_set
- fd_set监听的fd数量不能超过1024
-
poll
-
epoll:epoll会在通知用户进程FD就绪的同时,把已就绪的FD直接写入用户空间
事件通知机制
当FD有数据可读时,调用epoll_wait即可得到通知,事件通知机制模式有
- LT(Level Triggered):当FD有数据可读时,会重复通知多次,直到数据处理完成
- ET(Edge Triggered):当FD有数据可读时,只会被通知一次,无论数据是否处理完成
web服务流程
d. 信号驱动IO
e. 异步IO
IO操作是同步还是异步,关键是数据在内核空间和用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步
3. Redis单线程问题
- 如果仅仅是Redis的核心业务部分,也就是命令处理而言,Redis是单线程的
- 整个Redis是多线程的
使用单线程的优势
- Redis是纯内存操作,执行速度快。性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升
- 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为锁而导致的性能消耗
- 不存在多进程或者多线程导致的CPU切换,充分利用CPU资源
Redis网络模型
Redis通过IO多路复用来提高网络性能,Redis单线程网络模型整体流程如下:
aeEventLoop+before sleep+aeApiPoll等价于IO多路复用+事件派发机制
Redis多线程网络模型流程:在读取客户端的数据写入缓冲区,解析成命令可以进行多线程读取;将命令结果写回客户端也可以进行多线程并发写。但是执行redis命令依旧是单线程执行
4. Redis通信协议-RESP(Redis Serialization Protocol)协议
Redis是CS架构,通信一般分为两部分
- 客户端client向服务端server发送一条命令
- 服务端解析并执行命令,返回响应结果给client
RESP通过首字母来区分不同数据类型,包括5种
- 单行字符串:首字节为
+
,结尾\r\n,中间为内容。“+OK\r\n” => 返回"OK" - 错误:首字节为
-
,结尾\r\n,中间为内容 - 数值:首字节为
:
,结尾\r\n,中间为内容 - 多行字符串:首字节为
$
,二进制安全的字符串,最大支持512MB。$5\r\nhello\r\n
=> 只需读5个字节返回 “hello” - 数组:首字节为
*
,后面跟上元素个数,其次是元素,结尾\r\n
5. 基于Socket自定义Redis Client
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
public class customRedisClient {
public static Socket socket;
public static PrintWriter writer;
public static BufferedReader reader;
public static void main(String[] args) {
try{
// 1.建立连接
String host = "192.168.88.128";
int port = 6379;
socket = new Socket(host, port);
// 2.获取输出流,输入流
writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
// 3.1.先发送AUTH请求授权
// 3.2.再次发送redis命令
// 4.解析响应
sendRequest("auth", "hz1234");
System.out.println("授权响应结果: " + handleResponse());
sendRequest("set", "name", "大司马");
System.out.println("命令响应结果: " + handleResponse());
sendRequest("get", "name");
System.out.println("命令响应结果: " + handleResponse());
sendRequest("mget", "name", "age", "score");
System.out.println("命令响应结果: " + handleResponse());
}catch (IOException exception){
exception.printStackTrace();
}finally {
try{
if(reader != null){
reader.close();
}
}catch (IOException e){
e.printStackTrace();
}
try{
if(writer != null){
writer.close();
}
}catch (Exception e){
e.printStackTrace();
}
try{
if(socket != null){
socket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
private static Object handleResponse() throws IOException {
int prefix = reader.read();
// 判断数据类型
switch (prefix){
case '+': // 1.读取单行字符串
return reader.readLine();
case '-': // 2.读取单行错误信息
throw new RuntimeException(reader.readLine());
case ':': // 3.读取数值
return Long.parseLong(reader.readLine());
case '$': // 4.读取多行字符串
int len = Integer.parseInt(reader.readLine());
if(len == -1){
return null;
}
if(len == 0){
return "";
}
// 读取len个字节即为结果
return reader.readLine();
case '*':
// 5.读取数组
return readBulkString();
default:
throw new RemoteException("unknown data type!");
}
}
private static Object readBulkString() throws IOException {
// 获取数组大小
int len = Integer.parseInt(reader.readLine());
if(len <= 0){
return null;
}
List<Object> res = new ArrayList<>(len);
for (int i = 0; i < len; i++) {
// 每读取一行都有可能出现以上五种情况,调用一次handleResponse方法相当于读取了一行
res.add(handleResponse());
}
return res;
}
private static void sendRequest(String ... args) {
writer.println("*" + args.length);
for (String arg : args) {
writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
writer.println(arg);
}
writer.flush();
}
}