Redis的高性能之道

        前言:做码农这么多年,我也读过很多开源软件或者框架的源码,在我看来,Redis是我看过写得最优美、最像一件艺术品的软件,正如Redis之父自己说的那样,他宁愿以一个糟糕的艺术家身份而不是一名好程序员被别人记住,我认为他不仅做到了,而且还是一个非常高超的艺术家。记得当年看源码时,我不禁发出感叹:原来用C语言也可以写出如此美妙、优雅的代码来!

        Redis经过十多年的发展,几乎是所有公司必不可少的中间件了,要么用其做缓存,要么做数据统计,或者分布式锁,有些电商还会用Redis做购物车(突然让我想起来,那是在2019年,前东家,当时我们用的是codis搭的Redis集群,Redis集群挂了,导致大促时大家都操作不了购物车,这家把老板急的,现在还印象深刻,哈哈。)。Redis之所以如此受到欢迎,主要还是得益于其丰富的数据结构和较高的性能。Redis官方给出了单机的Redis性能,在忽略带宽影响的前提下,Redis的单机QPS可达到10万QPS。下图是使用Redis benchmark做的性能测试:

Redis的卓越性能主要原于以下几个方面:

  • 使用内存进行数据读写;
  • IO多路复用,复用思想提高请求处理能力;
  • 后台线程处理耗时任务,异步线程不阻塞主线程;
  • IO多线程,进一步提升网络请求处理性能;

1、使用内存

        多数情况下我们编写的计算机任务都是IO密集型的,80%甚至更多的时间都是在处理各种IO,磁盘IO,网络IO等等,Redis为了提升读写性能,让主线程的所有读写完全是在内存中进行,从而不需要数据磁盘和内核缓冲区之间的拷贝,我认为这是其性能较好的最重要原因。

        虽然Redis使用内存存储,但和Memcached不同的是,Redis是支持持久化的,即内存中的数据是可以持久化到磁盘文件中,从而可保证在断电重启后恢复数据以及支持主从复制。Redis的持久化包含RDB以及AOF持久化两种。

        RDB类似Mysql基于Row模式的binlog,即存储的是数据本身,是一种数据快照,RDB在全量复制以及断电恢复中非常有用,由于都是数据,所以恢复较快。Redis生成RDB文件的过程并不是在主进程中完成,其会fork出一个子进程来完成RDB文件生成,并不会阻塞主进程,此外其还充分利用了写时复制(COW)的机制,fork的子进程依然会和父进程共享物理内存,因此不会影响主进程处理命令的性能(这里不考虑内存等因素的影响),实现命令是bgsave。

        AOF(Append only file),文件记录格式类似于Mysql的基于statment记录的binlong,即记录的是执行命令,当然其只会记录写命令,读命令不会记录。当执行完写命令后,会直接返回客户端,随后主线程还是会将命令写入到AOF文件中,这个过程不会影响当前命令,但会影响下一个命令,因为其是在主进程中进行的。写完AOF之后,实际上此时数据是在PageCache中(我在之前的Linux零拷贝技术浅谈中介绍过),随后Redis可以主动地将数据刷盘,比如每秒刷一次(fsync,这个也是后台线程处理的),也可以同步刷,但会影响性能,也可以直接不管,由操作系统刷盘。

        另外一个需要提的是AOF的重写,AOF如果不断地追加命令,会导致文件过大,因此为了缩小AOF的文件,会进行重写,重写的目的是将命令进行整合,比如对同一个Key的写命令整合成一个,从而缩小文件大小。重写的操作也是通过子进程完成的,并不会阻塞主进程。

2、Redis的I/O多路复用

        Redis采用单线程IO多路复用来实现高并发,即著名的Reactor单线程模式,默认会使用epoll机制,通过回调函数、红黑树、mmap机制等各种骚操作提升请求处理能力。其中epoll_create用来创建epoll对象,epoll_ctl用于将就绪事件添加到epoll对象中,epoll_wait会检查对象中是否存在就绪事件,如果有就会把数据拷贝从内核拷贝到用户空间。

具体的关于多路复用和Reactor模式的介绍可以看我之前的文章:IO多路复用介绍; Java的NIO及Netty 。        

        这里可能你有疑问,为啥Redis另辟蹊径,只使用单线程的IO多路复用,这也无法充分发挥多核的优势啊?无法使用是多核是真的,但Redis由于本身使用内存读写数据,因此多数情况下瓶颈并不在CPU上,且使用单线程读写数据并不需要考虑线程安全性,不必加锁,一定程度简化了Redis的复杂度,这也算是作者做的一种权衡。可是,聪明的你,可能还会继续持怀疑态度,如果执行命令耗时较长不会阻塞吗? 网络请求过大时,网络IO还是会阻塞啊? 是的,对待这两个问题,Redis在后续版本中都给了解决方案,对应的就是第三和第四小节。

        下面是我在网上发现一个清晰得描述Redis的IO多路复用实现机制得示意图(来自飞哥,开发内功修炼):

Redis服务端的处理流程:

  1. Redis 服务启动调用main函数,initServer主要进行主线程事件循环aeCreateEventLoop,注册 acceptTcpHandler 函数,等待新连接到来。看到eventLoop大家应该熟悉,在Netty中你也会看到的,这是IO多路复用的核心;
  2. 客户端和服务端建立Socket连接;
  3. Redis Server多路复用接收请求并调用acceptTcpHandler 函数,最终调用 readQueryFromClient 命令读取并解析客户端连接;
  4. 客户端发送请求,触读就绪事件,主线程调用 readQueryFromClient 读取客户端命令,,并写入querybuf 读入缓冲区;
  5. 接着调用 processInputBuffer,随后调用 processCommand 执行命令;
  6. 执行命令后,将响应数据写入到对应客户端的写缓冲区,固定大小 16KB,一般来说可以缓冲足够多的响应数据,但是如果客户端在时间窗口内需要响应的数据非常大,那么则会自动切换到 client->reply 链表上去,最后把 client 添加进一个 LIFO 队列 clients_pending_write;
  7. 在事件(文件或定时)循环中,主线程执行 beforeSleep --> handleClientsWithPendingWrites,遍历 clients_pending_write 队列,调用 writeToClient 把 客户端写出缓冲区里的数据发送到客户端,如果写出缓冲区还有数据遗留,则注册 sendReplyToClient 命令回复处理器到该连接的写就绪事件,等待客户端可写时在事件循环中再继续回写剩余的响应数据。

        接下来具体看看Redis的实现,入口是src/server.c的main函数。

        1、这个函数会进行环境设置、初始化参数、数据恢复等等一系列操作。

int main(int argc, char **argv) {initServer();aeMain(server.el);aeDeleteEventLoop(server.el);return 0;
}

initServer用于创建epoll,注册定时和文件事件,这是Redis的最重要两类事件。

  • 文件事件(file event):Redis客户端通过socket与Redis服务器连接,而文件事件就是服务器对套接字操作的抽象。例如,客户端发了一个GET命令请求,对于Redis服务器来说就是一个文件事件。Redis的协议本身发送的是文本。
  • 时间事件(time event):服务器定时或周期性执行的事件。例如,定期执行RDB持久化。
  //创建epoll对象server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);

epoll对象创建过程:

aeEventLoop *aeCreateEventLoop(int setsize) {aeEventLoop *eventLoop;int i;if (aeApiCreate(eventLoop) == -1) goto err;return eventLoop;
}

重要的就是这句aeApiCreate,其用于创建实际的epoll对象。

static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));//露出真面目了state->epfd = epoll_create(1024); eventLoop->apidata = state;return 0;
}

2、随后开始注册回调行数,initServer中注册的函数acceptTcpHandler,当有新连接到来时,该函数会被执行。

  //注册定时事件,用于执行后台一些业务,如定时清理if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {serverPanic("Can't create event loop timers.");exit(1);}//创建文件事件处理器,用于处理tcp连接。for (j = 0; j < server.ipfd_count; j++) {if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL) == AE_ERR){serverPanic("Unrecoverable error creating server.ipfd file event.");}}

再看一下aeCreateFileEvent函数:

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{//关键的两句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;return AE_OK;
}

aeApiAddEvent会将文件描述符添加到epoll队列中,实际上调用的就是epoll_ctl。至此,Server初始化基本完成。

接着执行aeMain过程,进入死循环:

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_BEFORE_SLEEP|AE_CALL_AFTER_SLEEP);}
}

aeProcessEvents调用epoll_wait阻塞,等待事件就绪。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{//这里是调用epoll_wait阻塞numevents = aeApiPoll(eventLoop, tvp);for (j = 0; j < numevents; j++) {// 从已就绪队列中获取事件aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];//如果是读事件,并且有读回调函数fe->rfileProc()//如果是写事件,并且有写回调函数fe->wfileProc()}}
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {// 等待事件aeApiState *state = eventLoop->apidata;epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);...
}

这里具体实现机制取决于系统支持哪些,如果不支持epoll,默认会使用select。

假如现在有新连接到来了,此时会调用已注册的acceptTCPHandler函数,我们看下其具体处理流程。

acceptTCPHandler:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {while(max--) {//调用accept接收连接cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);}
}

2、然后执行acceptCommonHandler

static void acceptCommonHandler(connection *conn, int flags, char *ip) {//创建Redis客户端if ((c = createClient(conn)) == NULL) {connClose(conn); /* May be already closed, just ignore errors */return;}
}

3、创建连接客户端

client *createClient(connection *conn) {client *c = zmalloc(sizeof(client));/* passing NULL as conn it is possible to create a non connected client.* This is useful since all the commands needs to be executed* in the context of a client. When commands are executed in other* contexts (for instance a Lua script) we need a non connected client. */if (conn) {connNonBlock(conn);connEnableTcpNoDelay(conn);if (server.tcpkeepalive)connKeepAlive(conn,server.tcpkeepalive);//画重点connSetReadHandler(conn, readQueryFromClient);connSetPrivateData(conn, c);}
}

上面首先创建了一个读事件。

connSetReadHandler(conn, readQueryFromClient);

4、从客户端读取数据

readQueryFromClient

该函数负责读取客户端命令。

5、调用processCommand执行命令,执行具体操作:

int processCommandAndResetClient(lient *c) {if (processCommand(c) == C_OK) {commandProcessed(c);}
}

注意:Redis处理之后,并不是直接将数据返给客户端,而是先加入到写任务队列,在每次循环,首先进行数据发送。

3、后台线程

        上面提到了, 由于Redis的命令是单线程的,那如果某个命令出现了阻塞,就会影响到所有命令的执行。就拿我们公司去年发起的几个事故举例,有几个系统创建了big key,且对big key进行批量操作,这直接导致请求量较高时,Redis出现了严重的阻塞。这也是为什么使用Redis时,我们要禁止耗时操作,包括执行keys ,flush,执行批量操作,创建big key等。

        为了解决Redis内置命令的一些耗时操作影响主线程,自Redis4.0之后,Redis增加了异步线程的支持,使得一些比较耗时的任务可以在后台异步线程执行,不必再阻塞主线程。之前del和flush等都会阻塞主线程,现在的ulink,flushal async, flushdb async等操作都不会再阻塞。

        异步线程是通过Redis的bio实现,即Background I/O,不是我们大家经常说的阻塞IO啊,哈哈。

Redis在启动时,在后台会初始化三个后台线程。

void InitServerLast() {bioInit();initThreadedIO();set_jemalloc_bg_thread(server.jemalloc_bg_thread);server.initial_memory_usage = zmalloc_used_memory();
}

bioInit就是具体启动后台线程过程。启动的线程主要包括:

#define BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
#define BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
#define BIO_LAZY_FREE     2 /* Deferred objects freeing. */

即用来关闭文件描述符、AOF持久化以及惰性删除。这三个线程是完全独立的,互不干涉。每个线程都会有一个工作队列,用于生产和消费任务。图片来源小林coding:

for (j = 0; j < BIO_NUM_OPS; j++) {void *arg = (void*)(unsigned long) j;if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");exit(1);}bio_threads[j] = thread;}

        主线程负责把相关任务添加到对应线程的队列中,在添加和移除队列中都会加锁,防止并发问题。比如惰性删除的过程,即我们使用UNLINK,flushDB,flushall等命令时。


//添加任务到对应线程队列中,添加过程会加锁
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {struct bio_job *job = zmalloc(sizeof(*job));//加锁pthread_mutex_lock(&bio_mutex[type]);//将任务加到队列尾部listAddNodeTail(bio_jobs[type],job);pthread_mutex_unlock(&bio_mutex[type]);
}

        接下来就是后台线程会从对应队列中取出任务执行:

void *bioProcessBackgroundJobs(void *arg) {struct bio_job *job;unsigned long type = (unsigned long) arg;sigset_t sigset;switch (type) {case BIO_CLOSE_FILE:redis_set_thread_title("bio_close_file");break;case BIO_AOF_FSYNC:redis_set_thread_title("bio_aof_fsync");break;case BIO_LAZY_FREE:redis_set_thread_title("bio_lazy_free");break;}

4、IO多线程

        上面已提到Redis主要采用单线程IO多路复用实现高并发,后来为了处理耗时比较长的任务,Redis4.0引入了BackGround I/O线程。本身Redis的瓶颈并不是在于CPU,而是内存和网络IO。在一定程度上通过扩容即可。但是业务量不断扩大时,网络IO的瓶颈就体现出来了。这个时候使用多线程处理还是挺香的,可以充分发挥多核的优势,因此Redis6.0就引入了多线程。

        不过,这里强调一点,Redis引入了多线程,仅仅是用来网络读写,Redis命令的执行还是通过主线程顺序执行,这主要是为了减少Redis操作的复杂度等方面。

        上面在说启动Background IO时,说到了InitServerLast,里面有个initThreadedIO,这个就是初始化线程IO的过程。

初始化线程IO的实现:

void initThreadedIO(void) {server.io_threads_active = 0; /* We start with threads not active. *//* Spawn and initialize the I/O threads. */for (int i = 0; i < server.io_threads_num; i++) {/* Things we do for all the threads including the main thread. */io_threads_list[i] = listCreate();//第一个是主线程,创建完continueif (i == 0) continue; /* Thread 0 is the main thread. *//* Things we do only for the additional threads. *///worker线程注册回调函数。pthread_t tid;pthread_mutex_init(&io_threads_mutex[i],NULL);setIOPendingCount(i, 0);pthread_mutex_lock(&io_threads_mutex[i]); /* Thread will be stopped. *///创建线程,并注册回调函数IOThreadMainif (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");exit(1);}io_threads[i] = tid;}
}

看上面代码,第一个是主线程,其他的都是Worker Thread。

一个图形可以反映上面的实现:

        从上面示意图可以看出来,IO处理已经由直线的单线程IO多路复用,改成多线程的IO多路复用处理,在网络IO处理中充分发挥多核的优势。

        接下来具体看一下他是怎么实现多线程处理的。其实请求处理流程还是和上面的一致,只是在readQueryFromClient有所不同。

void readQueryFromClient(connection *conn) {client *c = connGetPrivateData(conn);int nread, readlen;size_t qblen;/* Check if we want to read from the client later when exiting from* the event loop. This is the case if threaded I/O is enabled. */if (postponeClientRead(c)) return;...
}

如果是开启了线程IO,PostoneClientRead会把事件加入到队列中,待主线程分配给工作线程执行。

/* Return 1 if we want to handle the client read later using threaded I/O.* This is called by the readable handler of the event loop.* As a side effect of calling this function the client is put in the* pending read clients and flagged as such. */
int postponeClientRead(client *c) {if (server.io_threads_active &&server.io_threads_do_reads &&!ProcessingEventsWhileBlocked &&!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ))){c->flags |= CLIENT_PENDING_READ;listAddNodeHead(server.clients_pending_read,c);return 1;} else {return 0;}
}

接着,在beforeSleep中会调用处理函数,多线程处理read操作。注意下面的函数注解,主线程也会处理一部分网络IO,同时IO线程也会并发处理,主线程会一直等到所有IO线程执行完。

int handleClientsWithPendingReadsUsingThreads(void) {if (!server.io_threads_active || !server.io_threads_do_reads) return 0;int processed = listLength(server.clients_pending_read);if (processed == 0) return 0;if (tio_debug) printf("%d TOTAL READ pending clients\n", processed);/* Distribute the clients across N different lists. */listIter li;listNode *ln;listRewind(server.clients_pending_read,&li);int item_id = 0;while((ln = listNext(&li))) {client *c = listNodeValue(ln);int target_id = item_id % server.io_threads_num;listAddNodeTail(io_threads_list[target_id],c);item_id++;}/* Give the start condition to the waiting threads, by setting the* start condition atomic var. */io_threads_op = IO_THREADS_OP_READ;for (int j = 1; j < server.io_threads_num; j++) {int count = listLength(io_threads_list[j]);setIOPendingCount(j, count);}/* Also use the main thread to process a slice of clients. */listRewind(io_threads_list[0],&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);readQueryFromClient(c->conn);}listEmpty(io_threads_list[0]);//阻塞等待所有IO线程执行完毕while(1) {unsigned long pending = 0;for (int j = 1; j < server.io_threads_num; j++)pending += getIOPendingCount(j);if (pending == 0) break;}if (tio_debug) printf("I/O READ All threads finshed\n");//开始执行命令while(listLength(server.clients_pending_read)) {ln = listFirst(server.clients_pending_read);client *c = listNodeValue(ln);c->flags &= ~CLIENT_PENDING_READ;listDelNode(server.clients_pending_read,ln);if (c->flags & CLIENT_PENDING_COMMAND) {c->flags &= ~CLIENT_PENDING_COMMAND;if (processCommandAndResetClient(c) == C_ERR) {/* If the client is no longer valid, we avoid* processing the client later. So we just go* to the next. */continue;}}processInputBuffer(c);}return processed;
}

主线程阻塞等待所有的工作线程都完成之后,开始串行执行命令,随后IO线程可以并行将数据发送到写任务队列。

下面是work线程的主要处理逻辑:

void *IOThreadMain(void *myid) {/* The ID is the thread number (from 0 to server.iothreads_num-1), and is* used by the thread to just manipulate a single sub-array of clients. */long id = (unsigned long)myid;char thdname[16];snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);redis_set_thread_title(thdname);redisSetCpuAffinity(server.server_cpulist);makeThreadKillable();while(1) {/* Wait for start */for (int j = 0; j < 1000000; j++) {if (getIOPendingCount(id) != 0) break;}/* Give the main thread a chance to stop this thread. */if (getIOPendingCount(id) == 0) {pthread_mutex_lock(&io_threads_mutex[id]);pthread_mutex_unlock(&io_threads_mutex[id]);continue;}serverAssert(getIOPendingCount(id) != 0);if (tio_debug) printf("[%ld] %d to handle\n", id, (int)listLength(io_threads_list[id]));/* Process: note that the main thread will never touch our list* before we drop the pending count to 0. */listIter li;listNode *ln;listRewind(io_threads_list[id],&li);
//处理读写while((ln = listNext(&li))) {client *c = listNodeValue(ln);//如果是写,执行writeToClientif (io_threads_op == IO_THREADS_OP_WRITE) {writeToClient(c,0);//读操作} else if (io_threads_op == IO_THREADS_OP_READ) {readQueryFromClient(c->conn);} else {serverPanic("io_threads_op value is unknown");}}listEmpty(io_threads_list[id]);setIOPendingCount(id, 0);if (tio_debug) printf("[%ld] Done\n", id);}
}

从上面可以看到,IO线程要么同时读,要么同时写,不可同时包含读和写两部分。

通过ITNEXT平台的测试报告中,可以看到在相同机器配置下,加入多线程后,Redis可支持的最大QPS达到 20W/s。

        Redis这部分和Memcached的思想有些类似,Memcached也是通过主线程IO多路复用接受连接,并通过一定算法分配到工作线程。但是,最大的不同是,Redis的多线程只是为了处理网络读写,不负责处理具体业务逻辑,命令还是主线程顺序执行的。然而Memcached是Master线程把连接分配给Worker之后,Worker线程就负责把处理后续的所有请求,完全是多线程执行。Redis要想做到这一点,需要做的工作还有很多,尤其是线程安全方面。据说或许要逐步加Key-level的锁,肯定是不断地完善的。

总结

        本文主要介绍了redis的高性能解决方案,包括内存、IO多路复用、后台线程以及IO多线程。  不知道后续的Redis发展会是什么样,会不会真的在命令执行中引入多线程?如果引入的话,这对于Redis来说是一次大的改动。我认为很长一段时间不会这么做,这个我们拭目以待吧。  我们看Redis7里面,主要还是对数据结构、持久化、命令等方面做的优化和调整,并没有特别大的改动。自从2020年Redis之父离开了Redis项目后,我感觉Redis的发展路线会有所改变,就像红楼梦的后四十回是高鹗写的,你不能说不好,只能说可能和曹雪芹最开始的整体规划以及结局是不同的。

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

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

相关文章

python opencv比较图片相似度

目录 一:均值哈希算法 二:三直方图算法 三:单通道直方图 一:均值哈希算法 均值哈希算法是一种快速比较图像相似度的方法。它首先将图像转化为灰度图像,然后计算图像的均值,接着将每个像素的

探索AI视频模型的无限可能:OpenAI的Sora引领创新浪潮

文章目录 &#x1f4d1;前言一、技术解析二、应用场景三、未来展望四、伦理与创意五、用户体验与互动&#x1f324;️总结 &#x1f4d1;前言 随着人工智能技术的蓬勃发展&#xff0c;AI视频模型正逐渐成为科技领域的新宠。在这个变革的浪潮中&#xff0c;OpenAI推出的首个AI视…

算法沉淀——动态规划之回文串问题(上)(leetcode真题剖析)

算法沉淀——动态规划之回文串问题 01.回文子串02.最长回文子串03.分割回文串 IV04.分割回文串 II05.最长回文子序列06.让字符串成为回文串的最少插入次数 01.回文子串 题目链接&#xff1a;https://leetcode.cn/problems/palindromic-substrings/ 给你一个字符串 s &#xf…

雾锁王国服务器官方配置要求说明

雾锁王国/Enshrouded服务器CPU内存配置如何选择&#xff1f;阿里云服务器网aliyunfuwuqi.com建议选择8核32G配置&#xff0c;支持4人玩家畅玩&#xff0c;自带10M公网带宽&#xff0c;1个月90元&#xff0c;3个月271元&#xff0c;幻兽帕鲁服务器申请页面 https://t.aliyun.com…

C# TCP/IP协议

在C#中编写TCP/IP通信 //开启监听各个网络的连接是否成功 //开启服务端&#xff0c;监听&#xff0c;并且进入监听线程 try { IPAddress ip IPAddress.Parse(HostAddress); IPEndPoint ipe new IPEndPoint(ip, HostPort); m_services new Socket(Add…

深入JVM:解析OOM的三大场景,原因及实战解决方案

在Java应用程序开发中&#xff0c;OutOfMemoryError&#xff08;OOM&#xff09;是一个令人头痛的问题。当JVM中的内存无法满足应用程序的需求时&#xff0c;就会抛出这个错误。本文将深入探讨OOM的三大场景&#xff1a;堆内存溢出、方法区内存溢出和栈内存溢出&#xff0c;并分…

ChatGPT能替代什么人?

上一讲关于ChatGPT的热炒&#xff0c;其实对于我们来说算是敲了敲警钟。 其实在今天&#xff0c;关于ChatGPT&#xff0c;最多人关注的一个问题就是&#xff1a;ChatGPT能取代人吗&#xff0c;或者说能抢人的饭碗么吗&#xff1f; 有人说不能&#xff0c;也有人说能&#xff…

代码随想录算法训练营day30 || 332.重新安排行程(未完成),51. N皇后,37. 解数独(未完成)

这就是传说中的N皇后&#xff1f; 回溯算法安排&#xff01;| LeetCode&#xff1a;51.N皇后_哔哩哔哩_bilibili // 时间复杂度O(n^2) // 空间复杂度O(n^2)class Solution {List<List<String>> res new ArrayList<>();public List<List<String>>…

【机器人最短路径规划问题(栅格地图)】基于蚁群算法求解

基于蚁群算法求解机器人最短路径规划问题的仿真结果 仿真结果 收敛曲线变化趋势 蚁群算法求解最优解的机器人运动路径 各代蚂蚁求解机器人最短路径的运动轨迹

Java+SpringBoot+Vue:瑜伽馆管理的黄金组合

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

LeetCode 刷题 [C++] 第128题.最长连续序列

题目描述 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 题目分析 对于数组中存在的连续序列&#xff0c;为了统计每个连续序列的…

CSS复合选择器(二)

CSS复合选择器&#xff08;二&#xff09; 伪类选择器一、动态伪类&#xff1a;二、结构伪类三、否定伪类&#xff1a;四、UI伪类&#xff1a;五、目标伪类&#xff08;了解&#xff09;六、语言伪类&#xff08;了解&#xff09; 伪类选择器 作用&#xff1a;选中特殊状态的元…

java工程师面试突击中华石杉,开发人员必学

如何高效的学习MyBatis源码呢&#xff1f; 市面上真正适合学习的MyBatis资料太少&#xff0c;有的书或资料虽然讲得比较深入&#xff0c;但是语言晦涩难懂&#xff0c;大多数人看完这些书基本都是从入门到放弃。学透MyBatis源码难道就真的就没有一种适合大多数同学的方法吗&am…

Linux系统安装使用nginx

1.编译安装Nginx服务 (1)关闭防火墙&#xff0c;将安装nginx所需要软件包传到/opt目录下 systemctl stop firewalld systemctl disable firewalld setenforce 0 将压缩包传入到/opt目录下 cd /opt wget http://nginx.org/download/nginx-1.18.0.tar.gz (2). 安装依赖…

从新手到专家:AutoCAD 完全指南

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 引言 AutoCAD是一款广泛用于工程设计和绘图的…

Redisson 3.18.0版本解决failover相关问题

前言 Redisson 在历史多个版本都出现了failover期间报错的问题并且目前没有一个版本可以完全解决这个问题&#xff0c;所以在当前使用版本3.18.0基础上做了二次开发&#xff0c;达到降低业务由于redis遇到问题导致不可用。 背景 Redisson 作为业务线使用的Redis 客户端&…

vue-router4 (四) 历史记录处理方法

网页切换之后不想留下原页面历史记录处理方法&#xff08;比如登陆完之后不想留下登陆页面的历史记录&#xff09;&#xff1a; 1.使用router-link 标签导航&#xff0c;可在标签中添加replace 属性 <router-link replace to"/reg">Reg</router-link> …

vue 中 Vue.prototype 详解及使用

前言&#xff1a; 我们可能会在很多组件里用到数据/实用工具&#xff0c;但是不想污染全局作用域。这种情况下&#xff0c;可以通过在原型上定义它们使其在每个 Vue 的实例中可用。 1. 基本示例 在main.js中添加一个变量到 Vue.prototype Vue.prototype.$appName My App这样…

基于JAVA的就医保险管理系统 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 科室档案模块2.2 医生档案模块2.3 预约挂号模块2.4 我的挂号模块 三、系统展示四、核心代码4.1 用户查询全部医生4.2 新增医生4.3 查询科室4.4 新增号源4.5 预约号源 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVue…

大数据相关知识

Hadoop三大组件 HDFS -- Hadoop Distributed File System Hadoop的分布式文件系统 主要负责数据的存储和管理&#xff0c;可以将大数据集分成多个数据块&#xff0c;并将这些数据块分配到不同的计算节点上存储&#xff0c;提高数据的可靠性和处理效率 MapReduce 是Hadoop的…