Redis源码分析之网络模型

Redis网络模型

  • 阅读源码的初衷
  • Redis源码阅读

阅读源码的初衷

很多网上解释这个Redis为啥这么块?都会说Redis这么快的原因会有一个Redis才用了单线程&使用了多路io复用来检查io事件,单线程可以避免多线程对资源的竞争。如果我们使用了多线程那么就需要考虑加锁,如果要加锁那么这个锁的粒度如何考虑应该加什么样的锁?如果出现了死锁那么效率只会更低。但是各位老铁有没有相关单线程最怕的是什么?当然是最怕阻塞操作如果此时单线程阻塞了那么就无法处理其他客户端的连接而且当reator模型检测到有事情发送时,需要调用read这样的系统调用将客户端的数据读取上了,而read这个操作是阻塞的,将客户端的数据读取上来之后我们还需要对这个decode进行解码,如果客户端的数据很大那么无疑这两部是非常耗时的肯定会导致这个Redis的效率下降。带着这个因为博主开始查看Redis6.2的源代码。在这里提前告知一下博主阅读完Redis网络模型的感受只能说是牛逼

  • 对于多线程来说最怕资源的竞争所以Redis的工作线程才用了多线程而对于IO线程会有一个自己的专属队列这样就避免了资源的竞争
  • 对于单线程最怕的是阻塞,redis采用多线程来进行IO的读取但是这个命令的执行确实单线程的。

Redis源码阅读

首先在这里我先说一下博主阅读Redis源码的思路,首先博主是从这个main函数开始看的,这个main函数在networking.c这个文件当中。
在这里插入图片描述
我们看到这个initServer这个函数。下面我们看看这个函数具体干了些什么?main函数当中这个其他函数暂时不用管因为我们只是查看Redis的网络部门

void initServer(void) {
    createSharedObjects();//创建共享对象
    server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);//重要创建事件循环器
    //多路io复用器
    server.db = zmalloc(sizeof(redisDb) * server.dbnum);//创建Redis的默认的16个数据库
    if (server.port != 0 &&
        listenToPort(server.port, server.ipfd, &server.ipfd_count) == C_ERR)
        //listenToPort主要干了3件事 socket创建套接字,bind,listen
        exit(1);
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }
    if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
        moduleBlockedClientPipeReadable, NULL) == AE_ERR) {
        serverPanic(
            "Error registering the readable event for the module "
            "blocked clients subsystem.");
    }
    //开启事件循环需要先调用beforeSleep这个回调
    aeSetBeforeSleepProc(server.el, beforeSleep);
    aeSetAfterSleepProc(server.el, afterSleep);
    //当事件循环处理完毕时调用的回调
}

在这里说明一下几个重要的函数。

  • aeCreateEventLoop:创建事件循环器
  • listenToPort创建套接字,bind,listen.
  • aeCreateFileEvent:创建文件事件并将监听套接字注册到事情循环器上。

下面我们看看这两个函数里面主要干了些啥事情

   .........上面代码逻辑忽略
   s = socket(p->ai_family,p->ai_socktype,p->ai_protocol;
  if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) s = ANET_ERR;
  而anetListen主要干的是
  bind(s,sa,len);
  listen(s, backlog)

下面我们看看则个aeCreateFileEvent这个函数主要是干了些啥

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;
}

在这里主要是将监听套接字注册到事件循环器当中,并设置当连接到来时需要执行的回调,当事情到来时只需要调用对应的回调即可,而这个监听套接字对应的回调是这个acceptTcpHandler这个回调函数下面我们一起来看看这个函数干了些什么

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);//将客户端的连接通过accepter获取上来
        .........
        acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);//给这个客户端的连接绑定对应的回调
    }
}

注意我们需要重点看一下这个connCreateAcceptedSocket这个函数他干了什么非常的重要我们点进去进行查看。

connection *connCreateSocket() {
    connection *conn = zcalloc(sizeof(connection));
    conn->type = &CT_Socket;//尤其是这一步非常的主要负责后面你可能难懂获取到的连接时怎么注册的事情循环器当中的
    conn->fd = -1;
    return conn;
}

下面我们在看看这个CT_Socket这货是什么东西

ConnectionType CT_Socket = {
    .ae_handler = connSocketEventHandler,
    .close = connSocketClose,
    .write = connSocketWrite,
    .read = connSocketRead,
    .accept = connSocketAccept,
    .connect = connSocketConnect,
    .set_write_handler = connSocketSetWriteHandler,//用来设置写回调
    .set_read_handler = connSocketSetReadHandler,//用来设置读回调
    .get_last_error = connSocketGetLastError,
    .blocking_connect = connSocketBlockingConnect,
    .sync_write = connSocketSyncWrite,
    .sync_read = connSocketSyncRead,
    .sync_readline = connSocketSyncReadLine,
    .get_type = connSocketGetType
};

我们点进去一看哦原来是一堆的回调函数,后面我们会见到这一堆回调函数的使用的请各位老铁重点关注一下。
下面我们继续往下面看

#define MAX_ACCEPTS_PER_CALL 1000
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
    client *c;
    ....................
    /* Create connection and client */
    if ((c = createClient(conn)) == NULL) {
    //客户端的连接打包成为一个客户端
    }
    c->flags |= flags;//设置对应的标志位
}

下面我们重点看一下这个createClient这个函数,我们点进去进行查看。


client *createClient(connection *conn) {
    client *c = zmalloc(sizeof(client));
    if (conn) {
        connNonBlock(conn);//设置非阻塞
        connEnableTcpNoDelay(conn);
        if (server.tcpkeepalive)//长连接
            connKeepAlive(conn,server.tcpkeepalive);
        connSetReadHandler(conn, readQueryFromClient);
        //当客户端有事件到来时会调用readQueryFromClient这个函数
    }
 }

这个readQueryFromClient函数非常的有意思,后面我们重点来解释这个函数。下面我们来看看这个事情循环的过程在networking.c的最后部门 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事情循环器。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
        int processed = 0, numevents;
        if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
            eventLoop->beforesleep(eventLoop);
            //在进行epoll_wait之前需要处理一些事情很重要
        numevents = aeApiPoll(eventLoop, tvp);
        //进行epoll_wait等待客户端的事件到来
        /* After sleep callback. */
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);//醒来之后需要处理的一些回调

        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++;
                fe = &eventLoop->events[fd]; 
            }

            /* Fire the writable event. */
            if (fe->mask & mask & AE_WRITABLE) {//写事情到来
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    //调用对应的写回调
                    fired++;
                }
            }

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

而这个读回调就是这个readQueryFromClient这个函数,这个函数非常的优秀一共会进行三次。
在这里插入图片描述
其主要功能主要是图上的这三个功能。可能有老铁一直关系的多线程在哪里了,其实还是在networking.c里面博主给忘记了,下面我们可以看看他里面的initLastServer.
在这里插入图片描述
下面我们点进去看看

void InitServerLast() {
    bioInit();
    initThreadedIO();//开始创建线程了
    set_jemalloc_bg_thread(server.jemalloc_bg_thread);
    server.initial_memory_usage = zmalloc_used_memory();
}

然后就是这个initThreadedIO

void initThreadedIO(void) {
    server.io_threads_active = 0; /* We start with threads not active. */
    if (server.io_threads_num == 1) return;//如果只有一个那么就是主线程

    if (server.io_threads_num > IO_THREADS_MAX_NUM) {
        serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
                             "The maximum number is %d.", IO_THREADS_MAX_NUM);
        exit(1);
    }

    /* 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();//创建每个线程的专属队列
        if (i == 0) continue; /* Thread 0 is the main thread. */
        pthread_t tid;
        pthread_mutex_init(&io_threads_mutex[i],NULL);
        setIOPendingCount(i, 0);//设置每个线程的队列长度为0,线程启动后会判断队列的长度是否大于0
        pthread_mutex_lock(&io_threads_mutex[i]);
        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;
    }
}

而这个线程的入口函数是这个IOThreadMain.然后我们在回到那个读连接到来时我们需要调用的回调函数readQueryFromClient

void readQueryFromClient(connection *conn) {
 
     ...............
    /* 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;
    //将客户端的连接分发到这个全局队列当中clients_pending_read这个全局队列当中
    ............
    nread = connRead(c->conn, c->querybuf+qblen, readlen);
    //读取客户端的连接
    ............

     processInputBuffer(c);//开始去执行命令
}

下面我们一起看看这个postponeClientRead这个函数

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->flag是这个CONN_STATE_ACCEPTING所有会进来
    {
        c->flags |= CLIENT_PENDING_READ;
        listAddNodeHead(server.clients_pending_read,c);//加入到全局队列当中
        return 1;
    } else {
        return 0;
    }
}

而一开始客户端的这个状态是这个
在这里插入图片描述
当加入到全局队列当中之后main线程就会通过取轮询的方式将全局队列当中的客户端交给每一个线程去处理。那么在哪里开始进行分配的了?在这里博主就直接说了,在beforeSleep这里会调用分发的函数

void beforeSleep(struct aeEventLoop *eventLoop) {
    UNUSED(eventLoop);

    ................
    handleClientsWithPendingReadsUsingThreads();
    //将客户端的分发到每个线程的队列当中只不过是读事情
    .......................
    handleClientsWithPendingWritesUsingThreads();
    将客户端的分发到每个线程的队列当中只不过是写事情
    ......................
}

也就是在进行epoll_wait之前会将client通过轮询的方式分发到每个线程的队列当中去。并更新每个线程的队列长度。那么每个线程在主线程没有分发给他们任务时他们在干什么了?

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) {
        //死循环的空转
        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);

        //如果走到了这里说明队列当中有东西了,开始从自己的队列当中处理请求
        listIter li;
        listNode *ln;
        listRewind(io_threads_list[id],&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            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);
    }
}

当然我们可以在redis.conf里面进行配置这个线程的数量
在这里插入图片描述

在main线程没有给每个线程的队列当中分配client时,其他线程一直在死循环的空转,并且不会让出CPU,直到main线程给他分配了client之后开始根据时读事情还是写事件执行对应的回调。我们惊奇的发现IO线程当中也再次调用了这个readQueryFromClient但是这次进入到这个函数当中在执行postponeClient的时候不会直接return 因为在第一次放入到全局队列当中c的flag已经被置为这个client_pending_read,就会继续执行下面的代码。而下面的代码时这个读取客户端的数据上面,并进行解码。但是不会执行这个命令
在这里插入图片描述
以为此时客户端的状态是这个CLIENT_PENDID_READ.直接break了并不会执行这下面的状态而当每个线程将客户端处理完成之后main线程会同一进行处理。
在这里插入图片描述
当命令执行完成之后,又会将结果放入到全局队列当中
在这里插入图片描述
函数:

void clientInstallWriteHandler(client *c) {
  
    if (!(c->flags & CLIENT_PENDING_WRITE) &&
        (c->replstate == REPL_STATE_NONE ||
         (c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
    {
     
        c->flags |= CLIENT_PENDING_WRITE;
        listAddNodeHead(server.clients_pending_write,c);//放入全局队列当中
    }
}

而在这个函数当中进行分发 handleClientsWithPendingWritesUsingThreads()

int handleClientsWithPendingWritesUsingThreads(void) {
    int processed = listLength(server.clients_pending_write);
    if (processed == 0) return 0; /* Return ASAP if there are no clients. */

    /* If I/O threads are disabled or we have few clients to serve, don't
     * use I/O threads, but the boring synchronous code. */
    if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
        return handleClientsWithPendingWrites();
    }

    /* Start threads if needed. */
    if (!server.io_threads_active) startThreadedIO();

    /* Distribute the clients across N different lists. */
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_write,&li);
    int item_id = 0;
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags &= ~CLIENT_PENDING_WRITE;

        /* Remove clients from the list of pending writes since
         * they are going to be closed ASAP. */
        if (c->flags & CLIENT_CLOSE_ASAP) {
            listDelNode(server.clients_pending_write, ln);
            continue;
        }

        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_WRITE;
    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);
        writeToClient(c,0);
    }
    listEmpty(io_threads_list[0]);

    /* Wait for all the other threads to end their work. */
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += getIOPendingCount(j);
        if (pending == 0) break;
    }

    /* Run the list of clients again to install the write handler where
     * needed. */
    listRewind(server.clients_pending_write,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);

        /* Install the write handler if there are pending writes in some
         * of the clients. */
        if (clientHasPendingReplies(c) &&
                connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
        {
            freeClientAsync(c);
        }
    }
    listEmpty(server.clients_pending_write);

    /* Update processed count on server */
    server.stat_io_writes_processed += processed;

    return processed;
}

和读取的过程查不多。

总结:

  • Redis通过io多路复用让一个线程能够高效的处理多个客户端的请求减少了网络IO的开销
  • 工作线程采用单线程,单线程避免了多线程对资源的竞争,因为多线程我们不得不面对的问题就是资源竞争,资源竞争就意味着需要加锁那么加什么样的锁,锁的粒度如何把握,出现了死锁这样返回会降低Redis的效率
  • 但是当客户端的请求数据很大时,通过read将客户端的数据读取上了和decode操作会很耗时,因此redis引入了这个多线程,但是多线程我们说过最怕的就是这个资源的竞争,因此redis为每一个线程分配了一个自己的本地队列,这样就不存在资源竞争问题
  • io线程只负责将读取客户端的发送过来的数据读取上了并进行decode和将结果发送给客户端,并不会参与命令的执行。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT其实并不想让开发人员做这5件事情

前言 ChatGPT已经火爆了快半年了吧&#xff0c;紧接着国内也开始推出了各种仿制品&#xff0c;我甚至一度怀疑&#xff0c;如果人家没有推出ChatGPT&#xff0c;这些仿制品会不会出现。而很多人也嗨皮得不行&#xff0c;利用各种方法开始科学上网&#xff0c;用ChatGPT做各种觉…

[读书笔记] Variational AutoEncoders

小全读书笔记 《Variational AutoEncoders》 1. Generative Model &#xff08;生成式模型&#xff09;简述2. 简单生成模型 AutoEncoders2.1 结构2.2 不足 3. Variational AutoEncoders4. Variational AutoEncoders的数学支持 此读书笔记来自于Joseph Rocca的Understanding Va…

APP 兼容性测试是什么?8年测试老鸟告诉你

1、APP 兼容性测试认识 随着 APP 应用范围越来越广&#xff0c;用户群体越来越大&#xff0c;终端设备的型号也越来越多&#xff0c;移动终端碎片化加剧&#xff0c;使得 APP 兼容性测试成为测试质量保障必须要考虑的环节。 APP 兼容性测试通常会考虑&#xff1a;操作系统、厂…

js执行思维导图

备注&#xff1a; js执行&#xff1a; 执行分为两部分&#xff1a;预执行和执行 预执行&#xff1a;创建好执行上下文 执行&#xff1a;执行栈中执行 js引擎&#xff1a; 读取并执行js 各个浏览器的引擎如下 …

linux学习[10]磁盘与文件系统(1):查看磁盘容量指令df 评估文件系统的磁盘使用量指令 du

文章目录 前言1. df指令2. du指令 前言 TF卡制作的过程中涉及到了磁盘分区格式化等问题&#xff0c;当时对具体的指令理解不是特别深刻&#xff1b;由此引申到我对linux中的整个磁盘与文件系统没有一个全面的认识&#xff0c;这个磁盘与文件系统的系列博客章节就对这些进行记录…

MySQL的主从复制与读写分离

一、MySQL主从复制 1、mysql支持的复制类型 STATEMENT∶基于语句的复制。在服务器上执行sql语句&#xff0c;在从服务器上执行同样的语句&#xff0c;mysql默认采用基于语句的复制&#xff0c;执行效率高。 ROW∶ 基于行的复制。把改变的内容复制过去&#xff0c; 而不是把命…

下载和安装appuploader

转载&#xff1a;下载和安装appuploader IOS开发工具官网地址 Appuploader home -- A tool improve ios develop efficiency such as submit ipa to appstore and manage ios certificate 最新版本已经优化了没支付688给apple的账号登录流程&#xff0c;无需再安装其他软件。…

微服务保护(线程隔离、降级、熔断)

线程隔离 线程隔离有两种方式实现: 线程池隔离信号量隔离 线程池隔离 假设服务A依赖于服务B和服务C&#xff0c;那么服务A就会分别对服务B和服务C创建线程池&#xff0c;当有请求进来时不会使用服务A本身的线程&#xff0c;而是到对应的线程池中取一个线程来调用feign的客户…

AMBER分子动力学模拟之TOP准备-- HIV蛋白酶-抑制剂复合物(1)

AMBER分子动力学模拟之TOP准备-- HIV蛋白酶-抑制剂复合物(1) 我们以HIV蛋白酶-抑制剂复合物为例子&#xff0c;跑Amber动力学模拟 下载1phv 从PBD下载文件&#xff1a;https://www.rcsb.org/ PDB文件预处理 我们以 “protein(water) ligandcomplex” 为例来说一下如何处…

力扣82删除排序链表中的重复元素 II:思路分析+代码实现+方法总结(三指针法快慢指针法【双指针】递归法)

文章目录 第一部分&#xff1a;题目描述第二部分&#xff1a;代码实现2.1 三指针法2.2 快慢指针法&#xff08;双指针&#xff09;2.3 递归 第一部分&#xff1a;题目描述 &#x1f3e0; 链接&#xff1a;82. 删除排序链表中的重复元素 II - 力扣&#xff08;LeetCode&#xf…

ChatGPT热门资料汇总,绝对不割韭菜

前言 ChatGPT 的出现&#xff0c;AI圈子一下就热闹起来了&#xff0c;各个公司争先恐后地出自己的产品&#xff0c;百度的文心一言、谷歌的Bard、阿里的通义千问等等&#xff0c;有很多人借此机会已经赚到百万&#xff0c;很多卖课搞培训的都是互为合伙人&#xff0c;大家都懂…

如何注册appuploader账号​

如何注册appuploader账号​ 我们上一篇讲到appuploader的下载安装&#xff0c;要想使用此软件呢&#xff0c;需要注册账号才能使用&#xff0c;今​ 天我们来讲下如何注册appuploader账号来使用软件。​ 1.Apple官网注册Apple ID​ 首先我们点击首页左侧菜单栏中的“常见网…

【更新日志】填鸭表单TduckPro v5.1 更新

hi&#xff0c;各位Tducker小伙伴。 填鸭表单pro迎来了v5.1版本&#xff1b;本次我们进行了许多的功能新增和优化&#xff0c;能够让我们在日常使用中获得更好的体验。 让我们一起来康康新功能吧。 01 新增Pro功能 新增登录后才能填写表单。 新增表单卡片一键发布。 新增矩…

【STM32CubeMX】F103窗口看门狗

前言 本文记录了我学习STM32CubeMX的过程&#xff0c;方便以后回忆。我们使用的开发板是基于STM32F103C6T6的。本章记录了窗口看门狗的使用配置。要学习的话&#xff0c;注意流程一说&#xff0c;省略的内容。 基础 窗口看门狗(WWDG)属于APB1上外设。窗口看门狗(WWDG)的时钟源…

JUC并发编程16 | CAS自旋锁

CAS自旋锁 是什么&#xff0c;干什么&#xff0c;解决了什么痛点&#xff1f;如何解决&#xff0c;如何使用。 原子类&#xff1a;java.util.concurrent.atomic 在没有CAS之前&#xff0c;多线程环境不使用原子类保证线程安全i等操作&#xff0c;会出现数据问题&#xff0c;…

Day968.如何开启一个遗留系统现代化项目? -遗留系统现代化实战

如何开启一个遗留系统现代化项目&#xff1f; Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于如何开启一个遗留系统现代化项目&#xff1f;的内容。那如何启动一个遗留系统现代化项目。 一、项目背景 说来有点唏嘘&#xff0c;国内遗留系统的重灾区&#xff0c;恰恰…

WiFi(Wireless Fidelity)基础(八)

目录 一、基本介绍&#xff08;Introduction&#xff09; 二、进化发展&#xff08;Evolution&#xff09; 三、PHY帧&#xff08;&#xff08;PHY Frame &#xff09; 四、MAC帧&#xff08;MAC Frame &#xff09; 五、协议&#xff08;Protocol&#xff09; 六、安全&#x…

迪赛智慧数——柱状图(象形标识图):在选择另一半时,你更看重的是?

效果图 好看只排第六&#xff0c;第一确实众望所归&#xff01;当代男女择偶标准出炉&#xff0c;一张图带你看清。 女性挑选另一半时&#xff0c;她们更看重伴侣收入高、职业体面、工作能力强、受教育程度高&#xff0c;还得和自己有共同话题。 男性择偶观和女性恰恰相反&am…

ctfshow周末大挑战2023/5/12

本周周末大挑战用到的函数讲解 parse_url() 作用&#xff1a;解析URL&#xff0c;返回其组成部分 语法&#xff1a; parse_url ( string $url [, int $component -1 ] ) 参数&#xff1a; url&#xff1a;要解析的 URL。无效字符将使用 _ 来替换。 component&#xff1a; …

软件测试月薪2万,需要技术达到什么水平?

最近跟朋友在一起聚会的时候&#xff0c;提了一个问题&#xff0c;说一个软件测试工程师如何能月薪达到二万&#xff0c;技术水平需要达到什么程度&#xff1f;人回答说这只能是大企业或者互联网企业工程师才能拿到。也许是的&#xff0c;小公司或者非互联网企业拿二万的不太可…