查漏补缺第一期(Redis相关)

前言

目前正在出一个查漏补缺专题系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~

本专题主要以Java语言为主, 好了, 废话不多说直接开整吧~

Q1 & 请介绍一下redis的架构

Redis是一种开源的高性能键值存储系统,通常被用作内存数据库、缓存和消息中间件。它具有简单、灵活、高效的特点,以及丰富的数据结构和功能,使其成为许多应用程序的首选。

Redis的架构主要由以下几个关键组件组成:

  1. 客户端:应用程序通过Redis客户端与Redis服务器进行通信。客户端可以使用多种编程语言和协议与Redis进行交互,如Redis的官方客户端库、REST API或其他第三方客户端。

  2. 服务器:Redis服务器是Redis的核心组件,负责处理客户端请求、执行命令和存储数据。服务器使用单线程事件循环模型,通过监听网络连接和处理事件来实现高并发和低延迟的性能。

  3. 数据存储:Redis支持多种数据结构,包括字符串、哈希表、列表、集合和有序集合等。这些数据结构都可以通过唯一的键来进行访问和操作。Redis的数据存储是基于内存的,但也可以通过持久化机制将数据写入磁盘,以便在服务器重启时进行恢复。

  4. 内存管理:Redis通过使用自定义的内存分配器来管理内存,以提高性能并减少内存碎片。它使用了多种技术,如对象共享、对象池和内存压缩,来有效地管理内存使用。

  5. 高可用性:为了实现高可用性和数据冗余,Redis提供了主从复制和哨兵机制。主从复制通过将数据从主服务器复制到多个从服务器,实现数据的冗余备份和读写分离。哨兵机制用于监控和管理Redis服务器集群,当主服务器发生故障时,自动选举新的主服务器。

  6. 集群模式:Redis还提供了集群模式,可以将数据分片存储在多个节点上,实现水平扩展和负载均衡。集群模式使用哈希槽来划分数据,每个节点负责管理一部分哈希槽的数据。

总体而言,Redis的架构设计简单而灵活,适用于各种场景。它的高性能、丰富的数据结构和功能,使其成为处理大规模数据、高并发访问和实时应用的理想选择。

Q2 & 请详细讲一下redis的线程模型

Redis的线程模型采用的是单线程的事件循环模型,也称为I/O多路复用模型。

  1. 单线程模型: Redis使用单线程来处理所有的客户端请求和数据库操作。这意味着Redis在任何给定的时间只能处理一个请求,但通过事件循环机制,它能够高效地处理大量并发请求。

  2. 事件循环: Redis使用事件循环来实现高并发和低延迟的性能。事件循环是通过I/O多路复用技术实现的,通常使用select、poll或epoll等系统调用。Redis通过监听套接字上的事件,如可读、可写或异常事件,来感知和处理客户端请求。

  3. 非阻塞I/ORedis使用非阻塞I/O来实现事件循环。在接收到客户端请求时,Redis不会阻塞等待请求完成,而是立即返回到事件循环并继续处理其他请求。当请求的I/O操作完成时,Redis会通过回调函数来处理响应数据。

  4. 文件事件: Redis使用文件事件来表示与客户端或其他网络连接相关的事件。每当有新的客户端连接或数据到达时,Redis会生成相应的文件事件并将其加入到事件队列中。事件循环会从队列中获取文件事件并处理它们。

  5. 时间事件: Redis还支持时间事件,用于执行定时任务。时间事件可以是一次性的,也可以是周期性的。通过时间事件,Redis可以执行诸如过期键的清理、统计信息的更新等后台任务。

  6. 非阻塞操作: Redis的数据存储是基于内存的,因此读写操作通常是非阻塞的。这使得Redis能够在快速的内存访问下提供高性能的读写操作。

  7. 多个数据库: Redis支持多个数据库,每个数据库都有自己的键值空间。在单线程模型下,Redis使用字典来存储数据库,通过索引来快速访问不同的数据库。

通过以上的线程模型,Redis能够高效地处理大量的并发请求,而无需为每个请求创建线程或进程。单线程模型可以避免线程切换和同步开销,提供较低的延迟和较高的吞吐量。同时,Redis通过使用非阻塞I/O事件驱动的方式,实现了高效的事件处理和资源利用。

下面我们使用Java来简单的仿写一下单线程模型

public class Q2 {
    public static void main(String[] args) throws InterruptedException {
        RedisTaskQueue taskQueue = new RedisTaskQueue();
        RedisEventLoop eventLoop = new RedisEventLoop(taskQueue);
        RedisClient client = new RedisClient(taskQueue);

        // 启动事件处理器线程
        Thread eventLoopThread = new Thread(eventLoop);
        eventLoopThread.start();

        // 模拟客户端发送命令
        client.sendCommand("SET key1 value1");
        client.sendCommand("GET key1");
        client.sendCommand("DEL key1");

        // 等待所有命令执行完成
        eventLoopThread.join();

//        Executing Redis command: SET key1 value1
//        Executing Redis command: GET key1
//        Executing Redis command: DEL key1
    }
}


// 模拟Redis命令的执行任务
class RedisCommandTask {
    private String command;

    public RedisCommandTask(String command) {
        this.command = command;
    }

    public void execute() {
        // 模拟命令的执行
        System.out.println("Executing Redis command: " + command);
    }
}

// 模拟Redis的任务队列
class RedisTaskQueue {
    private Queue<RedisCommandTask> queue = new LinkedList<>();

    public synchronized void enqueue(RedisCommandTask task) {
        queue.add(task);
        notify(); // 通知等待中的线程有新任务
    }

    public synchronized RedisCommandTask dequeue() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 如果队列为空,则等待新任务的到来
        }
        return queue.poll();
    }
}



// 模拟Redis的事件处理器
class RedisEventLoop implements Runnable {
    private RedisTaskQueue taskQueue;

    public RedisEventLoop(RedisTaskQueue taskQueue) {
        this.taskQueue = taskQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                RedisCommandTask task = taskQueue.dequeue();
                task.execute();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

// 模拟Redis的客户端
class RedisClient {
    private RedisTaskQueue taskQueue;

    public RedisClient(RedisTaskQueue taskQueue) {
        this.taskQueue = taskQueue;
    }

    public void sendCommand(String command) {
        RedisCommandTask task = new RedisCommandTask(command);
        taskQueue.enqueue(task);
    }
}

使用synchronized关键字实现了任务队列的线程安全性。enqueue()方法用于将任务添加到队列中,并使用notify()通知等待中的线程有新任务可执行。dequeue()方法则在队列为空时调用wait()进入等待状态,直到有新任务到来时被唤醒。

事件处理器线程在start()方法中通过循环不断从任务队列中取出任务并执行。如果队列为空,线程会调用wait()方法进入等待状态,直到有新任务到来。这样保证了事件处理器在没有任务时不会空转消耗CPU资源。

主程序中创建了一个RedisClient实例,并通过sendCommand()方法模拟了客户端发送命令的过程。每个命令都被封装成RedisCommandTask对象,并通过任务队列传递给事件处理器线程执行。这种方式模拟了Redis的非阻塞命令执行,即命令被添加到任务队列后,客户端可以继续发送其他命令而无需等待命令执行完成。

Q3 & 请详细讲一下redis的非阻塞式IO和多路复用

Redis使用非阻塞式I/O多路复用技术来实现高性能的事件驱动模型。

  1. 非阻塞式I/O: 非阻塞式I/O是一种I/O操作的模式,在进行读取或写入操作时,不会阻塞线程等待操作完成。Redis使用非阻塞I/O来实现事件驱动模型,它的实现原理如下:

    • 设置套接字为非阻塞模式:Redis在接受客户端连接或创建套接字时,将套接字设置为非阻塞模式。这样,在进行读取或写入操作时,不会阻塞线程,而是立即返回。

    • 非阻塞的读取操作:当Redis执行读取操作时,它会检查套接字上是否有数据可读。如果没有数据可读,读取操作会立即返回,避免线程阻塞。Redis可以使用非阻塞的方式读取数据,以便处理其他事件。

    • 非阻塞的写入操作:当Redis执行写入操作时,它会检查套接字是否可写。如果套接字不可写,写入操作会立即返回,避免线程阻塞。Redis可以使用非阻塞的方式写入数据,以便处理其他事件。

  2. 多路复用: 多路复用是一种I/O多路复用技术,用于同时监视多个套接字上的事件。Redis使用多路复用技术来管理多个套接字上的事件,以实现高效的事件驱动模型。下面是多路复用的实现原理:

    • 注册事件套接字Redis将需要监听的套接字注册到多路复用器中,并指定事件类型,如可读、可写或异常。

    • 多路复用器监听事件:多路复用器负责监听所有已注册套接字上的事件。它使用系统调用(如select、poll或epoll)来监视套接字上的事件状态。

    • 事件就绪通知:当套接字上的事件状态发生变化时,多路复用器会通知Redis。这样,Redis可以及时处理事件。

    • 事件处理:一旦事件就绪,Redis会调用相应的事件处理函数来处理事件。例如,如果有可读事件就绪,Redis会调用读取数据的函数来处理请求。

通过非阻塞式I/O多路复用的组合,Redis实现了高效的事件驱动模型非阻塞式I/O使得Redis能够在进行读取或写入操作时不会阻塞线程,提高了并发处理能力。

多路复用技术允许Redis同时监听多个套接字上的事件,提供高效的事件管理和触发能力。这种组合使得Redis能够处理大量并发请求,提供高性能和低延迟的服务。

下面通过Java简单的演示一下非阻塞式I/O和多路复用

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
 
public class NonBlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(8888));
 
        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
 
        while (true) {
            selector.select();
 
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
 
                if (key.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = serverSocketChannel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = clientChannel.read(buffer);
                    if (bytesRead > 0) {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        String message = new String(data);
                        System.out.println("Received message: " + message);
                    }
                    clientChannel.close();
                }
            }
        }
    }
}

这个示例中,我们创建了一个非阻塞的服务器,使用ServerSocketChannel进行监听,并将其注册到Selector上。然后,在循环中,调用selector.select()等待事件发生。当有事件发生时,通过遍历selectedKeys来处理事件。如果事件是OP_ACCEPT,表示有新的客户端连接,我们接受连接并将客户端的SocketChannel注册到Selector上,监听可读事件OP_READ。如果事件是OP_READ,表示有数据可读,我们读取数据并处理。

结束语

大家可以针对自己薄弱的地方进行复习, 然后多总结,形成自己的理解,不要去背~

本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注鼓励一下呗~

项目源码(源码已更新 欢迎star⭐️)

往期设计模式相关文章

设计模式项目源码(源码已更新 欢迎star⭐️)

Kafka 专题学习

项目源码(源码已更新 欢迎star⭐️)

ElasticSearch 专题学习

项目源码(源码已更新 欢迎star⭐️)

往期并发编程内容推荐

推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)

博客(阅读体验较佳)

猜你喜欢

转载自juejin.im/post/7233951543115415607