Redis 采用事件驱动机制来处理大量的网络IO。它并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库 ae_event。

事件机制

Redis中的事件驱动库只关注网络IO,以及定时器。

该事件库处理下面两类事件:

  • 文件事件(file event):用于处理 Redis 服务器和客户端之间的网络IO。
  • 时间事件(time eveat):Redis 服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的。

事件驱动库的代码主要是在src/ae.c中实现的,其示意图如下所示。

aeEventLoop是整个事件驱动的核心,它管理着文件事件表和时间事件列表,不断地循环处理着就绪的文件事件和到期的时间事件。

文件事件

Redis基于Reactor模式开发了自己的网络事件处理器,也就是文件事件处理器。文件事件处理器使用IO多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数。

  • 1. 为什么单线程的 Redis 能那么快

Redis的瓶颈主要在IO而不是CPU,所以为了省开发量,在6.0版本前是单线程模型;其次,Redis 是单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。(但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的)。

Redis 采用了多路复用机制使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

  • 2. Redis事件响应框架ae_event及文件事件处理器

Redis并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库 ae_event。

Redis 使用的IO多路复用技术主要有:selectepollevportkqueue等。每个IO多路复用函数库在 Redis 源码中都对应一个单独的文件,比如ae_select.cae_epoll.cae_kqueue.c等。Redis 会根据不同的操作系统,按照不同的优先级选择多路复用技术。事件响应框架一般都采用该架构,比如 netty 和 libevent。

如下图所示,文件事件处理器有四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器以及事件处理器。

文件事件是对套接字操作的抽象,每当一个套接字准备好执行 acceptreadwriteclose 等操作时,就会产生一个文件事件。因为 Redis 通常会连接多个套接字,所以多个文件事件有可能并发的出现。

I/O多路复用程序负责监听多个套接字,并向文件事件派发器传递那些产生了事件的套接字。

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生的套接字都放到同一个队列(也就是后文中描述的aeEventLoop的fired就绪事件表)里边,然后文件事件处理器会以有序、同步、单个套接字的方式处理该队列中的套接字,也就是处理就绪的文件事件。

所以,一次 Redis 客户端与服务器进行连接并且发送命令的过程如上图所示。

  • 客户端向服务端发起建立 socket 连接的请求,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接请求
  • 进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器关联。
  • 客户端建立连接后,向服务器发送命令,那么客户端套接字将产生 AE_READABLE 事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。
  • 执行命令获得相应的命令回复,为了将命令回复传递给客户端,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联。当客户端试图读取命令回复时,客户端套接字产生 AE_WRITEABLE 事件,触发命令回复处理器将命令回复全部写入到套接字中。
  • 3. Redis IO多路复用模型

PS:了解处理流程后,我们有必要深入看下Redis IO多路复用的模型,正好我看到极客时间中《Redis核心技术与实战》中相关内容讲的挺容易理解的,就转过来了

在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

下图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

基于多路复用的Redis高性能IO模型为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。那么,回调机制是怎么工作的呢?

其实,select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

为了方便你理解,我再以连接请求和读数据请求为例,具体解释一下。

这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。

这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于 Redis 单线程),效率也能提升。

时间事件

Redis 的时间事件分为以下两类:

  • 定时事件:让一段程序在指定的时间之后执行一次。
  • 周期性事件:让一段程序每隔指定时间就执行一次。

Redis 的时间事件的具体定义结构如下所示。

typedef struct aeTimeEvent {/* 全局唯一ID */long long id; /* time event identifier. *//* 秒精确的UNIX时间戳,记录时间事件到达的时间*/long when_sec; /* seconds *//* 毫秒精确的UNIX时间戳,记录时间事件到达的时间*/long when_ms; /* milliseconds *//* 时间处理器 */aeTimeProc *timeProc;/* 事件结束回调函数,析构一些资源*/aeEventFinalizerProc *finalizerProc;/* 私有数据 */void *clientData;/* 前驱节点 */struct aeTimeEvent *prev;/* 后继节点 */struct aeTimeEvent *next;
} aeTimeEvent;

一个时间事件是定时事件还是周期性事件取决于时间处理器的返回值:

  • 如果返回值是 AE_NOMORE,那么这个事件是一个定时事件,该事件在达到后删除,之后不会再重复。
  • 如果返回值是非 AE_NOMORE 的值,那么这个事件为周期性事件,当一个时间事件到达后,服务器会根据时间处理器的返回值,对时间事件的 when 属性进行更新,让这个事件在一段时间后再次达到。

服务器所有的时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。正常模式下的Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件,所以不影响事件执行的性能。

aeEventLoop的具体实现

介绍完文件事件和时间事件,我们接下来看一下 aeEventLoop的具体实现;

创建事件管理器

Redis 服务端在其初始化函数 initServer中,会创建事件管理器aeEventLoop对象。

函数aeCreateEventLoop将创建一个事件管理器,主要是初始化 aeEventLoop的各个属性值,比如events、fired、timeEventHead和apidata:

  • 首先创建aeEventLoop对象。
  • 初始化未就绪文件事件表、就绪文件事件表。events指针指向未就绪文件事件表、fired指针指向就绪文件事件表。表的内容在后面添加具体事件时进行初变更。
  • 初始化时间事件列表,设置timeEventHead和timeEventNextId属性。
  • 调用aeApiCreate 函数创建epoll实例,并初始化 apidata。
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;eventLoop->aftersleep = NULL;/* 将多路复用io与事件管理器关联起来 */if (aeApiCreate(eventLoop) == -1) goto err;/* 初始化监听事件 */for (i = 0; i < setsize; i++)eventLoop->events[i].mask = AE_NONE;return eventLoop;
err:.....
}

aeApiCreate 函数首先创建了aeApiState对象,初始化了epoll就绪事件表;然后调用epoll_create创建了epoll实例,最后将该aeApiState赋值给apidata属性。

aeApiState对象中epfd存储epoll的标识,events是一个epoll就绪事件数组,当有epoll事件发生时,所有发生的epoll事件和其描述符将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。

static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));if (!state) return -1;/* 初始化epoll就绪事件表 */state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);if (!state->events) {zfree(state);return -1;}/* 创建 epoll 实例 */state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */if (state->epfd == -1) {zfree(state->events);zfree(state);return -1;}/* 事件管理器与epoll关联 */eventLoop->apidata = state;return 0;
}
typedef struct aeApiState {/* epoll_event 实例描述符*/int epfd;/* 存储epoll就绪事件表 */struct epoll_event *events;
} aeApiState;

创建文件事件

aeFileEvent是文件事件结构,对于每一个具体的事件,都有读处理函数和写处理函数等。Redis 调用aeCreateFileEvent函数针对不同的套接字的读写事件注册对应的文件事件。

typedef struct aeFileEvent {/* 监听事件类型掩码,值可以是 AE_READABLE 或 AE_WRITABLE */int mask;/* 读事件处理器 */aeFileProc *rfileProc;/* 写事件处理器 */aeFileProc *wfileProc;/* 多路复用库的私有数据 */void *clientData;
} aeFileEvent;
/* 使用typedef定义的处理器函数的函数类型 */
typedef void aeFileProc(struct aeEventLoop *eventLoop, 
int fd, void *clientData, int mask);

比如说,Redis 进行主从复制时,从服务器需要主服务器建立连接,它会发起一个 socekt连接,然后调用aeCreateFileEvent函数针对发起的socket的读写事件注册了对应的事件处理器,也就是syncWithMaster函数。

aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL);
/* 符合aeFileProc的函数定义 */
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {....}

aeCreateFileEvent的参数fd指的是具体的socket套接字,proc指fd产生事件时,具体的处理函数,clientData则是回调处理函数时需要传入的数据。

aeCreateFileEvent主要做了三件事情:

  • 以fd为索引,在events未就绪事件表中找到对应事件。
  • 调用aeApiAddEvent函数,该事件注册到具体的底层 I/O 多路复用中,本例为epoll。
  • 填充事件的回调、参数、事件类型等参数。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{/* 取出 fd 对应的文件事件结构, fd 代表具体的 socket 套接字 */aeFileEvent *fe = &eventLoop->events[fd];/* 监听指定 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;
}

如上文所说,Redis 基于的底层 I/O 多路复用库有多套,所以aeApiAddEvent也有多套实现,下面的源码是epoll下的实现。其核心操作就是调用epoll的epoll_ctl函数来向epoll注册响应事件。

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee = {0}; /* avoid valgrind warning *//* 如果 fd 没有关联任何事件,那么这是一个 ADD 操作。如果已经关联了某个/某些事件,那么这是一个 MOD 操作。 */int op = eventLoop->events[fd].mask == AE_NONE ?EPOLL_CTL_ADD : EPOLL_CTL_MOD;/* 注册事件到 epoll */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.fd = fd;/* 调用epoll_ctl 系统调用,将事件加入epoll中 */if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;return 0;
}

事件处理

因为 Redis 中同时存在文件事件和时间事件两个事件类型,所以服务器必须对这两个事件进行调度,决定何时处理文件事件,何时处理时间事件,以及如何调度它们。

aeMain函数以一个无限循环不断地调用aeProcessEvents函数来处理所有的事件。

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {/* 如果有需要在事件处理前执行的函数,那么执行它 */if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);/* 开始处理事件*/aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);}
}

下面是aeProcessEvents的伪代码,它会首先计算距离当前时间最近的时间事件,以此计算一个超时时间;然后调用aeApiPoll函数去等待底层的I/O多路复用事件就绪;aeApiPoll函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。

/* 伪代码 */
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {/* 获取到达时间距离当前时间最接近的时间事件*/time_event = aeSearchNearestTimer();/* 计算最接近的时间事件距离到达还有多少毫秒*/remaind_ms = time_event.when - unix_ts_now();/* 如果事件已经到达,那么remaind_ms为负数,将其设置为0 */if (remaind_ms < 0) remaind_ms = 0;/* 根据 remaind_ms 的值,创建 timeval 结构*/timeval = create_timeval_with_ms(remaind_ms);/* 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 结构决定,如果remaind_ms 的值为0,则aeApiPoll 调用后立刻返回,不阻塞*//* aeApiPoll调用epoll_wait函数,等待I/O事件*/aeApiPoll(timeval);/* 处理所有已经产生的文件事件*/processFileEvents();/* 处理所有已经到达的时间事件*/processTimeEvents();
}

与aeApiAddEvent类似,aeApiPoll也有多套实现,它其实就做了两件事情,调用epoll_wait阻塞等待epoll的事件就绪,超时时间就是之前根据最快达到时间事件计算而来的超时时间;然后将就绪的epoll事件转换到fired就绪事件。aeApiPoll就是上文所说的I/O多路复用程序。具体过程如下图所示。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) 
{aeApiState *state = eventLoop->apidata;int retval, numevents = 0;// 调用epoll_wait函数,等待时间为最近达到时间事件的时间计算而来。retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);// 有至少一个事件就绪?if (retval > 0) {int j;/*为已就绪事件设置相应的模式,并加入到 eventLoop 的 fired 数组中*/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;
}

processFileEvent是处理就绪文件事件的伪代码,也是上文所述的文件事件分派器,它其实就是遍历fired就绪事件表,然后根据对应的事件类型来调用事件中注册的不同处理器,读事件调用rfileProc,而写事件调用wfileProc。

void processFileEvent(int numevents) {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 fired = 0;int invert = fe->mask & AE_BARRIER;/* 读事件 */if (!invert && fe->mask & mask & AE_READABLE) {/* 调用读处理函数 */fe->rfileProc(eventLoop,fd,fe->clientData,mask);fired++;}/* 写事件. */if (fe->mask & mask & AE_WRITABLE) {if (!fired || fe->wfileProc != fe->rfileProc) {fe->wfileProc(eventLoop,fd,fe->clientData,mask);fired++;}}if (invert && fe->mask & mask & AE_READABLE) {if (!fired || fe->wfileProc != fe->rfileProc) {fe->rfileProc(eventLoop,fd,fe->clientData,mask);fired++;}}processed++;}}
}

而processTimeEvents是处理时间事件的函数,它会遍历aeEventLoop的事件事件列表,如果时间事件到达就执行其timeProc函数,并根据函数的返回值是否等于AE_NOMORE来决定该时间事件是否是周期性事件,并修改器到达时间。

static int processTimeEvents(aeEventLoop *eventLoop) {int processed = 0;aeTimeEvent *te;long long maxId;time_t now = time(NULL);....eventLoop->lastTime = now;te = eventLoop->timeEventHead;maxId = eventLoop->timeEventNextId-1;/* 遍历时间事件链表 */while(te) {long now_sec, now_ms;long long id;/* 删除需要删除的时间事件 */if (te->id == AE_DELETED_EVENT_ID) {aeTimeEvent *next = te->next;if (te->prev)te->prev->next = te->next;elseeventLoop->timeEventHead = te->next;if (te->next)te->next->prev = te->prev;if (te->finalizerProc)te->finalizerProc(eventLoop, te->clientData);zfree(te);te = next;continue;}/* id 大于最大maxId,是该循环周期生成的时间事件,不处理 */if (te->id > maxId) {te = te->next;continue;}aeGetTime(&now_sec, &now_ms);/* 事件已经到达,调用其timeProc函数*/if (now_sec > te->when_sec ||(now_sec == te->when_sec && now_ms >= te->when_ms)){int retval;id = te->id;retval = te->timeProc(eventLoop, id, te->clientData);processed++;/* 如果返回值不等于 AE_NOMORE,表示是一个周期性事件,修改其when_sec和when_ms属性*/if (retval != AE_NOMORE) {aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);} else {/* 一次性事件,标记为需删除,下次遍历时会删除*/te->id = AE_DELETED_EVENT_ID;}}te = te->next;}return processed;
}

删除事件

当不在需要某个事件时,需要把事件删除掉。例如: 如果fd同时监听读事件、写事件。当不在需要监听写事件时,可以把该fd的写事件删除。

aeDeleteEventLoop函数的执行过程总结为以下几个步骤

  • 根据fd在未就绪表中查找到事件
  • 取消该fd对应的相应事件标识符
  • 调用aeApiFree函数,内核会将epoll监听红黑树上的相应事件监听取消。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux基础开发工具

目录 1.写在前面 2.权限 3.file命令 4.基础开发工具 1.软件包管理器 5.编辑器vim 1.写在前面 我们在上一讲解中讲解了权限是人事物属性&#xff0c;还知道了拥有者所属组其他人这三个概念&#xff0c;知道了33一组&#xff0c;rwx分别代表什么。那么下面我们继续进行权限…

ICCV2025 特征点检测 图像匹配 RIPE

目测对刚性物体效果比较好代码&#xff1a;https://github.com/fraunhoferhhi/RIPE 论文&#xff1a;https://arxiv.org/abs/2507.04839import cv2 import kornia.feature as KF import kornia.geometry as KG import matplotlib.pyplot as plt import numpy as np import torc…

Ubuntu22.0.4安装PaddleNLP

Ubuntu22.0.4安装PaddleNLP环境说明安装底层框架Paddle安装PddleNLP1. pip安装2. 验证安装3. 最后问题集锦环境说明 1. miniconda 25.5.1 2. python 3.12.11 3. pip 25.1 4. nvidia 570.144 5. cuda 12.8**注意&#xff1a;**安装过程可能遇到的一些问题&#xff0c;参考末尾的…

【HTTP服务端】Cookie?Session?Token?

文章目录cookie与sessiontoken什么是JWT&#xff1f;JWT的组成结构1. Header&#xff08;头部&#xff09;2. Payload&#xff08;负载&#xff09;3. Signature&#xff08;签名&#xff09;JWT工作原理JWT的特点安全注意事项cookie与session cookie有哪些属性 键值对&#xf…

安装Git

Git安装避坑指南技术 操作系统选择与准备 Windows用户需注意系统版本兼容性&#xff0c;建议使用Windows 10及以上版本 Mac用户需检查是否安装Xcode Command Line Tools Linux用户需区分apt/yum等包管理器命令差异 安装包下载注意事项 从官方渠道&#xff08;git-scm.com&a…

UDP服务器的优缺点都包含哪些?

UDP协议不需要像TCP协议那样进行复杂的连接建立与拆除过程&#xff0c;在进行传输数据信息的过程中&#xff0c;应用层将数据交给UDP层&#xff0c;UDP层直接加上首部就发往网络层&#xff0c;极大地减少了处理时间和资源消耗。例如在一些简单的网络监控程序中&#xff0c;只是…

sqli-labs靶场通关笔记:第7-8关 布尔盲注

第七关1.审题这里判断出是))闭合&#xff0c;但是页面只有正确和错误的回显状态&#xff0c;报错的回显也是固定的&#xff0c;没有显示报错具体信息。这关使用的方法是布尔盲注。为什么叫布尔盲注&#xff1f;因为它返回的结果只有true和false 两个值&#xff0c;攻击者需要通…

理解支持向量机(SVM):理论、数学和实现的综合指南

支持向量机&#xff08;SVMs&#xff09;是强大的监督学习算法&#xff0c;用于分类和回归任务&#xff0c;尽管它们主要用于分类。由Vladimir Vapnik及其同事在1990年代引入&#xff0c;SVMs基于统计学习理论&#xff0c;特别适用于需要将数据点稳健分离到不同类别的任务。本博…

使用Navicat对PostgreSQL数据表添加列,自动记录当前行的添加日期

点开表设计&#xff0c;向如下这样一个字段&#xff1a; 字段名称可以自定义&#xff0c;博主这里叫做&#xff1a;add_date_time类型选择&#xff1a;timestamp长度写成&#xff1a;6默认值输入&#xff1a;CURRENT_TIMESTAMP 添加行&#xff1a;默认值&#xff1a;

VR协作海外云:跨国企业沉浸式办公解决方案

随着全球化进程加速&#xff0c;VR协作海外云正成为跨国企业数字化转型的核心解决方案。本文将深入解析这项技术如何突破地理限制&#xff0c;实现沉浸式远程协作&#xff0c;并探讨其在跨文化团队管理、实时3D数据交互等场景中的独特优势。 VR协作海外云&#xff1a;跨国企业沉…

[ESP32]VSCODE+ESP-IDF环境搭建及blink例程尝试(win10 win11均配置成功)

ps:这是你为了点灯最繁琐的一次 1.软件下载 vscode下载地址&#xff1a;Documentation for Visual Studio Codeesp_idf下载地址&#xff1a;https://dl.espressif.cn/dl/esp-idf/?idf4.4 (从上往下第三&#xff09; 2.软件安装 可以均默认安装&#xff0c;但建议不要放在C盘&…

tailwindcss详解

Tailwind CSS 详解&#xff1a;实用主义的现代 CSS 框架 Tailwind CSS 是一个功能优先&#xff08;utility-first&#xff09;的 CSS 框架&#xff0c;它通过提供低级别的实用类来快速构建自定义设计&#xff0c;而无需离开 HTML 文件。以下是全面解析&#xff1a; 一、核心概念…

[spring6: TypeFilter MetadataReader MetadataReaderFactory]-源码解析

源码 MetadataReaderFactory MetadataReaderFactory 是用于创建 MetadataReader 实例的工厂接口&#xff0c;支持通过类名或资源读取类的元数据并可实现缓存优化。类型类/接口名功能描述是否需要加载类访问方式抽象接口AnnotatedTypeMetadata访问某类型&#xff08;类或方法&am…

基于redis的分布式session共享管理之销毁事件不生效问题

一、前言首先介绍下分布式session共享管理在Springboot项目中&#xff0c;经常提到分布式的概念&#xff0c;当实际部署应用后&#xff0c;多台服务器各自存储用户登录会话无法共享&#xff0c;导致操作A按钮还是正常&#xff0c;操作B按钮就提示登录过期需要重新登录。这是因为…

技术面试问题总结二

一、lvs的四种工作模式: LVS 有四种主要工作模式&#xff1a;NAT 模式、DR 模式、TUN 模式和Full-NAT 模式 1、NAT模式&#xff1a; 工作原理 LVS 作为客户端和真实服务器&#xff08;RS&#xff09;之间的中间节点&#xff0c;接收客户端请求后&#xff0c;修改请求的目标…

软考(软件设计师)软件工程-软件过程模型,敏捷开发

软件过程模型 瀑布模型 #mermaid-svg-daxck2eQmqfYelkV {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-daxck2eQmqfYelkV .error-icon{fill:#552222;}#mermaid-svg-daxck2eQmqfYelkV .error-text{fill:#552222;stro…

MySQL 中图标字符存储问题探究:成因、解决方案及单字段编码调整的利弊分析——仙盟创梦IDE

在 MySQL 数据库应用中&#xff0c;常出现无法正确保存图标字符&#xff0c;读出时显示为 “????” 的问题。本文深入剖析了该问题产生的原因&#xff0c;主要涉及字符编码设置不匹配等因素。同时&#xff0c;提出了全面的解决方案&#xff0c;包括全局和单字段的字符编码调…

快速上手UniApp(适用于有Vue3基础的)

作为一位有Vue3基础的开发者&#xff0c;学习UniApp将会是一个相对平滑的过程。UniApp是一个使用Vue.js开发跨平台应用的前端框架&#xff0c;可以编译到iOS、Android、H5以及各种小程序平台。 一、UniApp简介 UniApp是基于Vue.js的跨平台开发框架&#xff0c;具有以下特点&a…

background和background-color的区别

前言&#xff1a;由于全局切换变量时&#xff0c;发现空页面按钮变量颜色未生效&#xff0c;审查元素发现变量未定义。实际上是背景色由纯色变成了渐变色&#xff0c;而background-color不支持渐变色导致变量不生效特性backgroundbackground-color功能设置‌所有‌背景属性&…

Vue Vue-route (5)

Vue 渐进式JavaScript 框架 基于Vue2的学习笔记 - Vue-route History模式和路由懒加载 目录 History模式 设置history模式 后端配置 Apache 路由懒加载 配置 总结 History模式 设置history模式 Vue-route默认hash模式——使用URL的hash来模拟一个完整的URL&#xff0c…