Redis的高性能之道

news2024/11/19 1:21:03

        前言:做码农这么多年,我也读过很多开源软件或者框架的源码,在我看来,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();
        //第一个是主线程,创建完continue
        if (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. */
        //创建线程,并注册回调函数IOThreadMain
        if (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);
          //如果是写,执行writeToClient
            if (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.coloradmin.cn/o/1484955.html

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

相关文章

【双指针】删除有序数组中重复元素,双指针原地修改数组

删除有序数组中重复元素 链接 . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/remove-duplicates-from-sorted-array/…

Programming Abstractions in C阅读笔记:p308-p311

《Programming Abstractions in C》学习第76天&#xff0c;p308-p311总结&#xff0c;总计4页。 一、技术总结 1.快速排序伪代码 #include <stdbool.h>static int Partition(int array[], int n);/** Implementation notes: SortIntegerArray* ----------------------…

实践航拍小目标检测,基于YOLOv8全系列【n/s/m/l/x】参数模型开发构建无人机航拍场景下的小目标检测识别分析系统

关于无人机相关的场景在我们之前的博文也有一些比较早期的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《deepLabV3Plus实现无人机航拍目标分割识别系统》 《基于目标检测的无人机航拍场景下小目标检测实践》 《助力环保河道水质监测&#xff0c;基于yolov…

Django后端开发——中间件

文章目录 参考资料中间件注册中间件settings.pymiddleware/mymiddleware.pymysite3/views.pymysite3/urls.py 练习 参考资料 B站网课&#xff1a;点击蓝色字体跳转 或复制链接至浏览器&#xff1a;https://www.bilibili.com/video/BV1vK4y1o7jH?p39&vd_source597e21cf34f…

嵌入式Linux中GPIO设置的一些基本指令和步骤

一、GPIO的介绍 嵌入式Linux中的GPIO&#xff08;General Purpose Input/Output&#xff0c;通用输入/输出&#xff09;是一种常用的接口&#xff0c;允许开发者直接控制硬件设备的某些引脚&#xff0c;进行诸如LED控制、传感器读取、设备状态监测等任务。 二、设置步骤和示例…

08 yum和git

什么是软件包 安装软件&#xff0c;一个通常的办法就是下载程序的源代码进行编译。这种太麻烦&#xff0c;于是一些人把常用软件编译好&#xff0c;做成软件包放在服务器上&#xff0c;通过包管理器可以很方便的得到这个软件包安装&#xff0c;就好比手机上的应用商店 yum&am…

LeetCode 刷题 [C++] 第121题.买卖股票的最佳时机

题目描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的…

基于springboot+vue的中国陕西民俗网

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

ASUS华硕ROG冰刃6双屏GX650RX工厂模式原厂Windows11系统 带MyASUS WinRE恢复重置功能 恢复出厂开箱状态

华硕冰刃6笔记本电脑oem预装Win11系统&#xff0c;适用型号&#xff1a; NR2202RX_GX650RX、NR2202RW_GX650RW NR2202RM_GX650RM、NR2202RS_GX650RS 链接&#xff1a;https://pan.baidu.com/s/1nnGlmnqJ_YZIOX7RQXjhuQ?pwd79hm 提取码&#xff1a;79hm 原装出厂系统带有A…

大数据技术(一)

大数据技术概述 大数据技术层面及其功能 数据采集与预处理 利用ETL(extract-transform-load)工具将分布的、异构数据源中的数据&#xff0c;如关系数据、平面数据文件等&#xff0c;抽取到临时中间层后进行清洗、转换、集成&#xff0c;最后加载到数据仓库或数据集市中&…

【leetcode】用栈实现队列

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家刷题&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 点击查看题目 思路: 在做此题之前&#xff0c;我们先要实现栈&#xff0c;这在上个博客中已经写过&#xf…

Doris【数据模型】

一、数据模型简介 在 Doris 中&#xff0c;数据以表&#xff08;Table&#xff09;的形式进行逻辑上的描述。 一张表包括行&#xff08;Row&#xff09;和列&#xff08;Column&#xff09;。Row 即用户的一行数据。Column 用于描述一行数据中不同的字段。 Column 可以分为两…

“视频剪辑新风尚:批量操作,背景虚化,聚焦视频主题!“

在数字时代&#xff0c;视频成为了最直观、最生动的信息表达方式。但是&#xff0c;面对大量的视频素材&#xff0c;如何快速有效地进行剪辑&#xff0c;突出视频主题&#xff0c;成为了许多创作者和编辑者的难题。 首先&#xff0c;我们要打开【视频剪辑高手】进入主页面之后…

【研发日记】Matlab/Simulink技能解锁(四)——在Simulink Debugger窗口调试

前言 见《【研发日记】Matlab/Simulink技能解锁(一)——在Simulink编辑窗口Debug》 见《【研发日记】Matlab/Simulink技能解锁(二)——在Function编辑窗口Debug》 见《【研发日记】Matlab/Simulink技能解锁(三)——在Stateflow编辑窗口Debug》 Block断点 前文在Simulink编辑窗口…

【蓝桥杯】错误票据

今天是2024年3月1号&#xff0c;蓝桥杯比赛还有一个月的时间&#xff0c;虽说自己不指望拿奖吧&#xff0c;但是还是有些莫i名的焦虑&#xff0c;这道题目都做不出来&#xff0c;感觉自己真的有点菜啊&#xff01;但是还好啦&#xff0c;我觉得是因为我没有题感&#xff0c;慢慢…

(六)Dropout抑制过拟合与超参数的选择--九五小庞

过拟合 即模型在训练集上表现的很好&#xff0c;但是在测试集上效果却很差。也就是说&#xff0c;在已知的数据集合中非常好&#xff0c;再添加一些新数据进来效果就会差很多 欠拟合 即模型在训练集上表现的效果差&#xff0c;没有充分利用数据&#xff0c;预测准确率很低&a…

对缓冲区的初步认识——制作进度条小程序

对缓冲区的初步认识--进度条小程序 前言预备知识回车和换行的区别输出缓冲区/n 有清空输出缓冲区的作用stdout是什么&#xff1f;验证一切皆文件为什么是\n行刷新&#xff1f; 倒计时程序原理 代码实现为什么这里要强制刷新&#xff1f;没有会怎样&#xff1f;为什么是输出的是…

ABAP - SALV教程07 斑马纹显示和SALV标题

SALV设置斑马纹和标题 METHOD set_layout.DATA: lo_display TYPE REF TO cl_salv_display_settings. * 取得显示对象lo_display co_alv->get_display_settings( ).* 设置ZEBRA显示lo_display->set_striped_pattern( X ). * 设置Titlelo_display->set_list_he…

内存空间担保机制

什么是内存空间担保机制&#xff1f; 内存空间担保机制&#xff08;Memory Space Guarantee&#xff09;是垃圾回收&#xff08;Garbage Collection&#xff09;算法中的一种策略。它用于在进行垃圾回收过程&#xff08;如Minor GC或Full GC&#xff09;时&#xff0c;确保老年…

Linux创建,删除用户

Linux创建&#xff0c;删除用户 怎么登陆xshell创建用户设置密码 删除用户 我找了好久市面上的资料&#xff0c;我觉得写得不好&#xff0c;所以自己写一个 怎么登陆xshell ssh用户名ip地址 创建用户 adduser 用户名 设置密码 passwd 用户名 删除用户 userdel -r 用户…