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,一经查实,立即删除!

相关文章

探索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…

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

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

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

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

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 客户端&…

基于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…

在独立Unity工程中集成Vortex Studio

本文首发于&#xff1a;Unity3D入门教程09.01&#xff1a;在独立Unity工程中集成Vortex Studio 目的 在Unity中使用Vortex Studio引擎模拟Unity场景中的任何资源。 工程 打开桌面Unity Hub快捷方式 点击Open选择需要打开的工程&#xff0c;这里选择官方提供的默认工程C:\CM…

如何实现双向循环链表

博主主页&#xff1a;17_Kevin-CSDN博客 收录专栏&#xff1a;《数据结构》 引言 双向带头循环链表是一种常见的数据结构&#xff0c;它具有双向遍历的特性&#xff0c;并且在表头和表尾之间形成一个循环。本文将深入探讨双向带头循环链表的结构、操作和应用场景&#xff0c;帮…

【医学影像】LIDC-IDRI数据集的无痛制作

LIDC-IDRI数据集制作 0.下载0.0 链接汇总0.1 步骤 1.合成CT图reference 0.下载 0.0 链接汇总 LIDC-IDRI官方网址&#xff1a;https://www.cancerimagingarchive.net/nbia-search/?CollectionCriteriaLIDC-IDRINBIA Data Retriever 下载链接&#xff1a;https://wiki.canceri…

Golang使用Swag搭建api文档

1. 简介 Gin是Golang目前最为常用的Web框架之一。 公司项目验收需要API接口设计说明书&#xff08;Golang后端服务基于Gin框架编写&#xff09;&#xff0c;编写任务自然就落到了我们研发人员身上。 项目经理提供了文档模板&#xff0c;让我们参考模板来手动编写&#xff0c;要…

5.WEB渗透测试-前置基础知识-常用的dos命令

内容参考于&#xff1a; 易锦网校会员专享课 上一篇内容&#xff1a;4.WEB渗透测试-前置基础知识-快速搭建渗透环境&#xff08;下&#xff09;-CSDN博客 常用的100个CMD指令 1.gpedit.msc—–组策略 2. sndrec32——-录音机 3. Nslookup——-IP地址侦测器 &#xff0c;是一个…

Unity中的UI系统之GUI

目录 概述工作原理和主要作用基础控件重要参数及文本和按钮多选框和单选框输入框和拖动条图片绘制和框 复合控件工具栏和选择网络滚动视图和分组窗口 自定义整体样式自定义皮肤样式 概述 什么是UI系统 UI是User Interface&#xff08;用户界面&#xff09;的简称&#xff0c;用…

全域增长方法论:帮助品牌实现科学经营,助力长效生意增长

前两年由于疫情反复、供给需求收缩等条件制约&#xff0c;品牌业务均受到不同程度的影响。以双十一和618电商大促为例&#xff0c;就相比往年颇显“惨淡”&#xff0c;大多品牌营销都无法达到理想预期。 随着市场环境不断开放&#xff0c;2023年营销行业开始从低迷期走上了高速…

SDWAN异地组网难在哪?怎么解决?

SD-WAN作为一种先进的网络技术&#xff0c;为企业提供了更加灵活和高效的网络连接方案。然而&#xff0c;在异地组网的过程中&#xff0c;SD-WAN也面临一些挑战。本文将探讨SD-WAN异地组网所面临的难题&#xff0c;并提供相应的解决方案。 挑战一&#xff1a;网络延迟和不稳定性…

Jupyter Notebook 下载+简单设置

这里写目录标题 1. Jupyter Notebook安装2.切换打开别的盘3. 创建代码文件4.为jupyter notebook添加目录 (Jupyter安装拓展nbextensions)step1&#xff1a;安装命令step2&#xff1a;用户配置step3&#xff1a;上述过程均完成后&#xff0c;打开jupyter notebook就会发现界面多…

常见的socket函数封装和多进程和多线程实现服务器并发

常见的socket函数封装和多进程和多线程实现服务器并发 1.常见的socket函数封装2.多进程和多线程实现服务器的并发2.1多进程服务器2.2多线程服务器2.3运行效果 1.常见的socket函数封装 accept函数或者read函数是阻塞函数&#xff0c;会被信号打断&#xff0c;我们不能让它停止&a…