浅析Redis②:命令处理之epoll实现(中)

news2025/1/10 23:52:02

写在前面

Redis作为我们日常工作中最常使用的缓存数据库,其重要性不言而喻,作为普通开发者,我们在日常开发中使用Redis,主要聚焦于Redis的基层数据结构的命令使用,很少会有人对Redis的内部实现机制进行了解,对于我而言,也是如此,但一直以来,我对于Redis的内部实现都很好奇,它为什么会如此高效,本系列文章是旨在对Redis源代码分析拆解,通过阅读Redis源代码,了解Redis基础数据结构的实现机制。

关于Redis的源码分析,已经有非常多的大佬写过相关的内容,最为著名的是《Redis设计与实现》,对于Redis源码的分析已经非常出色,本系列文章对于源码拆解时,并不会那么详细,相信大部分读者应该不是从事Redis的二次开发工作,对于源码细节过于深入,会陷入细节的泥潭,这是我在阅读源码时尽量避免的,我尽量做到对大体的脉络进行梳理,讲清楚主干逻辑,细节部分,如果读者有兴趣,可以自行参阅源码或相关资料。

本系列源代码,基于Redis 3.2.6

前言

在上一篇中浅析Redis①:命令处理核心源码分析(上),我们大致了解了Redis客户端命令请求的处理流程,在整个流程中,我们还有两个问题没有解释:

1、非阻塞的核心epoll是如何实现的?

2、Redis是如何将数据写回Client端的?

本篇我们就围绕第一个问题,寻找答案,继续看Redis客户端命令请求的处理流程。

Redis的epoll实现

Redis的非阻塞I/O是指Redis在处理客户端请求时,不会一直等待I/O操作完成,而是会尽快返回,并在I/O操作完成后通知Redis进行后续处理。

epoll作为非阻塞I/O的实现,是Linux内核提供的一种多路I/O复用机制。epoll可以监视多个文件描述符,一旦某个文件描述符就绪,epoll就会通知Redis进行后续处理。

Redis的非阻塞I/O模型可以提高并发处理能力,在阻塞I/O模型中,Redis在处理一个客户端请求时,如果遇到I/O操作,会一直等待I/O操作完成,这意味着Redis无法处理其他客户端的请求。

而在非阻塞I/O模型中,Redis在遇到I/O操作时,会尽快返回,并在I/O操作完成后通知Redis进行后续处理。这样,Redis就可以同时处理多个客户端的请求,提高了并发处理能力。

同时非阻塞I/O模型还可以减少Redis的CPU占用率。在阻塞I/O模型中,Redis在遇到I/O操作时,会一直等待I/O操作完成,这意味着Redis的CPU会一直处于占用状态。

在非阻塞I/O模型中,Redis在遇到I/O操作时,会尽快返回,并在I/O操作完成后通知Redis进行后续处理。这样CPU就不会一直处于占用状态,可以减少CPU占用率,提升CPU使用效率。

核心实现

Redis非阻塞IO的实现是基于OS的内核函数支持,源码逻辑如下:

redis.c中main方法启动,执行initServer()初始化redis配置,同时创建非阻塞事件监听器:

server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);


for (j = 0; j < server.ipfd_count; j++) {
    if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
                          acceptTcpHandler,NULL) == AE_ERR)
    {
        redisPanic(
            "Unrecoverable error creating server.ipfd file event.");
    }
}

其中,ipfd_count默认参数为1024,该参数表示Redis可以同时处理的最大TCP连接数。

aeCreateEventLoop与aeCreateFileEvent的实现逻辑在ae.c文件中:

aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop;
    int i;

    if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
    if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
    eventLoop->setsize = setsize;
    eventLoop->lastTime = time(NULL);
    eventLoop->timeEventHead = NULL;
    eventLoop->timeEventNextId = 0;
    eventLoop->stop = 0;
    eventLoop->maxfd = -1;
    eventLoop->beforesleep = NULL;
    if (aeApiCreate(eventLoop) == -1) goto err;
    /* Events with mask == AE_NONE are not set. So let's initialize the
     * vector with it. */
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;
    return eventLoop;

    err:
    if (eventLoop) {
        zfree(eventLoop->events);
        zfree(eventLoop->fired);
        zfree(eventLoop);
    }
    return NULL;
}

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
                      aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

其中aeApiCreate()是核心创建逻辑,aeApiCreate()方法采用了类似Java中多态的实现方式,由于C本身并不支持多态,因此需要使用C中的技巧实现:

/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

这段代码是Redis中的一个条件编译语句,用于根据不同的操作系统和编译器选择不同的事件驱动库。

事件驱动库是Redis的一个核心组件,用于处理各种事件,包括网络IO事件、定时器事件等。Redis支持多种事件驱动库,比如epoll、kqueue、select等。在编译Redis时,需要根据操作系统和编译器选择合适的事件驱动库进行编译。

这段代码中,首先判断是否定义了HAVE_EVPORT宏。如果定义了该宏,则使用ae_evport.c文件中的事件驱动库,否则继续判断是否定义了HAVE_EPOLL宏。如果定义了该宏,则使用ae_epoll.c文件中的事件驱动库,否则继续判断是否定义了HAVE_KQUEUE宏。如果定义了该宏,则使用ae_kqueue.c文件中的事件驱动库,否则使用ae_select.c文件中的事件驱动库。

这种条件编译技术可以使Redis在不同操作系统和编译器下具有更好的兼容性和可移植性,使得Redis可以在不同的平台上运行,并且可以充分发挥不同平台的优势。

简言之,就是根据不同的操作系统,决定选择不同的内核IO模型,优先级: evport > epoll > kqueue > select

关于系统内核实现,参考:

#ifdef HAVE_EVPORT: 如果定义了宏 HAVE_EVPORT,则包含文件 ae_evport.c。ae_evport.c 可能包含了 Solaris 10 系统使用的事件驱动库。

#else: 如果没有定义宏 HAVE_EVPORT,则继续处理后续代码。

#ifdef HAVE_EPOLL: 如果定义了宏 HAVE_EPOLL,则包含文件 ae_epoll.c。ae_epoll.c 可能包含了 Linux 系统使用的事件驱动库 epoll。

#else: 如果没有定义宏 HAVE_EPOLL,则继续处理后续代码。

#ifdef HAVE_KQUEUE: 如果定义了宏 HAVE_KQUEUE,则包含文件 ae_kqueue.c。ae_kqueue.c 可能包含了 FreeBSD 或 macOS 系统使用的事件驱动库 kqueue。

#else: 如果没有定义宏 HAVE_KQUEUE,则包含文件 ae_select.c。ae_select.c 可能包含了所有系统都支持的 select 事件驱动库,但效率较低。

#endif: 结束条件编译语句块。

我们常用的CentOS使用的是epoll的实现,在Linux系统中,epoll机制是一种高效的事件触发机制,可以监听大量的文件描述符,并在文件描述符上发生事件时,立即通知应用程序。使用epoll机制时,需要使用epoll_create函数创建一个epoll对象,然后使用epoll_ctl函数向epoll对象添加或删除文件描述符,最后使用epoll_wait函数等待事件的发生。

epoll的实现在ae_epoll.c文件中:

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }
    eventLoop->apidata = state;
    return 0;
}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
        EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

epoll_create是Linux系统中的一个系统调用,用于创建一个epoll对象,以便对文件描述符进行事件监听。

在Linux系统中,如果需要对多个文件描述符进行事件监听,常用的方式是使用select或poll函数。但是随着文件描述符数量的增加,select和poll函数的效率会逐渐降低,因为它们需要遍历所有的文件描述符,而无法实现快速的事件通知。为了解决这个问题,Linux引入了epoll机制,通过epoll_create系统调用创建一个epoll对象,然后使用epoll_ctl函数向epoll对象添加或删除文件描述符,最后使用epoll_wait函数等待事件的发生。

epoll_create函数的原型如下:

#include <sys/epoll.h>
int epoll_create(int size);

其中,size参数表示epoll对象中能够监听的最大文件描述符数量,这个参数在Linux 2.6.8之后已经无效,可以忽略。epoll_create函数返回一个整数类型的文件描述符,表示创建的epoll对象的标识符。如果创建失败,返回-1。

需要注意的是,使用epoll_create函数创建的epoll对象是在内核中创建的,而不是在用户空间中创建的。因此,在使用epoll机制时,需要将文件描述符设置为非阻塞模式,并且需要使用epoll_ctl函数向内核注册文件描述符,从而实现文件描述符的事件监听。

epoll_ctl是Linux系统中的一个系统调用,用于向epoll对象中添加或删除文件描述符,并设置对应的事件类型。

在Linux系统中,epoll机制是一种高效的事件触发机制,可以监听大量的文件描述符,并在文件描述符上发生事件时,立即通知应用程序。使用epoll机制时,需要使用epoll_create函数创建一个epoll对象,然后使用epoll_ctl函数向epoll对象添加或删除文件描述符,最后使用epoll_wait函数等待事件的发生。

epoll_ctl函数的原型如下:

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

其中,epfd参数表示epoll对象的文件描述符,op参数表示操作类型,可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD或EPOLL_CTL_DEL,分别表示添加、修改或删除文件描述符。fd参数表示要添加、修改或删除的文件描述符,event参数表示要监听的事件类型,包括读事件、写事件等。

需要注意的是,使用epoll_ctl函数添加、修改或删除文件描述符时,需要将文件描述符设置为非阻塞模式。在调用epoll_wait函数等待事件时,如果有事件发生,epoll_wait函数会返回一组事件列表,然后可以处理这些事件。处理完毕后,可以使用epoll_ctl函数修改或删除已经处理过的文件描述符,然后再次调用epoll_wait函数等待事件的发生。

epoll_wait是一个Linux内核提供的系统调用,用于等待文件描述符上的事件。epoll是Linux内核提供的一种多路I/O复用机制,可以监视多个文件描述符,一旦某个文件描述符就绪,epoll就会通知用户进程进行后续处理。

epoll_wait的函数原型如下:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明如下:

  • epfd:epoll实例的文件描述符。
  • events:用于存放就绪文件描述符的数组。
  • maxevents:events数组的大小。
  • timeout:等待事件的超时时间,单位为毫秒。

epoll_wait的返回值如下:

  • 成功时,返回就绪文件描述符的数目。
  • 出错时,返回-1。

epoll_wait的使用步骤如下:

  1. 创建一个epoll实例,并获取其文件描述符。
  2. 将需要监视的文件描述符注册到epoll实例中。
  3. 调用epoll_wait函数,等待事件。
  4. 处理就绪文件描述符上的事件。

以下是epoll_wait的使用示例:

#include <sys/epoll.h>
int main() {
    // 创建一个epoll实例
    int epfd = epoll_create(1024);
    if (epfd == -1) {
        perror("epoll_create");
        return -1;
    }

    // 将需要监视的文件描述符注册到epoll实例中
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = 0; // 标准输入
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event) == -1) {
        perror("epoll_ctl");
        return -1;
    }

    // 等待事件
    struct epoll_event events[10];
    int nfds = epoll_wait(epfd, events, 10, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        return -1;
    }

    // 处理就绪文件描述符上的事件
    for (int i = 0; i < nfds; ++i) {
        if (events[i].events & EPOLLIN) {
            // 读取标准输入
            char buf[1024];
            int n = read(events[i].data.fd, buf, sizeof(buf));
            if (n == -1) {
                perror("read");
                return -1;
            }
            // ...
        }
    }
    return 0;
}

在上述示例中,我们创建了一个epoll实例,并将标准输入注册到epoll实例中。然后,我们调用epoll_wait函数,等待标准输入上的数据到达。如果标准输入上有数据到达,epoll_wait函数就会返回,并将就绪文件描述符的相关信息保存在events数组中。最后,我们遍历events数组,处理每个就绪文件描述符上的事件。

epoll_wait是Linux内核提供的一种高效的多路I/O复用机制。它可以提高程序的并发处理能力,减少CPU占用率。

ae_epoll.c文件中封装了一系列的epoll操作,包括epoll的创建、新增、删除、扩容、等待。

那这个非阻塞IO是怎么工作的?

核心关注aeApiPoll()

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
                        tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

epoll_wait等待FD的就绪通知,如果FD准备完毕,则进行数据流处理,否则就阻塞等待,在Redis启动时,会在main函数中创建一个死循环,轮询监听epoll事件,当有事件就绪时,执行事件的回调函数,即我们上一篇中所讲到的,具体的命令执行函数。

ae.c aeMain()

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    //省略部分非核心代码
    .....
        
		// 等待epoll就绪事件
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

	    /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            
            // 核心:执行命令对应的回调函数
            if (fe->mask & mask & AE_READABLE) {
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            // 核心:执行命令执行结果数据,写回客户端,回调函数
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

就此,命令执行流程的epoll部分,就此完成,我们还是用一张图描述整个执行过程:
redis命令执行

结语

本篇,我们对Redis源码中非阻塞的核心epoll是如何实现进行了浅析,简单了解了Redis中epoll的工作流程,至此,我们已经大体了解了Redis如何处理执行来自客户端的命令请求,但是还有一个问题我们没有清楚,Redis是如何将命令读取到的数据返回客户端的,下一篇中,我们将围绕这个问题,进行拆解,敬请期待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1412624.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024最新幻兽帕鲁服务器多少钱一个?

幻兽帕鲁服务器多少钱&#xff1f;价格便宜&#xff0c;阿里云4核16G幻兽帕鲁专属服务器32元1个月、66元3个月&#xff0c;4核32G配置113元1个月、339元3个月&#xff1b;腾讯云4核16G14M服务器66元1个月、277元3个月、1584元一年。阿腾云atengyun.com分享阿里云和腾讯云palwor…

Java面试题之序列化和反序列化

Java面试题之序列化和反序列化 文章目录 Java面试题之序列化和反序列化序列化和反序列化什么是序列化?什么是反序列化?如果有些字段不想进行序列化怎么办&#xff1f;常见序列化协议有哪些&#xff1f;为什么不推荐使用 JDK 自带的序列化&#xff1f; 文章来自Java Guide 用于…

快乐学Python,DataFrame的基本操作

在上一篇文章中&#xff0c;我们了解了如何使用 pandas 的函数来从多种数据源&#xff1a;csv、excel 和 html 网页。其中不管是哪一种数据读取的方式&#xff0c;最终返回的都是一个 DataFrame 对象。 对于 DataFrame 对象&#xff0c;我们只是简单将其打印出来&#xff0c;这…

Mac M1 Parallels CentOS7.9 Deploy 禅道

禅道官网下载地址: https://www.zentao.net/download/max4.10-83276.html 一、官网下载 二、解压安装 将下载好的包传至CentOS7.9虚拟机 zhinian192 ~ % scp Downloads/ZenTaoPMS-max4.10-zbox_arm64.tar.gz root10.211.55.36:~ ZenTaoPMS-max4.10-zbox_arm64.tar.gz …

分享5款专注于实用功能的小众软件

​ 电脑上的各类软件有很多&#xff0c;除了那些常见的大众化软件&#xff0c;还有很多不为人知的小众软件&#xff0c;专注于实用功能&#xff0c;简洁干净、功能强悍。 1.视频播放——Potplayer ​ Potplayer是一款功能强大的视频播放软件&#xff0c;支持各种格式的视频文…

Stable Diffusion插件Recolor实现黑白照片上色

今天跟大家分享一个使用Recolor插件通过SD实现老旧照片轻松变彩色&#xff0c;Recolor翻译过来的含义就是重上色&#xff0c;该模型可以保持图片的构图&#xff0c;它只会负责上色&#xff0c;图片不会发生任何变化。 一&#xff1a;插件下载地址 https://github.com/pkuliyi…

eNSP学习——利用三层交换机实现VLAN间路由

目录 背景 实验内容 实验目的 实验步骤 实验拓扑 实验编址 实验步骤 基本配置 配置三层交换机实现VLAN间通信 背景 虽说单臂路由可以实现不同VLAN之间主机的通信&#xff0c;但该技术存在一些局限性&#xff0c;比如带宽、转发效率等。 三层交换机在原有二层交换机…

2024最新Python安装教程且编写程序(一分钟版)

一分钟版 最近由于换了新电脑&#xff0c;所以重新安装Python 且Python也更新了多个版本&#xff0c;是时候更新啦 这是下载安装步骤的详细记录&#xff0c;望对需要学习Python的小伙伴有所帮助 那么我们在哪里下载呢&#xff1f; 打开这个网址 CNPM Binaries Mirror (npmmi…

2007-2022年全国货币供应量M2、失业率、CPI、第三方互联网支付、出口、人口等宏观经济指标数据(年度、季度)

2007-2022年全国货币供应量M2、失业率、CPI、第三方互联网支付、出口、人口等宏观经济指标数据&#xff08;年度、季度&#xff09; 1、时间&#xff1a;2007-2022年&#xff08;季度、年度&#xff09; 2、指标&#xff1a; 季度指标&#xff1a;时间、GDP不变价累计值(亿元…

使用EtherNET转Profinet网关配置EtherNET/IP地址说明

EtherNET转Profinet网关配置EtherNET/IP地址是将两种网络之间的连接进行设置和调整&#xff0c;以便实现数据的传输和信息的交互。这个过程中&#xff0c;需要对EtherNET/IP地址进行配置&#xff0c;以确保数据能够正确地在网络之间传递。通过配置EtherNET/IP地址&#xff0c;可…

为看清LED发光字而生!这款仪表仪盘摄像头最近火了

对于使用仪表仪盘、出入口道闸、地磅等设备的老板来说&#xff0c;想日常监控设备显示屏&#xff0c;但是因为反光、频闪等问题&#xff0c;常规摄像头无法看清LED显示屏文字。为了解决这一痛点&#xff0c;中维世纪专门推出定制款——仪表仪盘专用场景摄像机。 仪表仪盘专用场…

安装 nvm

前言&#xff1a; nvm 即 node 版本管理工具 (node version manager)&#xff0c;好处是方便切换 node.js 版本。 通过将多个 node 版本安装在指定路径&#xff0c;然后通过 nvm 命令切换时&#xff0c;就会切换我们环境变量中 node 命令指定的实际执行的软件路径。 使用场景…

Android App开发-简单控件(1)——文本显示

本章介绍了App开发常见的几类简单控件的用法&#xff0c;主要包括&#xff1a;显示文字的文本视图、容纳视图的常用布局、响应点击的按钮控件、显示图片的图像视图等。然后结合本章所涉及的知识&#xff0c;完成一个实战项目“简单计算器”的设计与实现。 1.1 文本显示 本节介绍…

Angular响应式表单表单验证触发另一个字段校验

Angular响应式表单校验联动 前言表单字段日期校验函数效果 前言 在某些业务场景中&#xff0c;校验某表单字段的同时也需要校验另外一个与之相关的字段&#xff0c;例如开始时间和结束时间&#xff0c;要求结束时间必须晚于开始时间。在angular 响应式表单中改如何实现该需求呢…

Numpy的学习 第三课 数组的相关数学使用

1. 聚合函数 注意这些都是针对数组或者其他来进行操作的 1. sum/min/max/average 我们一般只用前两个 第一个是数组&#xff0c;第二个是行 列 和之前一样&#xff0c;但是又不太一样 这里&#xff0c;0时&#xff0c;是每一列的值&#xff0c;1时&#xff0c;是每一行的…

DataKit迁移MySQL到openGauss

前言 本文将分享DataKit迁移MySQL到openGauss的项目实战&#xff0c;供广大openGauss爱好者参考。 1. 下载操作系统 https://www.openeuler.org/zh/download https://support.huawei.com/enterprise/zh/doc/EDOC1100332931/1a643956 https://support.huawei.com/enterprise…

3d gaussian splatting介绍整理

3D 高斯分布是用于实时辐射场渲染的 3D 高斯分布中描述的一种光栅化技术&#xff0c;它允许实时渲染从小图像样本中学习到的逼真场景。 paper github 本文翻译整理自&#xff1a; blog: Introduction to 3D Gaussian Splatting DDPMs - Part 2 给出一些2D图片&#xff0c;用…

uniapp组件库Line 线条 的适用方法

目录 #平台差异说明 #基本使用 #线条类型 1.3.7 #兼容性 #API #Props 此组件一般用于显示一根线条&#xff0c;用于分隔内容块&#xff0c;有横向和竖向两种模式&#xff0c;且能设置0.5px线条&#xff0c;使用也很简单。 #平台差异说明 AppH5微信小程序支付宝小程序百…

JVM系列-8.GC调优

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术、JVM原理&#x1f525;如果感觉博主的文…

C++哈希表模拟实现unordered_map 与unordered_set

哈希概念 unordered系列的关联式容器&#xff08;如unordered_map unordered_set&#xff09; 之所以效率比较高&#xff0c;是因为其底层使用了哈希结构 顺序结构以及平衡树 中&#xff0c;元素关键码与其存储位置之间没有对应的关系&#xff0c;因此在 查找一个元素 时&am…