当前位置: 首页 > news >正文

Redis高级篇之I/O多路复用的引入解析

文章目录

    • 一、问题背景
      • 1. 高并发连接的管理
      • 2. 避免阻塞和延迟
      • 3. 减少上下文切换开销
      • 4. 高效的事件通知机制
      • 5. 简化编程模型
      • 6. 低延迟响应
      • 本章小节
    • 二、I/O多路复用高性能的本质
      • 1. 避免无意义的轮询:O(1) 事件检测
      • 2. 非阻塞 I/O + 零拷贝:最大化 CPU 利用率
      • 3. 单线程事件循环:无锁、无上下文切换
      • 4. 高效的系统调用:内核级优化
      • 5. Reactor 模式:事件分发与业务逻辑解耦
      • 6. 边缘触发(ET) vs 水平触发(LT)
      • 性能对比:传统模型 vs 多路复用
      • 为什么 Redis 能单线程扛住 10 万 QPS?
      • 本章小节:I/O 多路复用快的本质
    • 三、基于I/O多路复用的Redis高性能设计源码分析
      • 1. 事件循环核心结构:`aeEventLoop`
      • 2. 多路复用 API 的抽象层
        • `aeApiCreate`:初始化多路复用实例
        • `aeApiAddEvent`:注册事件到epoll
      • 3. 事件循环主流程:`aeMain`
        • 关键函数 `aeProcessEvents`
      • 4. 文件事件处理:从连接建立到命令执行
        • 步骤1:监听客户端连接(`acceptTcpHandler`)
        • 步骤2:注册客户端读事件(`readQueryFromClient`)
        • 步骤3:读取并解析命令(`readQueryFromClient`)
        • 步骤4:执行命令并写回结果
      • 5. 多路复用 API 的性能优化
        • `epoll` 的边缘触发(ET) vs 水平触发(LT)
        • `aeApiPoll` 的实现(以epoll为例)
      • 6. 单线程模型与 I/O 多路复用的协同
      • 本章小结:Redis I/O 多路复用的设计精髓
    • 四、使用java实现I/O多路复用模型
      • 关键设计解析
        • 1. Selector 核心机制
        • 2. Channel 注册与事件类型
        • 3. ByteBuffer 状态管理
        • 4. 事件处理流程
      • 性能优化点
        • 1. 零拷贝优化
        • 2. 批量处理事件
        • 3. 对象池化
      • 对比传统BIO模型
      • 运行测试
      • 扩展方向

一、问题背景

Redis采用I/O多路复用技术(如epollkqueueselect)作为其高性能设计的核心机制,主要解决了以下关键问题:


1. 高并发连接的管理

  • 问题:传统多线程/多进程模型中,每个连接需分配独立线程或进程,资源消耗大(内存、CPU上下文切换)。
  • 解决:I/O多路复用允许单线程同时监听和管理成千上万的网络连接,通过事件驱动的方式处理请求,避免为每个连接创建独立线程,显著降低资源占用。
  • 场景:适用于高并发场景(如10万+并发连接),如实时消息队列、高频访问的缓存服务。

2. 避免阻塞和延迟

  • 问题:传统阻塞I/O中,线程在等待数据时会被挂起,导致吞吐量下降。
  • 解决:结合非阻塞I/O,I/O多路复用仅在有数据到达或可写时通知线程处理,线程无需阻塞等待,最大化CPU利用率。
  • 示例:客户端发送请求后,Redis线程无需阻塞等待数据,转而处理其他连接的请求,直到数据就绪。

3. 减少上下文切换开销

  • 问题:多线程/进程模型中,频繁的上下文切换(Context Switching)会消耗大量CPU时间。
  • 解决:单线程配合I/O多路复用,无需线程间切换,减少CPU浪费,提升整体吞吐量。
  • 对比:多线程模型在并发1万连接时可能因切换开销导致性能骤降,而Redis仍能保持低延迟。

4. 高效的事件通知机制

  • 问题:传统轮询(如select)需遍历所有连接检查状态,时间复杂度为O(n),效率低下。
  • 解决:采用epoll(Linux)或kqueue(BSD)等高效多路复用器,仅关注活跃连接,时间复杂度为O(1)。
    • epoll优势:通过事件回调机制直接获取就绪事件列表,避免无意义的遍历。
    • 性能提升:连接数越多,相比select/poll的性能优势越明显。

5. 简化编程模型

  • 问题:多线程同步(如锁、信号量)增加代码复杂度和调试难度。
  • 解决:单线程事件循环模型避免了锁竞争,代码逻辑更简洁,降低并发编程的复杂度。
  • Redis设计:单线程处理命令执行和网络I/O,通过异步机制(如后台线程处理持久化)平衡性能与功能。

6. 低延迟响应

  • 问题:传统多线程模型中,线程调度和锁竞争可能导致请求处理延迟波动。
  • 解决:单线程按事件顺序处理请求,无锁竞争,确保每个请求的响应时间更可预测。
  • 适用场景:对延迟敏感的应用(如实时排行榜、会话存储)。

本章小节

通过I/O多路复用,Redis在单线程中实现了:

  • 高并发连接管理
  • 非阻塞I/O操作
  • 低资源消耗与上下文切换
  • 高效事件驱动处理
  • 稳定低延迟响应

二、I/O多路复用高性能的本质

I/O 多路复用之所以能实现高性能,核心在于它通过一种高效的事件驱动机制,解决了传统阻塞 I/O 和多线程模型的根本性缺陷。以下是其速度快的本质原因:


1. 避免无意义的轮询:O(1) 事件检测

  • 传统模型(如 select/poll:需要遍历所有文件描述符(FD)检查状态,时间复杂度为 O(n),连接数越大效率越低。
  • 多路复用(如 epoll/kqueue
    • 事件回调机制:内核直接维护一个“就绪队列”,仅返回已就绪的事件列表,时间复杂度 O(1)
    • 示例:10 万个连接中只有 100 个活跃时,epoll 直接返回这 100 个事件,而 select 需遍历全部 10 万个。

2. 非阻塞 I/O + 零拷贝:最大化 CPU 利用率

  • 非阻塞 I/O:线程无需等待数据就绪,立即返回处理其他任务,避免 CPU 空转。
  • 零拷贝技术:通过 sendfile 或内存映射(mmap)减少数据在内核态和用户态之间的复制次数,降低 CPU 和内存开销。
  • 对比:传统阻塞 I/O 下,线程在等待数据时完全挂起,浪费 CPU 周期。

3. 单线程事件循环:无锁、无上下文切换

  • 单线程模型:所有 I/O 事件由单线程顺序处理,避免了多线程的锁竞争上下文切换开销。
  • 资源消耗极低:单线程管理数万连接,内存占用仅为多线程模型的 1/100 甚至更低。
  • 适用场景:Redis 的单线程设计正是利用这一点,在 CPU 不是瓶颈时实现超高吞吐量。

4. 高效的系统调用:内核级优化

  • epoll 的优势(Linux)
    • 红黑树管理 FD:快速插入、删除、查找,时间复杂度 O(log n)。
    • 事件驱动回调:通过 epoll_ctl 注册事件,内核直接通知就绪的 FD。
  • kqueue(BSD/MacOS):类似原理,支持更复杂的事件类型(如文件变化、信号)。

5. Reactor 模式:事件分发与业务逻辑解耦

  • 核心思想:将 I/O 事件监听(Reactor)与事件处理(Handler)分离。
  • 工作流程
    1. Reactor 监听所有 I/O 事件(如可读、可写)。
    2. 事件就绪后,分发给对应的 Handler(如 Redis 的命令处理器)。
    3. Handler 处理完成后,将结果写回网络缓冲区。
  • 优势:逻辑清晰,避免阻塞,适合高并发。

6. 边缘触发(ET) vs 水平触发(LT)

  • 水平触发(LT):只要 FD 处于就绪状态,每次调用 epoll_wait 都会返回该事件。
  • 边缘触发(ET):仅在 FD 状态变化时(如从不可读变为可读)触发一次事件。
  • ET 的优势:减少重复事件通知,强制开发者一次性处理完所有数据,避免饥饿问题,性能更高。

性能对比:传统模型 vs 多路复用

场景多线程阻塞 I/OI/O 多路复用
10 万并发空闲连接10 万线程,内存爆炸单线程,内存占用极低
CPU 利用率高(上下文切换)高(无阻塞)
延迟稳定性波动大(线程调度)稳定(单线程顺序处理)
代码复杂度高(锁、同步)低(事件驱动)

为什么 Redis 能单线程扛住 10 万 QPS?

  • 纯内存操作:数据在内存中处理,速度极快(纳秒级)。
  • I/O 多路复用:单线程高效管理所有网络事件。
  • 无锁设计:避免线程竞争,保证原子性。
  • 批量写入优化:通过缓冲区合并小数据包,减少系统调用次数。

本章小节:I/O 多路复用快的本质

  1. 事件驱动:只处理实际发生的 I/O 事件,避免无效轮询。
  2. 非阻塞 + 零拷贝:最大化 CPU 和内存效率。
  3. 单线程无锁:消除多线程开销,简化编程模型。
  4. 内核级优化epoll/kqueue 等机制的高效实现。

这种设计在高并发、低延迟场景(如 Redis、Nginx)中表现尤为突出,成为现代高性能服务器的基石。

三、基于I/O多路复用的Redis高性能设计源码分析

Redis 的 I/O 多路复用实现是其高性能的核心设计之一,源码中通过 事件驱动模型(Event Loop)结合操作系统提供的多路复用 API(如 epollkqueueselect)来实现。以下是关键源码模块的分析,结合 Redis 6.0 源码(代码片段已简化)。


1. 事件循环核心结构:aeEventLoop

Redis 通过 aeEventLoop 结构体管理所有事件(文件事件和时间事件),定义在 ae.h 中:

typedef struct aeEventLoop {int maxfd;                   // 当前注册的最大文件描述符int setsize;                 // 最大监听的文件描述符数量long long timeEventNextId;   // 下一个时间事件的IDaeFileEvent *events;         // 注册的文件事件数组(每个fd对应一个事件)aeFiredEvent *fired;         // 已触发的文件事件数组aeTimeEvent *timeEventHead;  // 时间事件链表头void *apidata;               // 多路复用API的私有数据(如epoll实例)// ...
} aeEventLoop;
  • events 数组:记录每个文件描述符(如客户端 Socket)的读写事件及回调函数。
  • fired 数组:存储每次事件循环中触发的就绪事件。
  • apidata:指向底层多路复用 API 的私有数据结构(如 epollepoll_event 列表)。

2. 多路复用 API 的抽象层

Redis 对不同操作系统的多路复用 API 进行了统一封装,代码在 ae_epoll.cae_kqueue.cae_select.c 中。以 epoll 为例:

aeApiCreate:初始化多路复用实例
static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));state->epfd = epoll_create(1024); // 创建epoll实例state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);eventLoop->apidata = state; // 绑定到aeEventLoopreturn 0;
}
aeApiAddEvent:注册事件到epoll
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee;ee.events = 0;if (mask & AE_READABLE) ee.events |= EPOLLIN;  // 读事件if (mask & AE_WRITABLE) ee.events |= EPOLLOUT; // 写事件epoll_ctl(state->epfd, EPOLL_CTL_ADD, fd, &ee); // 注册到epollreturn 0;
}

3. 事件循环主流程:aeMain

事件循环的核心逻辑在 aeMain 函数中,代码在 ae.c

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {// 1. 处理时间事件(如过期键清理)// 2. 处理文件事件(网络I/O)aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_BEFORE_SLEEP);}
}
关键函数 aeProcessEvents
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {// 1. 计算最近的时间事件触发时间(决定epoll_wait的超时时间)long long maxWait = calculateMaxWaitTime(eventLoop);// 2. 调用多路复用API等待事件(如epoll_wait)int numevents = aeApiPoll(eventLoop, maxWait);// 3. 处理触发的文件事件for (int j = 0; j < numevents; j++) {aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];if (fe->mask & AE_READABLE) {fe->rfileProc(eventLoop, eventLoop->fired[j].fd, fe->clientData, mask);}if (fe->mask & AE_WRITABLE) {fe->wfileProc(eventLoop, eventLoop->fired[j].fd, fe->clientData, mask);}}// 4. 处理时间事件(如定时任务)processTimeEvents(eventLoop);return numevents;
}
  • aeApiPoll:调用底层多路复用 API(如 epoll_wait)等待事件,返回就绪事件数量。
  • 事件回调:根据事件类型(读/写)执行预先注册的回调函数(如 rfileProcwfileProc)。

4. 文件事件处理:从连接建立到命令执行

Redis 的网络事件处理流程如下:

步骤1:监听客户端连接(acceptTcpHandler

当监听 Socket(如 6379 端口)有新的连接到达时,触发读事件,执行 acceptTcpHandler

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {int cfd, cport;char cip[NET_IP_STR_LEN];// 接受客户端连接cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);// 创建客户端对象redisClient *client = createClient(cfd);
}
步骤2:注册客户端读事件(readQueryFromClient

为新客户端 Socket 注册读事件,回调函数为 readQueryFromClient

client *createClient(int fd) {client *c = zmalloc(sizeof(client));// 注册读事件到事件循环aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c);// ...
}
步骤3:读取并解析命令(readQueryFromClient

当客户端发送数据时,触发读事件回调:

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {client *c = privdata;// 从Socket读取数据到客户端缓冲区nread = read(fd, c->querybuf + qblen, readlen);// 解析命令(如SET/GET)processInputBuffer(c);
}
步骤4:执行命令并写回结果

命令解析完成后,执行命令并将结果写入客户端输出缓冲区,注册写事件:

void processCommand(client *c) {// 查找命令并执行(如dictFind(server.commands, c->cmd->name))call(c, CMD_CALL_FULL);// 将响应写入客户端缓冲区if (clientHasPendingReplies(c)) {// 注册写事件,回调函数sendReplyToClientaeCreateFileEvent(server.el, c->fd, AE_WRITABLE, sendReplyToClient, c);}
}

5. 多路复用 API 的性能优化

epoll 的边缘触发(ET) vs 水平触发(LT)

Redis 默认使用 水平触发(LT) 模式:

  • 水平触发:只要 Socket 可读/可写,事件会持续触发,直到数据被处理完。
  • 边缘触发(ET):仅在 Socket 状态变化时触发一次,需一次性读取所有数据(可能需循环读取)。

Redis 选择 LT 的原因:

  1. 代码简洁性:避免处理 ET 模式下的“饥饿”问题(需循环读取直到 EAGAIN)。
  2. 兼容性:LT 模式在所有多路复用 API(如 selectpoll)中行为一致。
aeApiPoll 的实现(以epoll为例)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval;// 调用epoll_wait,等待事件(最大阻塞时间由tvp决定)retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);// 将就绪事件填充到eventLoop->fired数组中for (int j = 0; j < retval; j++) {struct epoll_event *e = state->events + j;eventLoop->fired[j].fd = e->data.fd;eventLoop->fired[j].mask = e->events;}return retval; // 返回就绪事件数量
}

6. 单线程模型与 I/O 多路复用的协同

Redis 的单线程模型通过以下方式与 I/O 多路复用协同工作:

  1. 事件顺序处理:所有网络事件由单线程按顺序处理,避免锁竞争。
  2. 非阻塞 I/O:Socket 设置为非阻塞模式,确保 read/write 不会阻塞线程。
  3. 批量处理:通过一次 epoll_wait 获取所有就绪事件,批量处理。

本章小结:Redis I/O 多路复用的设计精髓

  1. 统一抽象层:封装不同操作系统的多路复用 API,保证跨平台兼容性。
  2. 事件驱动模型:通过 aeEventLoop 管理所有事件,实现高效调度。
  3. 非阻塞 + 回调:最大化 CPU 利用率,避免线程阻塞。
  4. 单线程无锁:消除多线程上下文切换和锁竞争的开销。

通过这种设计,Redis 在单线程中轻松支持数万甚至数十万的并发连接,成为高性能内存数据库的标杆。

四、使用java实现I/O多路复用模型

以下是一个基于 Java NIO 的 I/O 多路复用模型的完整实现示例。该示例将创建一个简单的 Echo 服务器,使用 Selector 实现单线程管理多个客户端连接。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;public class NIOMultiplexingServer {private static final int PORT = 8080;private static final int BUFFER_SIZE = 1024;public static void main(String[] args) throws IOException {// 1. 创建Selector(多路复用器)Selector selector = Selector.open();// 2. 创建ServerSocketChannel并配置为非阻塞模式ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(PORT));serverSocketChannel.configureBlocking(false);// 3. 将ServerSocketChannel注册到Selector,监听ACCEPT事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("Server started on port " + PORT);// 4. 事件循环while (true) {// 阻塞等待就绪的事件(支持超时参数)int readyChannels = selector.select();if (readyChannels == 0) continue;// 获取所有就绪的SelectionKey集合Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove(); // 必须手动移除已处理的keytry {if (key.isAcceptable()) {handleAccept(key, selector);} else if (key.isReadable()) {handleRead(key);} else if (key.isWritable()) {handleWrite(key);}} catch (IOException e) {// 处理客户端异常断开key.cancel();key.channel().close();System.out.println("Client disconnected abnormally");}}}}// 处理新连接private static void handleAccept(SelectionKey key, Selector selector) throws IOException {ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();SocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false);// 注册读事件,并附加一个Buffer用于数据读写clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(BUFFER_SIZE));System.out.println("New client connected: " + clientChannel.getRemoteAddress());}// 处理读事件private static void handleRead(SelectionKey key) throws IOException {SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();int bytesRead = channel.read(buffer);if (bytesRead == -1) { // 客户端正常关闭System.out.println("Client closed connection: " + channel.getRemoteAddress());channel.close();return;}// 切换为读模式buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String message = new String(data);System.out.println("Received: " + message);// 注册写事件(准备回写数据)key.interestOps(SelectionKey.OP_WRITE);buffer.rewind(); // 重置position以便重新读取数据}// 处理写事件private static void handleWrite(SelectionKey key) throws IOException {SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();channel.write(buffer);if (!buffer.hasRemaining()) { // 数据已全部写入// 重新注册读事件key.interestOps(SelectionKey.OP_READ);buffer.clear(); // 重置Buffer}}
}

关键设计解析

1. Selector 核心机制
  • Selector.open():创建多路复用器(底层使用操作系统提供的 epoll/kqueue
  • select():阻塞等待就绪事件(可设置超时时间)
  • selectedKeys():获取所有就绪的事件集合
2. Channel 注册与事件类型
  • 注册事件类型
    • SelectionKey.OP_ACCEPT:新连接事件
    • SelectionKey.OP_READ:数据可读事件
    • SelectionKey.OP_WRITE:数据可写事件
  • 非阻塞模式configureBlocking(false) 是必须的
3. ByteBuffer 状态管理
  • flip():切换为读模式(position=0, limit=原position
  • clear():重置Buffer(position=0, limit=capacity
  • rewind():重置position为0(用于重复读取数据)
4. 事件处理流程
Client ServerSocketChannel Selector SocketChannel Server 发起连接 触发ACCEPT事件 调用handleAccept 注册READ事件 发送数据 触发READ事件 调用handleRead 注册WRITE事件 回写数据 调用handleWrite Client ServerSocketChannel Selector SocketChannel Server

性能优化点

1. 零拷贝优化
// 使用FileChannel直接传输文件(无需用户态内存拷贝)
FileChannel fileChannel = new FileInputStream("largefile.txt").getChannel();
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
2. 批量处理事件
// 使用selectedKeys迭代器快速处理所有事件
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {// ...处理每个key...
}
3. 对象池化
// 复用ByteBuffer对象(避免频繁GC)
private static final ThreadLocal<ByteBuffer> bufferCache = ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(1024) // 直接内存更高效
);

对比传统BIO模型

特性BIO(阻塞I/O)NIO(多路复用)
线程模型1连接1线程单线程管理所有连接
资源消耗高(线程内存、上下文切换)低(单线程+事件驱动)
吞吐量低(受限于线程数)高(万级并发)
编程复杂度简单较高(需处理事件状态机)

运行测试

  1. 编译运行服务端:

    javac NIOMultiplexingServer.java
    java NIOMultiplexingServer
    
  2. 使用 telnetnc 测试:

    telnet localhost 8080
    > Hello  # 输入任意内容,服务器会原样返回
    

扩展方向

  1. 多线程优化:将业务处理与I/O线程分离(如使用线程池处理复杂逻辑)
  2. 协议解析:实现HTTP等复杂协议(需处理半包/粘包问题)
  3. 心跳机制:添加空闲连接检测(通过 IdleStateHandler 类似机制)

通过这种方式,你可以用 Java 原生 NIO 实现一个高性能的 I/O 多路复用服务端,支撑高并发网络请求。

注意:本文章不适合初级人员使用,建议先了解NIO、BIO和Netty的前提之下进行学习

相关文章:

Redis高级篇之I/O多路复用的引入解析

文章目录 一、问题背景1. 高并发连接的管理2. 避免阻塞和延迟3. 减少上下文切换开销4. 高效的事件通知机制5. 简化编程模型6. 低延迟响应本章小节 二、I/O多路复用高性能的本质1. 避免无意义的轮询&#xff1a;O(1) 事件检测2. 非阻塞 I/O 零拷贝&#xff1a;最大化 CPU 利用率…...

FTP协议命令和响应码

文章目录 &#x1f4e6; 一、什么是 FTP 协议&#xff1f;&#x1f9fe; 二、FTP 常见命令&#xff08;客户端发送&#xff09;&#x1f4e1; 三、FTP 响应码&#xff08;服务端返回&#xff09;&#x1f4cc; 响应码分类&#xff08;第一位&#xff09;✅ 常见成功响应码&…...

在win上安装Ubuntu安装Anaconda(linx环境)

一&#xff0c;安装Ubuntu 1. 在 Microsoft 商城去下载Ubuntu(LTS:是长期维护的版本) 2.安装完之后启动程序&#xff0c;再重新打开一个黑窗口&#xff1a; wsl --list --verbose 3.关闭Ubuntu wsl --shutdown Ubuntu-22.04 WSL2 Ubuntu-20.04文件太占c盘空间&#xff0c;…...

【Elasticsearch入门到落地】11、RestClient初始化索引库

接上篇《10、初始化RestClient》 上一篇我们已经完成了RestHighLevelClient的初始化工作&#xff0c;本篇将正式进入索引库的创建阶段。我们将使用Java代码来创建酒店数据的索引库。 一、准备工作 1. 创建常量类 首先&#xff0c;我们需要定义一个常量类来存放索引库的mappi…...

远程服务调用的一些注意事项

引言 最近工作中&#xff0c;遇到了一些关于远程服务调用的问题&#xff0c;背景是调用三方接口获取某些特征数据&#xff0c;但由于调用出现了超时&#xff0c;导致业务本身的接口的可用行降低。因此整理一些远程服务调用时的注意事项&#xff0c;通过不同维度的考虑来提高系…...

QML 样式库

在 QML 中&#xff0c;样式库&#xff08;或 UI 框架&#xff09;用于快速构建一致且美观的界面。Qt/QML 本身不提供内置的完整样式库&#xff0c;但可以通过以下方式实现样式管理或使用第三方库。 1. Qt Quick Controls 2 样式系统 Qt Quick Controls 2 是官方提供的 UI 组件…...

[RHEL8] 指定rpm软件包的更高版本模块流

背景&#xff1a;挂载RHEL ISO使用kickstart安装操作系统&#xff0c;安装包未指定安装perl&#xff0c;但是安装完可以查到其版本&#xff0c;且安装的是ISO中多个版本中的最低版本。 原因&#xff1a;&#xff08;1&#xff09;为什么没有装perl&#xff0c;perl -v可以看到版…...

使用Python可视化洛伦兹变换

引言 大家好!今天我们将探讨一个非常有趣且重要的物理概念—洛伦兹变换。它是相对论的核心内容之一,描述了在高速运动下,时间、长度以及其他物理量是如何发生变化的。通过使用 Python 进行可视化,我们不仅可以更好地理解这个概念,还能感受到物理世界中的奇妙之处。 什么…...

【二叉树专题】一道深入浅出的 DFS 题:求二叉树的直径(含通俗易懂讲解)

题目&#xff1a; 给你一棵二叉树的根节点&#xff0c;返回这棵树的 直径。 直径 是任意两个节点路径中&#xff0c;最长的一条路径所经过的边数。 比如下面这棵树&#xff1a; 1/ \2 3/ \ 4 5它的最长路径是&#xff1a;4 → 2 → 5 或者 4 → 2 → 1 → 3&#xff0c…...

考研系列-计算机网络-第三章、数据链路层

一、数据链路层的功能 1.知识点总结 2.习题总结...

医药采购系统平台第10天02:按药品分类的统计按供货商统计按医院统计统计数据的导出DWR的配置和应用

如果想要获取相关的源码&#xff0c;笔记&#xff0c;和相关工具&#xff0c;对项目需求的二次开发&#xff0c;可以关注我并私信&#xff01;&#xff01;&#xff01; 一 按药品分类的统计实现 1 按药品分类统计的需求 按药品统计&#xff1a;在指定时间段中采购量、采购金…...

Navicat、DataGrip、DBeaver在渲染 BOOLEAN 类型字段时的一种特殊“视觉风格”

文章目录 前言✅ 为什么 Boolean 字段显示为 [ ]&#xff1f;✅ 如何验证实际数据类型&#xff1f;✅ 小结 前言 看到的 deleted: [ ] 并不是 Prisma 的问题&#xff0c;而是数据库客户端&#xff08;如 Navicat、DataGrip、DBeaver&#xff09;在渲染 BOOLEAN 类型字段时的一种…...

(undone) 吴恩达版提示词工程 2. 指南

url: https://www.bilibili.com/video/BV1Z14y1Z7LJ?spm_id_from333.788.videopod.episodes&vd_source7a1a0bc74158c6993c7355c5490fc600&p2 别人的笔记 url: https://zhuanlan.zhihu.com/p/626966526 指导原则&#xff08;Guidelines&#xff09; 编写提示词有两个…...

VLC搭建本机的rtsp直播推流和拉流

媒体---流---捕获设备&#xff0c;选择摄像头&#xff0c;点击串流 x下一步 选择rtsp&#xff0c;点击添加 看到了端口&#xff0c;并设置路径&#xff1a; 选择Video -H 264 mp3(TS) 点击下一个&#xff0c; 点击流&#xff0c;就开始推流了 拉流&#xff0c;观看端&#x…...

Rocky Linux 9.1 修改网卡和DNS

在 Rocky Linux 9.1 中修改网卡和 DNS 配置可以通过 NetworkManager 工具实现(推荐)或直接编辑配置文件。以下是两种方法的详细步骤: 方法一:使用 nmcli 命令行工具(动态生效) 查看当前网络连接nmcli connection show # 输出示例: # NAME UUID …...

Web前端:常用的布局属性

常见的布局方式有哪些&#xff1f; float&#xff1a;浮动布局 ​position 定位布局 ​flex 弹性布局&#xff08;display&#xff09; ​table 表格布局&#xff08;弃用&#xff09; 一、HTML5 语义化布局标签 这些标签本身不提供布局能力&#xff0c;但能增强页面结构…...

XSS学习2

一、客户端的Cookie 1. 无状态的影响 无状态问题: HTTP协议的无状态特性导致每次请求都是独立的&#xff0c;无法保持会话。例如&#xff0c;在银行办理业务时&#xff0c;柜员不需要重复询问客户信息&#xff0c;但在计算机网络中&#xff0c;每次HTTP请求都需要重新认证用户…...

软件设计师/系统架构师---计算机网络

概要 什么是计算机网络&#xff1f; 计算机网络是指将多台计算机和其他设备通过通信线路互联&#xff0c;以便共享资源和信息的系统。计算机网络可以有不同的规模&#xff0c;从家庭网络到全球互联网。它们可以通过有线&#xff08;如以太网&#xff09;或无线&#xff08;如W…...

Kubernetes(k8s)学习笔记(二)--k8s 集群安装

1、kubeadm kubeadm 是官方社区推出的一个用于快速部署 kubernetes 集群的工具。这个工具能通过两条指令完成一个 kubernetes 集群的部署&#xff1a; 1.1 创建一个 Master 节点$ kubeadm init 1.2 将一个 Node 节点加入到当前集群中$ kubeadm join <Master 节点的 IP 和…...

线性DP:最长上升子序列(子序列可不连续,子数组必须连续)

目录 Q1&#xff1a;简单遍历 Q2&#xff1a;变式&#xff08;加大数据量&#xff09; Q1&#xff1a;简单遍历 Dp问题 状态表示 f(i,j) 集合所有以第i个数结尾的上升子序列集合-f(i,j)的值存的是什么序列长度最大值max- 状态计算 &#xff08;其实质是集合的划分&#xff09;…...

SpringBoot 基本原理

SpringBoot 为我们做的自动配置&#xff0c;确实方便快捷&#xff0c;但一直搞不明白它的内部启动原理&#xff0c;这次就来一步步解开 SpringBoot 的神秘面纱&#xff0c;让它不再神秘。 目录 SpringBootApplication 背后的秘密 Configuration ComponentScan EnableAutoC…...

LeetCode第158题_用Read4读取N个字符 II

LeetCode 第158题&#xff1a;用Read4读取N个字符 II 题目描述 给你一个文件&#xff0c;并且该文件只能通过给定的 read4 方法来读取&#xff0c;请实现一个方法来读取 n 个字符。 read4 方法&#xff1a; API read4 可以从文件中读取 4 个连续的字符&#xff0c;并且将它…...

webgl入门实例-矩阵在图形学中的作用

矩阵在图形学中扮演着核心角色&#xff0c;几乎所有图形变换、投影和空间转换都依赖矩阵运算来实现高效计算。以下是矩阵在图形学中的主要作用及具体应用&#xff1a; 1. 几何变换 矩阵乘法可以高效表示物体的平移、旋转、缩放等基本变换&#xff0c;并通过矩阵连乘实现复合变…...

基于Matlab求解矩阵电容等效容值

1需求 仿真测试8*10阶举证电容等效容值。 2模型搭建 2.1打开simscape 在打开simulink之后打开simscape库&#xff0c;Simscape库位置如下 2.2搭建模型 在库中寻找需要的元件搭建电路。 2.2.1基本元件 电阻电容电感等基础器件&#xff0c;搭建电路之后需要对其进行幅值&…...

铅酸电池充电器方案EG1253+EG4321

参考&#xff1a; 基于EG1253EG4321铅酸电池(48V20AH)三段式充电器 屹晶微高性价比的电瓶车充电器方案——EG1253 电瓶电压 48V电瓶锂电池&#xff0c;其充满约为55V~56V&#xff0c;因此充电器输出电压为55V~56V&#xff1b; 若是48V铅酸电池&#xff0c;标称电压为48V&…...

每天学一个 Linux 命令(26):less

​​可访问网站查看,视觉品味拉满: http://www.616vip.cn/26/index.html less 是 Linux 中一个强大的文件内容查看工具,用于分页显示文件内容,支持快速搜索、滚动浏览、跳转等操作。相比 more,less 功能更丰富且支持向前和向后翻页,适合查看大文件或日志。 命令格式 les…...

【网络】数据链路层知识梳理

全是通俗易懂的讲解&#xff0c;如果你本节之前的知识都掌握清楚&#xff0c;那就速速来看我的笔记吧~ 自己写自己的八股&#xff01;让未来的自己看懂&#xff01; &#xff08;全文手敲&#xff0c;受益良多&#xff09; 数据链路层 我们来重新理解一下这个图&#xff1a;…...

2.2 BackgroundWorker的使用介绍

BackgroundWorker 是 .NET Framework 中一个简化异步操作的组件&#xff0c;它位于 System.ComponentModel 命名空间下。它为开发人员提供了一种简单的方式在后台执行耗时操作&#xff0c;同时保持与 UI 线程的交互 主要属性以及任务如下&#xff1a; DoWork 事件&#xff1a;…...

Java从入门到“放弃”(精通)之旅——类和对象全面解析⑦

Java从入门到“放弃”&#xff08;精通&#xff09;之旅&#x1f680;——类和对象全面解析⑦ 一、面向对象初探 1.1 什么是面向对象&#xff1f; Java是一门纯面向对象的语言(OOP)&#xff0c;在面向对象的世界里&#xff0c;一切皆为对象。面向对象是解决问题的一种思想&a…...

无回显RCE

在CTF和实战渗透中&#xff0c;不是每一个命令执行点都有回显&#xff0c;有时我们审了半天代码&#xff0c;却发现好不容易找到的命令执行没有回显&#xff0c;但是这并不代表这段代码不能被我们利用&#xff0c;在无回显的情况下也是可以利用的 首先我们来写一个最简单的php…...

DQN在Gym的MountainCar环境的实现

DQN on MountainCar 引言 在本次实验里&#xff0c;我构建了DQN和Dueling DQN&#xff0c;并在Gymnasium库的MountainCar环境中对它们展开测试。我通过调整训练任务的超参数&#xff0c;同时设计不同的奖励函数及其对应参数&#xff0c;致力于获取更优的训练效果。最后&#…...

typescript判断是否为空

1 判断数据类型 1.1 基础数据类型 比如number&#xff0c;string&#xff0c;boolean&#xff0c;使用typeof&#xff0c;返回值是string类型&#xff1a; 例如&#xff1a; if("number" typeof(item)) {egret.log("item的类型是number"); } else if(&…...

JavaScript forEach介绍(JS forEach、JS for循环)

文章目录 JavaScript forEach 方法全面解析基本概念语法详解参数说明 工作原理与其他循环方法的比较forEach vs for循环forEach vs map 实际应用场景DOM元素批量操作数据处理 性能考量常见陷阱与解决方案无法中断循环异步操作问题 高级技巧链式调用&#xff08;不使用 forEach …...

C语言之图像文件的属性

&#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 图像文件属性提取系统设计与实现 目录 设计题目设计内容系统分析总体设计详细设计程序实现…...

Java链表反转方法详解

一、理解链表结构 假设链表节点定义为&#xff1a; class ListNode {int val;ListNode next;ListNode(int x) { val x; } } 二、迭代法反转链表 核心思路 逐步反转每个节点的指针方向&#xff0c;最终使整个链表反向。 步骤拆解 初始化三个指针&#xff1a; prev&#xf…...

lmm-r1开源程序是扩展 OpenRLHF 以支持 LMM RL 训练,用于在多模态任务上重现 DeepSeek-R1

一、软件介绍 文末提供程序和源码下载学习 lmm-r1开源程序是扩展 OpenRLHF 以支持 LMM RL 训练&#xff0c;用于在多模态任务上重现 DeepSeek-R1。 二、简介 小型 3B 大型多模态模型&#xff08;LMMs&#xff09;由于参数容量有限以及将视觉感知与逻辑推理相结合的固有复杂性…...

Java学习笔记(数组,方法)

一&#xff0c;数组 1.数组初始化 1.1动态初始化 格式&#xff1a;数据类型[] 数组名 new 数据类型[数组长度]; int[] arr new int[3]; // 定义长度为3的int数组&#xff0c;元素默认值为0 double[] scores new double[5]; // 长度5&#xff0c;元素默认0.0 String[…...

嵌入式---零点漂移(Zero Drift)

一、零点漂移的定义与本质 零点漂移&#xff08;简称“零漂”&#xff09;指传感器在输入信号为零&#xff08;或理论上应输出固定零值&#xff09;时&#xff0c;输出信号随时间、温度、环境等因素变化而偏离初始零点的现象。 核心特征&#xff1a;无输入时输出非零且缓慢变…...

健身房管理系统设计与实现(springboot+ssm+vue+mysql)含万字详细文档

健身房管理系统设计与实现(springbootssmvuemysql)含万字详细文档 健身房管理系统是一个全面的解决方案&#xff0c;旨在帮助健身房高效管理日常运营。系统主要功能模块包括个人中心、会员管理、员工管理、会员卡管理、会员卡类型管理、教练信息管理、解聘管理、健身项目管理、…...

C语言if

一、题目引入 如果从键盘输入58,则以下程序输出的结果是多少? 二、运行结果 三、题目分析 因为这道题中的多个if是并列结构 所以只要条件满足都会执行 这一题58满足所有的条件 所以可以运行出来 也就是说每个if里面的条件都满足 所以都会打印出来 而下面的这种情况就是 if e…...

XSS学习1之http回顾

1. HTTP的基本结构与工作流程 HTTP是一个请求-响应协议&#xff0c;基于客户端与服务器之间的交互。每次用户通过浏览器请求某个资源时&#xff0c;HTTP协议都会完成一系列的步骤。 HTTP请求&#xff1a; HTTP请求由以下几个部分构成&#xff1a; 请求行&#xff1a; 请求方…...

小迪抓包技术算法加密(6-9天)

抓包技术 https://blog.csdn.net/2301_81015455/article/details/147014382 算法加密入门&#xff08;了解&#xff09; 在实际测试中安全性高一些得采用得都是AES等高安全加密&#xff0c;遇到这种&#xff0c;放弃你啥都不知道测个毛啊,所以直接run!!! 大部分解密时碰撞式…...

Tkinter与ttk模块对比:构建现代 Python GUI 的进化之路

在 Python GUI 开发中&#xff0c;标准库 tkinter 及其子模块 ttk&#xff08;Themed Tkinter&#xff09;常被同时使用。本文通过功能对比和实际案例&#xff0c;简单介绍这两个模块的核心差异。 1. 区别 Tkinter&#xff1a;Python 标准 GUI 工具包&#xff08;1994年集成&…...

【数据结构入门训练DAY-18】信息学奥赛一本通T1331-后缀表达式的值

文章目录 前言一、题目二、解题思路总结 前言 本次训练内容&#xff1a; 栈的复习。栈模拟四则运算计算问题的练习。训练解题思维。 一、题目 从键盘读入一个后缀表达式&#xff08;字符串&#xff09;&#xff0c;只含有0-9组成的运算数及加&#xff08;&#xff09;、减…...

时序预测 | Transformer-LSTM-SVM时间序列预测(Matlab完整源码和数据,适合基础小白研究)

时序预测 | Transformer-LSTM-SVM时间序列预测&#xff08;Matlab完整源码和数据&#xff0c;适合基础小白研究&#xff09; 目录 时序预测 | Transformer-LSTM-SVM时间序列预测&#xff08;Matlab完整源码和数据&#xff0c;适合基础小白研究&#xff09;效果一览基本介绍代码…...

【HarmonyOS 5】makeObserved接口详解

【HarmonyOS 5】makeObserved接口详解 一、makeObserved接口是什么&#xff1f; makeObserved 接口&#xff08;API version 12 起可用&#xff09;用于将非观察数据转为可观察数据&#xff0c;适用于三方包类、Sendable 装饰的类、JSON.parse 返回的对象、collections.Array…...

色谱图QCPColorMap

一、QCPColorMap 概述 QCPColorMap 是 QCustomPlot 中用于绘制二维颜色图的类&#xff0c;可以将矩阵数据可视化为颜色图&#xff08;热力图&#xff09;&#xff0c;支持自定义色标和插值方式。 二、主要属性 属性类型描述dataQCPColorMapData存储颜色图数据的对象interpol…...

【数据结构_12】二叉树(4)

一、二叉树的层序遍历 思路&#xff1a;可以按照先序的方式来遍历这个树&#xff0c;递归的时候&#xff0c;给递归方法&#xff0c;加上辅助的参数&#xff0c;level表示当前层数&#xff0c;递归过程中&#xff0c;根据level的值&#xff0c;决定当前整个节点要放到哪个list中…...

HCIA-Datacom高阶:vlan、vlanif、单臂路由、静态路由、ospf综合实验

本实验拓扑图如下&#xff1a;实验包含 AR1、AR2、AR3 路由器&#xff0c;LSW1&#xff08;三层交换机&#xff09;、LSW2、LSW3 交换机&#xff0c;以及 PC1-4 和 Server1。AR1 与 LSW2 通过单臂路由连接&#xff0c;AR2 与 AR3、LSW1 构成 OSPF 网络&#xff0c;AR1 与 AR2 间…...

HTTP 1.0 和 2.0 的区别

HTTP 1.0 和 2.0 的核心区别体现在性能优化、协议设计和功能扩展上&#xff0c;以下是具体对比&#xff1a; 一、核心区别对比 特性HTTP 1.0HTTP 2.0连接方式非持久连接&#xff08;默认每次请求新建 TCP 连接&#xff09;持久连接&#xff08;默认保持连接&#xff0c;可复用…...