来聊聊Redis持久化AOF管道通信的设计

写在文章开头

最近遇到很多烦心事,希望通过技术来得以放松,今天这篇文章笔者希望会通过源码的方式分析一下AOF如何通过Linux父子进程管道通信的方式保证进行AOF异步重写时还能实时接收用户处理的指令生成的AOF字符串,从而保证尽可能的可靠性。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解AOF管道通信的设计

Linux管道通信进程

在进程AOF重写时,redisfork出一个子进程,让子进程进行异步重写机制,避免AOF文件重写的耗时导致redis执行性能下降。由此也诞生了另外一个问题,AOF子进程异步重写期间,用户最新发送的指令能否被AOF子进程接收并持久化到文件中。
对此redis借助Linux管道通信的方式实现,通过管道通信的方式实现实时数据发送,对应子进程收到这些指令对应的字符串之后,就会将其写入AOF重写文件。

在这里插入图片描述

需要注意的是Linux管道通信通常都是单向的,即收发通道需要交由两个数组空间才能实现,例如父进程写入客户端实时指令到通道只能通过数组0空间完成发送,而客户端也只能通过数组1空间完成数组接收。同理要实现通道上客户端向服务端写数据和服务端读取数据就需要在新建相同的2长度的数组了。

在这里插入图片描述

我们给出创建AOF子进程的核心代码,即位于aof.crewriteAppendOnlyFileBackground,可以看到在创建子进程之前,redis会通过aofCreatePipes函数创建管道为后续的重写子进程以及父进程提供条件:

int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1) return REDIS_ERR;
    if (aofCreatePipes() != REDIS_OK) return REDIS_ERR;//创建管道
    start = ustime();
    if ((childpid = fork()) == 0) {//fork子进程进行aof重写
        char tmpfile[256];

        //......
        //生成一个tmp文件
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {//重写aof
            size_t private_dirty = zmalloc_get_private_dirty();

             //......
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
       //......
    }
    return REDIS_OK; 
}

步入aofCreatePipes我们就可以看到笔者上文所介绍的管道pipes的创建逻辑,可以看到其内部初始化一个长度为6的数组空间,两两构成一个逻辑上的通道,按序通道依次是:

  1. 父进程写数据到子进程的收发通道。
  2. 子进程向父进程发送确保ACK信号的通道。
  3. 父进程向子进程发送ACK确认信号的通道。

在这里插入图片描述

对应的我们给出创建管道的核心代码即位于aof.caofCreatePipes,可以看到其通道本质就是通过创建一个长度为6的数组fds,按照笔者上文所说构成父进程发、子进程确认、父进程确认的通道,这其中父进程会调用anetNonBlock方法将该通道设置为写入时非阻塞以保证主进程写入最新数据时不会阻塞整个流程:

int aofCreatePipes(void) {
    //创建3个管道
    int fds[6] = {-1, -1, -1, -1, -1, -1};
    int j;
    
    if (pipe(fds) == -1) goto error; /* parent -> children data. */
    if (pipe(fds+2) == -1) goto error; /* children -> parent ack. */
    if (pipe(fds+4) == -1) goto error; /* children -> parent ack. */
    /* Parent -> children data is non blocking. */
    //父进程写到子进程的管道设置为非阻塞
    if (anetNonBlock(NULL,fds[0]) != ANET_OK) goto error;
    if (anetNonBlock(NULL,fds[1]) != ANET_OK) goto error;
    //设置读事件监听
    if (aeCreateFileEvent(server.el, fds[2], AE_READABLE, aofChildPipeReadable, NULL) == AE_ERR) goto error;
    //将管道复制给各个成员遍历
    //主进程向子进程读写数据的通道
    server.aof_pipe_write_data_to_child = fds[1];
    server.aof_pipe_read_data_from_parent = fds[0];
    //子进程向父进程发送ack的通道
    server.aof_pipe_write_ack_to_parent = fds[3];
    server.aof_pipe_read_ack_from_child = fds[2];
    //父进程向子进程发送ack通道的
    server.aof_pipe_write_ack_to_child = fds[5];
    server.aof_pipe_read_ack_from_parent = fds[4];
    server.aof_stop_sending_diff = 0;
    return REDIS_OK;

error:
    redisLog(REDIS_WARNING,"Error opening /setting AOF rewrite IPC pipes: %s",
        strerror(errno));
    for (j = 0; j < 6; j++) if(fds[j] != -1) close(fds[j]);
    return REDIS_ERR;
}

AOF重写如何接收父进程数据

后续的父进程一旦收到客户端实时传入的指令例如set k v之后,其核心流程就会传播该事件到AOF链路上,将用户指令的字符串转为RESP格式(redis协议要求的格式)写入到父进程发送数据到子进程即第一个通道上,后续的子进程就会通过该通道的索引1数组获取这个最新的数据:

在这里插入图片描述

当服务端接收到客户端指令后就会执行call方法执行解析并执行客户端指令,然后通过propagate方法将客户端指令传播到AOF函数上并写入到通道中:

void call(redisClient *c, int flags) {
	//......
  	//基于命令者模式执行客户端传入的指令
    c->cmd->proc(c);
   //......
    //将指令传播到aof链路
    if (flags & REDIS_CALL_PROPAGATE) {
        int flags = REDIS_PROPAGATE_NONE;

     	  //......
		
        if (flags != REDIS_PROPAGATE_NONE)
        	//将指令cmd和键值对argv传入交由aof事件执行
            propagate(c->cmd,c->db->id,c->argv,c->argc,flags);
    }

    //......
}

我们步入propagate即可看到其内部发现如果AOF非关闭状态且允许传播事件,则调用feedAppendOnlyFile追加客户端指令和键值对到通道中:

void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)
{
	//如果aof非关闭且允许传播aof事件则调用feedAppendOnlyFile
    if (server.aof_state != REDIS_AOF_OFF && flags & REDIS_PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc);
	 //......
}

再次步入feedAppendOnlyFile就可以看到redis解析指令生成RESP字符串写入aof缓冲区之后再调用aofRewriteBufferAppend注册一个将缓冲区数据写入通道中的事件:

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    robj *tmpargv[3];

    //......
    //基于当前数据库生成select指令字符串
    if (dictid != server.aof_selected_db) {
        char seldb[64];

        snprintf(seldb,sizeof(seldb),"%d",dictid);
        buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
            (unsigned long)strlen(seldb),seldb);
        server.aof_selected_db = dictid;
    }
	//基于命令和参数生成命令的字符串
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
        /* Translate SETEX/PSETEX to SET and PEXPIREAT */
        tmpargv[0] = createStringObject("SET",3);
        tmpargv[1] = argv[1];
        tmpargv[2] = argv[3];
        buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
        decrRefCount(tmpargv[0]);
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else {
        /* All the other commands don't need translation or need the
         * same translation already operated in the command vector
         * for the replication itself. 生成字符串 */
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }

    //如果开启aof则将buf写入aof_buf
    if (server.aof_state == REDIS_AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    
    if (server.aof_child_pid != -1)//如果在进行aof重写将解析后指令的数据写入缓冲区
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    sdsfree(buf);
}

最终我们可以看到aofRewriteBufferAppend函数可以看到该方法会将上一步写入aof缓冲区的数据写入到10M的数据块,再判断当前aof_pipe_write_data_to_child是否为0(默认为-1,0说明没有任何事件,可以写入数据)则注册一个aofChildWriteDiffData方法将这些数据写入到通道中:

void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
    listNode *ln = listLast(server.aof_rewrite_buf_blocks);
    aofrwblock *block = ln ? ln->value : NULL;

    while(len) {
        /* If we already got at least an allocated block, try appending
         * at least some piece into it. */
        if (block) {
            unsigned long thislen = (block->free < len) ? block->free : len;
            if (thislen) {  /* The current block is not already full. */
            //将数据追加到aof_rewrite_buf_blocks中一个10M的数据块
                memcpy(block->buf+block->used, s, thislen);
                block->used += thislen;
                block->free -= thislen;
                s += thislen;
                len -= thislen;
            }
        }

       //......
    //查看aof_pipe_write_data_to_child是否有事件
    if (aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0) {
        //注册一个写事件调用aofChildWriteDiffData写入缓冲区
        aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,
            AE_WRITABLE, aofChildWriteDiffData, NULL);
    }
}

最后redis定时任务即定时的时间时间会轮询到注册的事件aofChildWriteDiffData,将数据块的数据取出并写入到aof_pipe_write_data_to_child所指向的即父进程写数据到子进程的数组中:

void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {
  //......

    while(1) {
        //取出数据块
        ln = listFirst(server.aof_rewrite_buf_blocks);
        block = ln ? ln->value : NULL;
        if (server.aof_stop_sending_diff || !block) {
            aeDeleteFileEvent(server.el,server.aof_pipe_write_data_to_child,
                              AE_WRITABLE);
            return;
        }
        if (block->used > 0) {
            //将数据写入1通道传给子进程
            nwritten = write(server.aof_pipe_write_data_to_child,
                             block->buf,block->used);
            if (nwritten <= 0) return;
            memmove(block->buf,block->buf+nwritten,block->used-nwritten);
            block->used -= nwritten;
        }
        if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
    }
}

子进程如何保证可靠接收

后续的AOF重写的异步子进程会调用rewriteAppendOnlyFile遍历数据库键值完成重写之后,等到通道数据并完成写入后,双方各自发送确认ACK之后,再次将父进程写入通道的数据持久化到文件后,将数据刷盘:

int rewriteAppendOnlyFile(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    rio aof;
    FILE *fp;
    char tmpfile[256];
    int j;
    long long now = mstime();
    char byte;
    size_t processed = 0;

    /* Note that we have to use a different temp name here compared to the
     * one used by rewriteAppendOnlyFileBackground() function. */
    snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
        return REDIS_ERR;
    }

    server.aof_child_diff = sdsempty();
    rioInitWithFile(&aof,fp);
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);
    for (j = 0; j < server.dbnum; j++) {
        //根据遍历结果获得当前库
        char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
        redisDb *db = server.db+j;
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;
        //获取库的字典迭代器
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        /* SELECT the new DB */
        //写入切库指令
        if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;

        /* Iterate this DB writing every entry */
        //遍历库
        while((de = dictNext(di)) != NULL) {
            sds keystr;
            robj key, *o;
            long long expiretime;
            //获取键值对
            keystr = dictGetKey(de);
            o = dictGetVal(de);
            initStaticStringObject(key,keystr);

            expiretime = getExpire(db,&key);

            /* If this key is already expired skip it */
            if (expiretime != -1 && expiretime < now) continue;

            /* Save the key and associated value */
            if (o->type == REDIS_STRING) {//如果value是字符串则记录set指令
                /* Emit a SET command */
                char cmd[]="*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                /* Key and value */
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkObject(&aof,o) == 0) goto werr;
            } else if (o->type == REDIS_LIST) {//如果是list则用RPUSH插入到尾部
                if (rewriteListObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_SET) {//调用SADD遍历并存储
                if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_ZSET) {//调用ZADD进行遍历重写
                if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
            } else if (o->type == REDIS_HASH) {//调用HMSET进行重写
                if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
            } else {
                redisPanic("Unknown object type");
            }
            /* Save the expire time */
            if (expiretime != -1) {
                char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
                if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
            }
            /* Read some diff from the parent process from time to time. */
            if (aof.processed_bytes > processed+1024*10) {
                processed = aof.processed_bytes;
                aofReadDiffFromParent();
            }
        }
        dictReleaseIterator(di);
        di = NULL;
    }

    //刷盘
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;

    //......
    //等待父进程写入通道数据到来
	 int nodata = 0;
    mstime_t start = mstime();
    while(mstime()-start < 1000 && nodata < 20) {
    	//等待数据到来
        if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
        {
            nodata++;
            continue;
        }
        nodata = 0; 
        //从通道拿数据写入文件中
        aofReadDiffFromParent();
    }

    /* Ask the master to stop sending diffs. */
    //通过通道发送!,告知主进程停止发送新信号进行重写
    if (write(server.aof_pipe_write_ack_to_parent,"!",1) != 1) goto werr;
    if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
        goto werr;
  
    //收到parent确认信号后,确认收到后进行后续的最后数据写入和刷盘
    if (syncRead(server.aof_pipe_read_ack_from_parent,&byte,1,5000) != 1 ||
        byte != '!') goto werr;
    redisLog(REDIS_NOTICE,"Parent agreed to stop sending diffs. Finalizing AOF...");

   	//再一次通道中拿到父进程的数据
    aofReadDiffFromParent();

   //......
   //刷盘,将文件数据持久化到硬盘中
    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;
	
	//......
}

最后我们给出aofReadDiffFromParent方法,可以看到AOF重写子进程本质就是通过read方法获取aof_pipe_read_data_from_parent数组中父进程写入的数据到aof缓冲区buf中,最后回到外层函数完成数据写入,由此完成一次完整的可靠AOF重写:

//AOF重写时调用这个函数
ssize_t aofReadDiffFromParent(void) {
    char buf[65536]; /* Default pipe buffer size on most Linux systems. */
    ssize_t nread, total = 0;
    //读取数据到buf然后写入到aof_child_diff
    while ((nread =
            read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {
        server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
        total += nread;
    }
    return total;
}

小结

自此我们通过三篇文章完整的介绍了AOF写入重写的完整的流程,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

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

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

相关文章

window 安装 openssl

文章目录 前言window 安装 openssl1. 下载2. 安装3. 配置环境变量4. 测试 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会太差&#xff0c;实在白嫖的话…

LVS集群及其它的NAT模式

1.lvs集群作用&#xff1a;是linux的内核层面实现负载均衡的软件&#xff1b;将多个后端服务器组成一个高可用、高性能的服务器的集群&#xff0c;通过负载均衡的算法将客户端的请求分发到后端的服务器上&#xff0c;通过这种方式实现高可用和负载均衡。 2.集群和分布式&#…

Mattermost:一个强大的开源协作平台

Mattermost是一个强大的开源协作平台&#xff0c;基于云原生架构&#xff0c;为企业级用户提供安全、可扩展且自托管的消息传递解决方案。 一、平台特点 开源与定制性&#xff1a;Mattermost是一个开源项目&#xff0c;用户可以根据自身需求定制界面、添加功能或扩展其功能&am…

百川工作手机实现销售管理微信监控系统

在瞬息万变的商业战场中&#xff0c;每一分效率的提升都是企业制胜的关键。传统销售管理模式已难以满足现代企业对精准、高效、合规的迫切需求。今天&#xff0c;让我们一同探索如何利用工作手机这一创新工具&#xff0c;为您的销售团队装上智能翅膀&#xff0c;开启销售管理的…

计算云服务3

第三章 镜像服务 什么是镜像服务(IMS) 镜像服务(lmage ManagementService&#xff0c;IMS)提供镜像的生命周期管理能力。用户可以灵活地使用公共镜像、私有镜像或共享镜像申请弹性云服务器和裸金属服务器。同时&#xff0c;用户还能通过已有的云服务器或使用外部镜像文件创建…

【C++报错已解决】Invalid Use of ‘void’ Expression

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言一、问题描述1.1 报错示例1.2 报错分析1.3 解决思路 二、解决方法2.1 方法一&#xff1a;调整函数返回类型方法二…

ArcGIS的智慧与情怀

初识ArcGIS 在这个信息化的时代&#xff0c;ArcGIS如同一位智者&#xff0c;静静地伫立在地理信息系统的巅峰。初识它时&#xff0c;我仿佛走进了一片未知的领域&#xff0c;心中充满了好奇与期待。ArcGIS&#xff0c;这款专业的地理信息系统软件&#xff0c;凭借其强大的功能…

【Mutilism NPN三极管驱动P-MOS】2022-4-2

缘由NPN三极管驱动P-MOS异常导通-硬件开发-CSDN问答 有电感性负载应该接反向吸收泻放二极管才能保证安全&#xff1b; 同时建议修改电路使得工作更安全可靠&#xff0c;取消下拉电阻R2&#xff0c;R3用小于100欧姆左右串联一个发光管&#xff0c;这样既可可靠工作也能观察IO输出…

Labview_Workers5.0 学习笔记

1.Local Request 个人理解该类型的请求针对自身的&#xff0c;由EHL或者MHL到该vi的MHL中。 使用快速放置快捷键"Ctrl9"创建方法如下&#xff1a; 创建后的API接口命名均为rql开头&#xff0c;并且在所选main.vi中的MHL创建对应的条件分支。 此时使用该API函数就…

activemq-CVE-2022-41678

Apache ActiveMQ Jolokia 后台远程代码执行漏洞 Apache ActiveMQ在5.16.5&#xff0c;5.17.3版本及以前&#xff0c;后台Jolokia存在一处任意文件写入导致的远程代码执行漏洞。 启动环境 admin/admin 方法一&#xff1a;利用poc 这个方法受到ActiveMQ版本的限制&#xff0c;因…

XTuner 微调 LLM:1.8B, 部署

扫码立刻参与白嫖A100&#xff0c;书生大模型微调部署学习活动。亲测有效 内容来源&#xff1a;Tutorial/xtuner/personal_assistant_document.md at camp2 InternLM/Tutorial GitHubLLM Tutorial. Contribute to InternLM/Tutorial development by creating an account on G…

【Unity 实用技巧】为游戏截图添加自定义水印LOGO

1. 前言 大家好&#xff0c;我是Mark。在Unity开发中&#xff0c;屏幕截图功能是一项常用的功能&#xff0c;它常用于游戏分享而默认的截图往往缺乏辨识度。本文将介绍如何在Unity中实现带有自定义LOGO的屏幕截图&#xff0c;话不多说开搞~ 2. 最终效果 3. 示例代码 代码比较…

基于vue的可视化大屏2

这个可视化大屏分为四个部分 一个引入代码&#xff0c;引入全局 index.vue. 左边代码centerleft.vue 右边代码centerright.vue 中间代码center.vue 主代码&#xff1a; 这是一段 Vue 框架的代码。 在 <template> 部分&#xff1a; 定义了一个根 div 元素。其中包含一…

一些学习网站分享

一些学习网站分享&#xff1a; ✅力扣(LeetCode) 力扣 (LeetCode) 官网 - 全球极客挚爱的技术成长平台 力扣是一个刷题站&#xff0c;支持C&#xff0c;Java&#xff0c;Python等多种编程语言&#xff0c;并按难度分为简单、中等、困难三个等级。是真的能刷到大厂真题 ✅Gith…

程序员学CFA——经济学(六)

经济学&#xff08;六&#xff09; 国际贸易与资本流动国际贸易相关术语开放/封闭经济自由贸易/贸易保护贸易比价国内生产总值与国民生产总值 国际贸易的利弊分析益处弊端 从贸易中获益&#xff1a;比较优势比较优势和绝对优势比较优势的来源 贸易限制和贸易保护施行贸易保护政…

【Linux】WEB网站网络防火墙(WAF软件)Fail2ban:保护服务器免受恶意攻击的必备工具

随着互联网的迅速发展&#xff0c;服务器的安全性日益成为用户和管理员关注的焦点。恶意攻击者不断寻找机会侵入服务器&#xff0c;窃取敏感信息、破坏数据或者滥用系统资源。为了抵御这些威胁&#xff0c;许多安全工具应运而生&#xff0c;其中一款备受推崇的工具就是 Fail2ba…

基于Python的哔哩哔哩数据分析系统设计实现过程,技术使用flask、MySQL、echarts,前端使用Layui

背景和意义 随着互联网和数字媒体行业的快速发展&#xff0c;视频网站作为重要的内容传播平台之一&#xff0c;用户量和内容丰富度呈现爆发式增长。本研究旨在设计并实现一种基于Python的哔哩哔哩数据分析系统&#xff0c;采用Flask框架、MySQL数据库以及echarts数据可视化技术…

保密U盘仍然存在数据安全危机?该怎么用才能规避?

保密U盘以前主要用于国家涉密单位或部门&#xff0c;但随着人们对于信息安全的重视越来越高&#xff0c;在民用企事业单位以及个人用户方面也应用得日益广泛。 使用保密U盘在安全性上比普通U盘具有优势&#xff0c;但却仍然存在安全危机&#xff0c;具体为&#xff1a; 病毒和…

在Windows中使用开源高性能编辑器Zed(持续更新)

简介 “Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter. It’s also open source.” “Zed是一款高性能的支持多人协作的代码编辑器&#xff0c;由Atom和Tree-sitter的创建者开发。它也是开源的。” Zed主打“高性能”&…