来聊聊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) {//重写aofsize_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事件则调用feedAppendOnlyFileif (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_bufif (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_diffwhile ((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://xiahunao.cn/news/3225528.html

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

相关文章

神经网络中的激活函数

目录 一、什么是激活函数&#xff1a;二、如何选择激活函数&#xff1a;1.Sigmoid激活函数&#xff1a;2.线性激活函数&#xff1a;3.ReLU激活函数&#xff1a; 一、什么是激活函数&#xff1a; 激活函数是神经网络中的一种函数&#xff0c;它在神经元中起到了非线性映射的作用…

【附源码】ttkbootstrap实现GUI信息管理系统

【附源码】ttkbootstrap实现GUI信息管理系统 文章目录 【附源码】ttkbootstrap实现GUI信息管理系统效果预览环境搭建功能实现展示学生信息表格新增学生信息表单修改学生信息表单删除学生信息 代码解析完整代码运行和测试结尾 效果预览 环境搭建 Python 3.8 ttkbootstrap 1.10.…

IPD流程验证阶段模板及表单

目录 简介 内容brief&#xff08;部分截图&#xff09; 作者简介 简介 前面几期分享了 IPD 开发流程中的&#xff0c; 概念、计划、开发阶段的相关资料。 今天就来分享一下验证阶段的资料及表单内容。 在 IPD 流程的这个阶段&#xff0c; 就不仅仅是测试功能的实现这么…

AD9361的0x05E寄存器的说明

AD9361的0x05E寄存器在配置过程中扮演着重要的角色&#xff0c;特别是在与基带锁相环&#xff08;Base Band PLL, BB-PLL&#xff09;的状态监测相关时。以下是对AD9361的0x05E寄存器的详细说明&#xff1a; 一、功能概述 AD9361的0x05E寄存器通常用于监测BB-PLL的状态&#…

【国产开源可视化引擎Meta2d.js】鹰眼地图

鹰眼地图 画布右下角弹出一个缩略导航地图&#xff0c;鼠标点击可以跳到指定位置。 在线体验&#xff1a; 乐吾乐2D可视化 示例&#xff1a; // 显示缩略地图 meta2d.showMap();// 关闭缩略地图 meta2d.hideMap();

多会话 Telnet 日志记录器

创建一个多会话 Telnet 日志记录器可以实现对多个 Telnet 会话进行连接、监控和记录日志。以下是一个基本的 Python 示例&#xff0c;使用 telnetlib 库来实现多会话 Telnet 日志记录器&#xff0c;并使用 threading 模块来处理多个会话。 1、问题背景 我们需要编写一个脚本&a…

合合信息大模型加速器重磅上线,释放智能文档全新可能

目录 0 写在前面1 高速文档解析引擎&#xff1a;拓宽大模型认知边界2 文本嵌入模型acge&#xff1a;克服大模型感知缺陷3 行业赋能&#xff1a;以百川智能为例总结 0 写在前面 随着人工智能技术的飞速发展&#xff0c;大模型以强大的数字处理能力和深度学习能力&#xff0c;不…

Spark RDD优化

Spark RDD优化 一、分区优化二、持久化优化三、依赖优化四、共享变量优化五、提交模式与运行模式优化六、其他优化 一、分区优化 分区数调整&#xff1a;RDD的分区数可以通过repartition和coalesce方法进行调整。合理的分区数可以提高并行度&#xff0c;但过多的分区会增加管…

数据库之DML

1&#xff0c;创建表 mysql> create table student(-> id int primary key,-> name varchar(20) not null,-> grade float-> );插入记录 mysql> insert into student values(1,monkey,98.5); Query OK, 1 row affected (0.01 sec)一次性插入多条记录 mysql…

Rti DDS qos

1. parent.allow_interfaces_list 字符串列表&#xff0c;每个字符串标识一系列接口地址或接口名称。接口必须指定为逗号分隔的字符串&#xff0c;每个逗号分隔一个接口。 例如&#xff0c;以下是可接受的字符串&#xff1a; 192.168.1.1 192.168.1.* 192.168.* 192.* e…

高速电吹风方案介绍,多档温度风速调节,转速可达105000RPM

高速电吹风是这几年很火的一种电动小家电&#xff0c;能够在较短时间内完成头发干燥&#xff0c;减少对头发的热损伤。可以通过高速电机和风扇来产生高速风流&#xff0c;迅速将头发表面的水分吹干。高速电吹风通常配有多种档位风速和温度可以设置&#xff0c;用户可以根据需要…

VS安装Qt扩展工具

1-Visual Studio中安装QT插件 **插件下载地址&#xff1a;**http://download.qt.io/development_releases/vsaddin/ 关闭VS,双击下载的QT插件&#xff0c;默认安装即可&#xff1b; &#xff08;1&#xff09;配置Qt的MSVC编译器安装路径 打开Visual Studio&#xff0c;在菜单栏…

CentOS 6.5配置国内在线yum源和制作openssh 9.8p1 rpm包 —— 筑梦之路

CentOS 6.5比较古老的版本了&#xff0c;而还是有一些古老的项目仍然在使用。 环境说明 1. 更换国内在线yum源 CentOS 6 在线可用yum源配置——筑梦之路_centos6可用yum源-CSDN博客 cat > CentOS-163.repo << EOF [base] nameCentOS-$releasever - Base - 163.com …

unity使用 MQTT复现plant simulate仿真

unity使用 MQTT复现plant simulate仿真 一、plant simulate端配置 1、plant simulate MQTT组件配置,该组件在类库的信息流类目下,端口不变,填写ip即可; 2、设备配置界面,在控件入口和出口处各挂一个脚本,当物料出入该设备时会分别触发执行这两个脚本,粘贴如下代码; E…

Windows 黑暗模式是什么意思?如何开启它?

随着计算机和移动设备的普及&#xff0c;长时间盯着屏幕已经成为现代人生活和工作的常态。为了减轻眼睛疲劳&#xff0c;并在低光环境中提供更舒适的视觉体验&#xff0c;许多操作系统和应用程序都引入了黑暗模式&#xff08;Dark Mode&#xff09;。 Windows 黑暗模式就是其中…

(补充):java各种进制和文本、图像、音频在计算机中的存储方式

文章目录 前言一、进制1 逢几进一2 常见进制在java中的表示3 进制中的转换(1)任意进制转十进制(2)十进制转其他进制二、计算机中的存储1 计算机的存储规则(文本数据)(1)ASCII码表(2)编码规则的发展演化2 计算机的存储规则(图片数据)(1)分辨率、像素(2)黑白图与灰度…

基于Java中的SSM框架实现疫情冷链追溯系统项目【项目源码+论文说明】

基于Java中的SSM框架实现疫情冷链追溯系统演示 摘要 近几年随着城镇化发展和居民消费水平的不断提升&#xff0c;人们对健康生活方式的追求意识逐渐加强&#xff0c;生鲜食品逐渐受到大众青睐&#xff0c;诸如盒马鲜生、7-fresh等品牌生鲜超市&#xff0c;一时间如雨后春笋般迅…

单片机中有FLASH为啥还需要EEROM?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 一是EEPROM操作简单&…

matlab数值溢出该怎么解决?

&#x1f3c6;本文收录于《CSDN问答解惑》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&…

ssm华天计算机面试刷题系统-计算机毕业设计源码22543

摘 要 华天计算机面试刷题系统是一款基于SSM&#xff08;Spring、Spring MVC、MyBatis&#xff09;框架、利用Java编程语言和MySQL数据库&#xff0c;开发的在线学习和测试平台。系统利用SSM框架及前端开发技术&#xff0c;实现了模块化开发和管理&#xff0c;前后端交互以及数…