Redis源码分析之网络模型

news2024/9/20 10:48:05

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和将结果发送给客户端,并不会参与命令的执行。

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

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

相关文章

Revit中栏杆扶手、坡道的绘制及插件太多问题

一、在Revit中栏杆与扶手的绘制方法有两种&#xff1a; ①绘制路径 ②放置在主体上 二、执行方式 功能区&#xff1a;“建筑”选项卡“楼梯坡道”面板“栏杆扶手”下拉菜单“绘制路径”。 三、绘制技巧 首先我们一起来看看如何设置栏杆扶手属性。 第一步&#xff1a;设置属…

【vue3学习系列】对比vue2生命周期做了哪些改变,vue3初学者快来看看

文章目录 前言官方生命周期图分析去除beforeCreate与createdsetup代替created其他钩子只是改了名称 剔除vue2后的生命周期图其他钩子函数keepalive错误捕获其他的一些钩子去官方文档看看即可 前言 看了下官方的生命周期的说明&#xff0c;感觉讲的不算太清晰&#xff0c;所以个…

C++ 内联函数(inline)

内联函数&#xff1a;就是在函数前加inline 让函数在调用的地方直接展开 可是内联函数有什么作用呢&#xff1f;&#xff1f; 我们都知道&#xff0c;如果调用一个函数的话&#xff0c;会建立栈帧&#xff0c;在建立栈帧的时候会进行压栈等一系列操作。 而内联函数会在调用的…

Nginx和tomcat反向代理

七层反向代理 实验准备&#xff1a;准备三台虚拟机 192.168.146.20 tomcat&#xff08;两个&#xff09; 192.168.146.30 tomcat 192.168.146.50 七层反向代理&#xff08;nginx&#xff09; 部署虚拟机192.168.146.20&#xff08;两个tomcat已部署完毕&#xff09; …

SwiftUI 4.0(iOS 16)极简实现一个美美哒的多选 Toggle 按钮组

概览 在 SwiftUI 4.0 之前&#xff0c;想要实现如下效果的多选/全选 Toggle 按钮组是要写不少行代码滴&#xff1a; 不过&#xff0c;在 iOS 16 之后我们仅用1行代码即可搞定以上所有&#xff01;在某些场合下这非常有用哦。 在本篇博文中&#xff0c;我们就来看看如何实现它…

2023年的深度学习入门指南(6) - 剪枝和量化

2023年的深度学习入门指南(6) - 剪枝和量化 从这一节开始&#xff0c;我们要准备一些技术专项了。因为目前大模型技术还在快速更新迭代中&#xff0c;各种库和实现每天都在不停出现。因为变化快&#xff0c;所以难免会遇到一些问题。对于细节有一定的把握能力起码可以做到出问…

5.1 数值微分

学习目标&#xff1a; 作为数值分析的基础内容&#xff0c;我建议你可以采取以下步骤来学习数值微分&#xff1a; 掌握微积分基础&#xff1a;数值微分是微积分中的一个分支&#xff0c;需要先掌握微积分基础知识&#xff0c;包括导数、极限、微分等。 学习数值微分的概念和方…

Scillus | 来吧!它可以大大简化你的Seurat分析流程哦!~(二)(高级可视化)

1写在前面 不知道大家那里天气热了没有&#xff0c;苦逼的我虽然“享受”着医院的恒温&#xff0c;但也并没有什么卵用&#xff0c;毕竟我只是个不可以生锈的“小螺丝”。&#x1f972; 上期介绍了Scillus包的基本功能&#xff0c;如何进行数据的预处理及其可视化。&#x1f92…

分享github上比较热门的ChatGPT项目,值得收藏

&#x1f517; 运行环境&#xff1a;chatGPT &#x1f6a9; 撰写作者&#xff1a;左手の明天 &#x1f947; 精选专栏&#xff1a;《python》 &#x1f525; 推荐专栏&#xff1a;《算法研究》 #### 防伪水印——左手の明天 #### &#x1f497; 大家好&#x1f917;&#x1f9…

Elasticsearch painless脚本教程(包含Java API和SpringDataElasticsearch调用脚本)

Elasticsearch painless脚本 1.什么是painless2.painless的特性3.使用kibana进行准备操作3.1 使用kibana创建索引和映射3.2 使用kibana添加测试数据 4.使用painless执行查询操作关于脚本查询须知4.1 字段查询脚本4.1 排序查询脚本 5.如何使用painless执行更新操作关于脚本查询须…

Spring Security 04 自定义认证

登录⽤户数据获取 SecurityContextHolder Spring Security 会将登录⽤户数据保存在 Session 中。但是&#xff0c;为了使⽤⽅便, Spring Security 在此基础上还做了⼀些改进&#xff0c;其中最主要的⼀个变化就是线程绑定。当⽤户登录成功后,Spring Security 会将登录成功的⽤户…

Node内置模块 【crypto加密模块】

文章目录 &#x1f31f;前言&#x1f31f;crypto加密模块&#x1f31f;Crypto模块介绍&#x1f31f;Hash算法&#x1f31f;Hash算法介绍&#x1f31f;Hash算法之MD5&#x1f31f;算法简介&#x1f31f;MD5加密使用 &#x1f31f;Hash算法之SHA1&#x1f31f;算法简介&#x1f3…

二叉树经典题题解

目录 &#x1f345;1.单值二叉树&#x1f345; &#x1f349; 2.相同的树&#x1f349; &#x1f34a;3.对称二叉树&#x1f34a; &#x1f34e;4.另一颗树的子树&#x1f34e; &#x1f34f;5.翻转二叉树&#x1f34f; &#x1f351;6.平衡二叉树&#x1f351; &#x1f3…

【LeetCode: 1027. 最长等差数列 | 暴力递归=>记忆化搜索=>动态规划】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

SHELL的脚本编辑与运行

目录 1.编写脚本for1.sh,使用for循环创建20账户&#xff0c;账户名前缀由用户从键盘输入&#xff0c;账户初始密码由用户输入&#xff0c;例如&#xff1a;test1、test2、test3、、test10 a.编辑脚本 b.运行脚本 c.进行检验 2.编写脚本for2.sh,使用for循环&#xff0c;通过…

如何把aac转化为mp3,4个处理方法教给你

一般情况下&#xff0c;将 AAC 文件转换为 MP3 文件有以下几种情况&#xff1a;设备不兼容&#xff1a;AAC 格式通常用于苹果设备上。如果您想在其他设备上播放音频文件&#xff0c;如 Android 手机、MP3 播放器等&#xff0c;就需要将其转换为 MP3 格式。需要更好的兼容性&…

机器学习(七):基于多项式贝叶斯对蘑菇毒性分类预测分析

系列文章目录 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1;评论&#x1f4d2;关注…

第六章 原型模式

文章目录 前言一、克隆羊问题sheep类clint 调用方 二、引入原型模式动态克隆对象sheep类clint 类 三、原型模式在Spring框架中源码分析四、深拷贝与浅拷贝完整代码DeepCloneableTargetDeepProtoTypeClient2 五、原型模式的注意事项和细节 前言 一、克隆羊问题 sheep类 package…

mysql查询字段未加引号问题及隐式转换

1. 问题重现 最近线上出了个问题&#xff0c;用户明明没有投票&#xff0c;却提示已投票&#xff0c;我查询数据&#xff0c;刚开始没有查出数据&#xff0c;后来却查出数据了&#xff0c;以为没有问题&#xff0c;后来以为是插入的时候通过int类型插入&#xff0c;导致varcha…

美国最大公共养老基金之一Strs Ohio不断增持IonQ股票

​ &#xff08;图片来源&#xff1a;网络&#xff09; 截至2022年12月31日第四季度末&#xff0c;美国最大的公共养老基金之一Strs Ohio发布报告称&#xff0c;其一直在增持IonQ&#xff0c;Inc.&#xff08;纽约证券交易所代码&#xff1a;IONQ&#xff09;的股份。资产管理公…