Redis网络模型&基于Socket自定义Redis Client

博客介绍了Linux用户空间和内核空间的划分及缓冲区设置,阐述了五种网络模型,包括阻塞IO、非阻塞IO等。还分析了Redis单线程问题,指出其核心业务单线程的优势及网络模型。介绍了Redis通信协议RESP,最后提及基于Socket自定义Redis Client。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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();
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

从现在开始壹并超

你的鼓励,我们就是hxd

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值