深度解析RocketMq源码-持久化组件(一) MappedFile

1. 绪论

rocketmq之所以能够有如此大的吞吐量,离不开两个组件,一个是利用netty实现的高性能网络通信组件;另一个就是利用mmap技术实现的存储组件。而在rocketmq的存储组件中主要有三个组件,分别是持久化文件commitLog,让消费直接消费信息的文件consumerqueue,同时我们需要根据msgKey进行检索,所以还有一个indexFile。这三个文件是rocketmq存储组件的核心。本文将要介绍的是持久化组件commitLog中与磁盘交互的组件mappedFile。

2. mmap

在介绍commitLog之前,我们来看看它所使用的核心技术,mmap。

2.1 传统读写

我们来看看传统读操作有什么局限?如果是利用传统的读写操作从磁盘文件中读取一块数据的话,需要经过如下几步:

1.用户线程通过调用操作系统的read方法,发起读取磁盘文件的请求。

2.操作系统通过需要读取文件的inode信息,在页缓存中查询是否找到页内容,如果找到,便直接返回文件页内容。

3.如果未找到,便通过inode信息定位到磁盘文件地址,并且通过计算机的dma组件,将磁盘文件复制到页缓存中。

4.cpu再将页缓存数据返回给用户进程的缓冲区中。

通过上面的步骤看出,如果采用普通的读写操作,需要经过磁盘->内核空间页缓存->用户空间缓冲区->内核空间缓冲区->网卡这四个步骤。

2.2 零拷贝技术

2.2.1 物理地址和虚拟地址

1.什么是物理地址?

物理地址就是实际的物理内存的地址

2.什么是虚拟地址

在早期的计算机系统中,cpu其实操作的是物理地址,后来发现内存不够过后,便发明了swap技术,即将内存中的不经常访问的数据放到磁盘中去,这样内存空间可以存储更多的数据,给用户营造了一种内存很大的假象。而这些复杂的操作是操作系统完成的,而操作系统给用户提供的是一片连续的地址空间,这些地址就是虚拟地址。并且通过内存管理单元(mmu)实现虚拟地址和物理地址的转换。

2.2.2 mmap映射步骤

1.进程启动映射,并且在进程的虚拟地址空间中创建映射区域

其实就是进程会在用户空间中的一块专门的虚拟地址空间(直接内存中)划分一块区域用来存储映射磁盘文件的虚拟地址。

2.建立物理内存地址和虚拟内存地址的映射关系

即建立地址虚拟空间和磁盘文件的地址之间的映射关系

3.对映射空间进行访问,发生缺页异常,实现磁盘到主存的拷贝

在通过对虚拟地址空间进行读写时,会通过mmu转换成物理地址,操作系统发现对应的物理地址缺失,产生缺页异常,从而将磁盘文件读取到虚拟空间的这片内存中来。

2.2.3 mmap的优点

mmap其实就是零拷贝,通过上面传统操作的读写,可以看出从磁盘需要拷贝到内核空间中转后才能到用户空间。有没有办法减少中间这次拷贝呢?那就是利用mmap技术。

通过mmap技术可以将磁盘文件地址可以和进程虚拟地址空间中的一段虚拟地址映射,在对虚拟地址进行读写操作时,就相当于对磁盘文件进行读写操作。减少了两次用户缓冲区和内核空间页表复制的过程。

3.mappedFile

mappedfile是rocketmq真正将数据写入到磁盘的组件。接下来,我们看看mappedfile是如何存储数据的。

3.1.1 mappedFile的组成

public class DefaultMappedFile extends AbstractMappedFile {//一个page页的大小为4kb,即mappedFile隔4kb便写入一个byte值实现文件预热,前面说过mmap技术是利用缺页中断将磁盘中的数据加载到内存中的,而一个内存页的大小为4kb//如果不采用文件预热机制的话,1gb的commitLog需要发生26w次缺页中断,所以在初始化commitLog的时候,每隔4kb就写入一个假0,这样就一次性的将所以的文件页加载到了内存中。public static final int OS_PAGE_SIZE = 1024 * 4;public static final Unsafe UNSAFE = getUnsafe();private static final Method IS_LOADED_METHOD;public static final int UNSAFE_PAGE_SIZE = UNSAFE == null ? OS_PAGE_SIZE : UNSAFE.pageSize();protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);//加载过的总的虚拟内存大小protected static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);//加载过的总的虚拟内存数量protected static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);protected static final AtomicIntegerFieldUpdater<DefaultMappedFile> WROTE_POSITION_UPDATER;protected static final AtomicIntegerFieldUpdater<DefaultMappedFile> COMMITTED_POSITION_UPDATER;protected static final AtomicIntegerFieldUpdater<DefaultMappedFile> FLUSHED_POSITION_UPDATER;//当前虚拟buffer的写指针protected volatile int wrotePosition;//虚拟buffer的读指针protected volatile int committedPosition;//flush指针protected volatile int flushedPosition;//文件大小protected int fileSize;protected FileChannel fileChannel;/*** Message will put to here first, and then reput to FileChannel if writeBuffer is not null.*/protected ByteBuffer writeBuffer = null;//在高并发场景,直接内存可能也抵挡不住,所以可以先将数据写入到堆内存中,然后再commiit到直接内存中,最后通过flush到磁盘中protected TransientStorePool transientStorePool = null;//文件名,mappedfile的文件名protected String fileName;//当前文件写入offsetprotected long fileFromOffset;protected File file;//这个是对应的虚拟bufferprotected MappedByteBuffer mappedByteBuffer;//存储的时间protected volatile long storeTimestamp = 0;protected boolean firstCreateInQueue = false;//最后一次flush的时间private long lastFlushTime = -1L;protected MappedByteBuffer mappedByteBufferWaitToClean = null;protected long swapMapTime = 0L;protected long mappedByteBufferAccessCountSinceLastSwap = 0L;
}

3.3.2 mmapedFile的写入的两种模式-是否开启瞬时缓存技术

1.如果不开启瞬时缓存技术

在写入的时候,直接写入到直接内存中(MapedFileBuffer),然后flush到磁盘

2.如果开启瞬时缓存技术

如果开启瞬时缓存技术的话,数据会先写入bytebuffer中,然后commit到MappedBytebuffer,最后再flush到磁盘中去。其实commit和flush都是采用异步线程刷入来实现的,所以增加了吞吐量。

3. 瞬时缓存技术TransientStorePool

初始化:

public class TransientStorePool {private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);//有多少内存页private final int poolSize;private final int fileSize;//核心:就是多个ByteBuffer队列private final Deque<ByteBuffer> availableBuffers;private volatile boolean isRealCommit = true;
}

借内存页

其实就是从内存队列中取出第一个buffer。

 public ByteBuffer borrowBuffer() {ByteBuffer buffer = availableBuffers.pollFirst();if (availableBuffers.size() < poolSize * 0.4) {log.warn("TransientStorePool only remain {} sheets.", availableBuffers.size());}return buffer;}

归还内存页

其实就是清空buffer,放入到内存队列中。

  public void returnBuffer(ByteBuffer byteBuffer) {byteBuffer.position(0);byteBuffer.limit(fileSize);this.availableBuffers.offerFirst(byteBuffer);}

3.3.3 mappedFile的初始化

在介绍完上面mappedFile的两种写入方式过后,mappedFile的初始化就很清晰了。

    public void init(final String fileName, final int fileSize,final TransientStorePool transientStorePool) throws IOException {init(fileName, fileSize);//这个是开启瞬时存储技术的话,需要从transientStorePool获取到一个bufferthis.writeBuffer = transientStorePool.borrowBuffer();this.transientStorePool = transientStorePool;}private void init(final String fileName, final int fileSize) throws IOException {this.fileName = fileName;this.fileSize = fileSize;//构造文件this.file = new File(fileName);//可以看出,mappedfile的文件名就是他的offsetthis.fileFromOffset = Long.parseLong(this.file.getName());boolean ok = false;//确保文件夹初始化完成UtilAll.ensureDirOK(this.file.getParent());try {this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();//核心:就是将mappedByteBuffer映射到对于的磁盘文件上this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);TOTAL_MAPPED_FILES.incrementAndGet();ok = true;} catch (FileNotFoundException e) {log.error("Failed to create file " + this.fileName, e);throw e;} catch (IOException e) {log.error("Failed to map file " + this.fileName, e);throw e;} finally {if (!ok && this.fileChannel != null) {this.fileChannel.close();}}}

至此,mappedfile中的直接内存缓冲区映射完成,堆内存buffer也初始化完成。

3.3.4 mappedFile的数据写入

分析这段逻辑其实我们可以发现,本质上是调用的AppendMessageCallback方法,写入数据封装到了MessageExt中,并且返回了PutMessageContext这一写入结果。

    public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb,PutMessageContext putMessageContext) {assert messageExt != null;assert cb != null;//获取当前的写指针int currentPos = WROTE_POSITION_UPDATER.get(this);//判断写指针大小是否超过了文件大小,如果超过便抛出异常if (currentPos < this.fileSize) {ByteBuffer byteBuffer = appendMessageBuffer().slice();byteBuffer.position(currentPos);AppendMessageResult result;if (messageExt instanceof MessageExtBatch && !((MessageExtBatch) messageExt).isInnerBatch()) {// traditional batch messageresult = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos,(MessageExtBatch) messageExt, putMessageContext);} else if (messageExt instanceof MessageExtBrokerInner) {// traditional single message or newly introduced inner-batch message//本质上是调用的AppendMessageCallback的doAppend方法result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos,(MessageExtBrokerInner) messageExt, putMessageContext);} else {return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);}//更新写指针WROTE_POSITION_UPDATER.addAndGet(this, result.getWroteBytes());//更新时间戳this.storeTimestamp = result.getStoreTimestamp();return result;}log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);}
1. MappedFile的写入数据格式-MessageExt

MessageExt包含了消息的一些元信息和它的实际内容。

public class Message implements Serializable {private static final long serialVersionUID = 8445773977080406428L;//消息属于哪个topicprivate String  topic;private int flag;//额外的附加属性private Map<String, String> properties;//真正的消息内容private byte[] body;//如果是事务消息,有事务idprivate String transactionId;
}public class MessageExt extends Message {private static final long serialVersionUID = 5720810158625748049L;//当前broker的名称private String brokerName;//消息属于哪个queueIdprivate int queueId;//消息大小private int storeSize;//消息在queue中的偏移量是多少private long queueOffset;private int sysFlag;//消息产生时间private long bornTimestamp;//消息是诞生主机的ip地址private SocketAddress bornHost;private long storeTimestamp;//消息存储主机的ip地址private SocketAddress storeHost;//消息idprivate String msgId;//消息在commitLog中的offset是多少private long commitLogOffset;private int bodyCRC;private int reconsumeTimes;private long preparedTransactionOffset;}

2.mappedFile的核心写入逻辑-AppendMessageCallback

其实就是在MessageExt的基础上,补充上写入commitLog的一些信息,并且刷新到buffer中

    //byteBuffer - 消息写入到直接内存的buffer//preEncodeBuffer - 消息内容public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,final MessageExtBrokerInner msgInner, PutMessageContext putMessageContext) {// STORETIMESTAMP + STOREHOSTADDRESS + OFFSET <br>//取出消息的内容preEncodeBuffer里面ByteBuffer preEncodeBuffer = msgInner.getEncodedBuff();boolean isMultiDispatchMsg = messageStoreConfig.isEnableMultiDispatch() && CommitLog.isMultiDispatchMsg(msgInner);if (isMultiDispatchMsg) {AppendMessageResult appendMessageResult = handlePropertiesForLmqMsg(preEncodeBuffer, msgInner);if (appendMessageResult != null) {return appendMessageResult;}}//获取消息的总长度final int msgLen = preEncodeBuffer.getInt(0);preEncodeBuffer.position(0);preEncodeBuffer.limit(msgLen);// PHY OFFSET//当前文件的位置+消息长度 = 实际开始写入的位置long wroteOffset = fileFromOffset + byteBuffer.position();//构建消息id 机器ip+端口号+偏移量Supplier<String> msgIdSupplier = () -> {int sysflag = msgInner.getSysFlag();int msgIdLen = (sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 : 16 + 4 + 8;ByteBuffer msgIdBuffer = ByteBuffer.allocate(msgIdLen);MessageExt.socketAddress2ByteBuffer(msgInner.getStoreHost(), msgIdBuffer);msgIdBuffer.clear();//because socketAddress2ByteBuffer flip the buffermsgIdBuffer.putLong(msgIdLen - 8, wroteOffset);return UtilAll.bytes2string(msgIdBuffer.array());};// Record ConsumeQueue information//记录consumerqueue中的偏移量Long queueOffset = msgInner.getQueueOffset();// this msg maybe an inner-batch msg.short messageNum = getMessageNum(msgInner);// Transaction messages that require special handlingfinal int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag());switch (tranType) {// Prepared and Rollback message is not consumed, will not enter the consume queuecase MessageSysFlag.TRANSACTION_PREPARED_TYPE:case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:queueOffset = 0L;break;case MessageSysFlag.TRANSACTION_NOT_TYPE:case MessageSysFlag.TRANSACTION_COMMIT_TYPE:default:break;}//如果写入文件大小超过了磁盘大小,抛出END_OF_FILE的异常// Determines whether there is sufficient free spaceif ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {this.msgStoreItemMemory.clear();// 1 TOTALSIZEthis.msgStoreItemMemory.putInt(maxBlank);// 2 MAGICCODEthis.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);// 3 The remaining space may be any value// Here the length of the specially set maxBlankfinal long beginTimeMills = CommitLog.this.defaultMessageStore.now();byteBuffer.put(this.msgStoreItemMemory.array(), 0, 8);return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset,maxBlank, /* only wrote 8 bytes, but declare wrote maxBlank for compute write position */msgIdSupplier, msgInner.getStoreTimestamp(),queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);}//更新queue中偏移量、物理地址、产生消息的时间戳、机器地址等,int pos = 4 + 4 + 4 + 4 + 4;// 6 QUEUEOFFSETpreEncodeBuffer.putLong(pos, queueOffset);pos += 8;// 7 PHYSICALOFFSETpreEncodeBuffer.putLong(pos, fileFromOffset + byteBuffer.position());int ipLen = (msgInner.getSysFlag() & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4;// 8 SYSFLAG, 9 BORNTIMESTAMP, 10 BORNHOST, 11 STORETIMESTAMPpos += 8 + 4 + 8 + ipLen;// refresh store time stamp in lockpreEncodeBuffer.putLong(pos, msgInner.getStoreTimestamp());if (enabledAppendPropCRC) {// 18 CRC32int checkSize = msgLen - crc32ReservedLength;ByteBuffer tmpBuffer = preEncodeBuffer.duplicate();tmpBuffer.limit(tmpBuffer.position() + checkSize);int crc32 = UtilAll.crc32(tmpBuffer);tmpBuffer.limit(tmpBuffer.position() + crc32ReservedLength);MessageDecoder.createCrc32(tmpBuffer, crc32);}final long beginTimeMills = CommitLog.this.defaultMessageStore.now();CommitLog.this.getMessageStore().getPerfCounter().startTick("WRITE_MEMORY_TIME_MS");// Write messages to the queue buffer//写入文件到直接内存中byteBuffer.put(preEncodeBuffer);CommitLog.this.getMessageStore().getPerfCounter().endTick("WRITE_MEMORY_TIME_MS");msgInner.setEncodedBuff(null);if (isMultiDispatchMsg) {CommitLog.this.multiDispatch.updateMultiQueueOffset(msgInner);}//返回写入成功return new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgIdSupplier,msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills, messageNum);}
3.,mappedFile的写入结果-AppendMessageResult

主要是返回了写入的物理地址+消息的MsgId+consumerqueue中的逻辑地址。可以看出MsgId其实是在写入到commitLog后生成的,因为里面需要包含写入的物理地址。

public class AppendMessageResult {// Return codeprivate AppendMessageStatus status;// Where to start writingprivate long wroteOffset;// Write Bytesprivate int wroteBytes;// Message IDprivate String msgId;private Supplier<String> msgIdSupplier;// Message storage timestampprivate long storeTimestamp;// Consume queue's offset(step by one)private long logicsOffset;private long pagecacheRT = 0;
}

3.3.5 mappedFile的数据commit操作

其实就是将数据写入到bytebuffer中.

  public int commit(final int commitLeastPages) {//如果没有开启瞬时缓存技术,直接返回写指针if (writeBuffer == null) {//no need to commit data to file channel, so just regard wrotePosition as committedPosition.return WROTE_POSITION_UPDATER.get(this);}//no need to commit data to file channel, so just set committedPosition to wrotePosition.if (transientStorePool != null && !transientStorePool.isRealCommit()) {COMMITTED_POSITION_UPDATER.set(this, WROTE_POSITION_UPDATER.get(this));} else if (this.isAbleToCommit(commitLeastPages)) {if (this.hold()) {commit0();this.release();} else {log.warn("in commit, hold failed, commit offset = " + COMMITTED_POSITION_UPDATER.get(this));}
}
 protected void commit0() {int writePos = WROTE_POSITION_UPDATER.get(this);int lastCommittedPosition = COMMITTED_POSITION_UPDATER.get(this);if (writePos - lastCommittedPosition > 0) {try {ByteBuffer byteBuffer = writeBuffer.slice();byteBuffer.position(lastCommittedPosition);byteBuffer.limit(writePos);//其实就是将当前数据写入到对内存buffer中this.fileChannel.position(lastCommittedPosition);this.fileChannel.write(byteBuffer);//更新commit指针COMMITTED_POSITION_UPDATER.set(this, writePos);} catch (Throwable e) {log.error("Error occurred when commit data to FileChannel.", e);}}}

3.3.6 mappedFile数据的flush操作

 public int flush(final int flushLeastPages) {if (this.isAbleToFlush(flushLeastPages)) {if (this.hold()) {int value = getReadPosition();try {this.mappedByteBufferAccessCountSinceLastSwap++;//We only append data to fileChannel or mappedByteBuffer, never both.if (writeBuffer != null || this.fileChannel.position() != 0) {this.fileChannel.force(false);} else {//利用force方法,将缓存中数据刷入到磁盘中this.mappedByteBuffer.force();}this.lastFlushTime = System.currentTimeMillis();} catch (Throwable e) {log.error("Error occurred when force data to disk.", e);}FLUSHED_POSITION_UPDATER.set(this, value);this.release();} else {log.warn("in flush, hold failed, flush offset = " + FLUSHED_POSITION_UPDATER.get(this));FLUSHED_POSITION_UPDATER.set(this, getReadPosition());}}return this.getFlushedPosition();}

3.3.7 mappedFile数据的内存预热

    public void warmMappedFile(FlushDiskType type, int pages) {this.mappedByteBufferAccessCountSinceLastSwap++;long beginTime = System.currentTimeMillis();ByteBuffer byteBuffer = this.mappedByteBuffer.slice();long flush = 0;// long time = System.currentTimeMillis();//fileSize:总的mappedfile大小,即每隔4kb,便向mappedByteBuffer写入一个零,否者的话,会产生频繁的页中断,导致磁盘和数据频繁交互。for (long i = 0, j = 0; i < this.fileSize; i += DefaultMappedFile.OS_PAGE_SIZE, j++) {byteBuffer.put((int) i, (byte) 0);// 如果是同步刷盘的话,便会进行缓存预热,因为同步刷盘可能一次数据量很小,造成频繁的os//与buffer的交互if (type == FlushDiskType.SYNC_FLUSH) {if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {flush = i;mappedByteBuffer.force();}}}

4.总结

mappedFile本质上是利用mapp技术来提高读写效率的,而mappedFile的核心本质上就是mappedFileBuffer,默认大小为1G,可以由mappedFileSizeCommitLog来进行控制。

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

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

相关文章

多模态MLLM都是怎么实现的(10)-Chameleon和Florence-2如果你想玩多模态就不能不了解

这个也是一个补充文&#xff0c;前9章基本把该讲的讲了&#xff0c;今天这个内容主要是因为Meta出了一个Chameleon&#xff0c;这个以后可能会成为LLaMA的一个很好的补充&#xff0c;或者说都有可能统一起来&#xff0c;叫LLaMA或者Chamleon或者什么别的&#xff0c;另外我司把…

【图解IO与Netty系列】Netty源码解析——事件循环

Netty源码解析——事件循环 Netty事件循环源码解析select()processSelectedKeys()NioMessageUnsafe#read()NioByteUnsafe#read() runAllTasks() Netty事件循环 当Netty服务端启动起来以后&#xff0c;就可以接受客户端发送的请求&#xff0c;接收到客户端发来的请求后就会有事…

计算机网络 交换机的VLAN配置

一、理论知识 1.VLAN的定义 ①VLAN虚拟局域网&#xff0c;是一种通过将局域网内的设备逻辑地而不是物理地划分成一个个网段从而实现虚拟工作组的技术。 ②IEEE于1999年颁布了用以标准化VLAN实现方案的802.1Q协议标准草案。 ③VLAN技术允许网络管理者将一个物理的LAN逻辑地划…

MySQL存储管理(一):删数据

从表中删除数据 从表中删除数据&#xff0c;也即是delete过程。 什么是表空间 表空间可以看做是InnoDB存储引擎逻辑结构的最高层&#xff0c;所有的数据都存放在表空间中。默认情况下&#xff0c;InnoDB存储引擎有一个共享表空间idbdata1&#xff0c;即所有数据都存放在这个表…

无限滚动表格

纵向无限滚动 单元格内部横向滚动 <!--* Description: 横向、纵向滚动表格* Author: liyanfeng liyanfenghopewind.com* Date: 2024-06-15 16:06:57* LastEditors: liyanfeng liyanfenghopewind.com* LastEditTime: 2024-06-20 17:15:37* FilePath: \plus-ui\src\componen…

SEO是什么?SEO相关发展历史

一、SEO是什么意思&#xff1f; SEO&#xff08;Search Engine Optimization&#xff09;&#xff0c;翻译成中文就是“搜索引擎优化”。简单来讲&#xff0c;seo是指自然搜索结果下获得的网站流量的技术&#xff0c;是可以不用花钱就可以让自己的网站有好的排名&#xff0c;也…

C语言:生命周期和作用域,static和extern

关键字static与extern 1.作用域&#xff08;scope&#xff09;&#xff1a;代码中能够访问到变量的范围&#xff08;变量可以被使用的文本区间&#xff09;。&#xff08;分为全局作用域和局部作用域&#xff09; ☺全局作用域&#xff1a;在整个程序中都能访问的变量。通常…

C语言入门系列:数据类型转换

文章目录 一&#xff0c;自动类型转换1&#xff0c;赋值运算1.1&#xff0c;浮点数赋值给整型变量-不安全1.2&#xff0c;整数赋值给浮点数变量-安全1.3&#xff0c;窄类型赋值给宽类型-安全1.4&#xff0c;宽类型赋值给窄类型-不安全 2&#xff0c;混合类型的运算2.1&#xff…

Ubuntu24使用kubeadm部署高可用K8S集群

Ubuntu24使用kubeadm部署高可用K8S集群 使用kubeadm部署一个k8s集群&#xff0c;3个master1个worker节点。 1. 环境信息 操作系统&#xff1a;ubuntu24.04内存: 2GBCPU: 2网络: 能够互访&#xff0c;能够访问互联网 hostnameip备注k8s-master1192.168.0.51master1k8s-maste…

20.Cargo和Crates.io

标题 一、采用发布配置自定义构建1.1 默认配置1.2 修改配置项 二、将crate发布到Crates.io2.1 编写文档注释2.2 常用&#xff08;文档注释&#xff09;部分2.3 文档注释作用测试2.4 为包含注释的项添加文档注释2.5 使用pub use导出公有API2.6 创建Crates.io账号2.7 发布2.8 版本…

基于STM32的智能停车场管理系统

目录 引言环境准备智能停车场管理系统基础代码实现&#xff1a;实现智能停车场管理系统 4.1 车位检测模块4.2 数据处理与分析4.3 控制系统实现4.4 用户界面与数据可视化应用场景&#xff1a;智能停车场管理与优化问题解决方案与优化收尾与总结 1. 引言 智能停车场管理系统通…

Linux常用命令(17)—pastesortcomm命令(有相关截图)

写在前面&#xff1a; 最近在学习Linux命令&#xff0c;记录一下学习Linux常用命令的过程&#xff0c;方便以后复习。仅供参考&#xff0c;若有不当的地方&#xff0c;恳请指正。如果对你有帮助&#xff0c;欢迎点赞&#xff0c;关注&#xff0c;收藏&#xff0c;评论&#xf…

仿中波本振电路的LC振荡器电路实验

手里正好有一套中波收音机套件的中周。用它来测试一下LC振荡器&#xff0c;电路如下&#xff1a; 用的是两只中频放大的中周&#xff0c;初步测试是用的中周自带的瓷管电容&#xff0c;他们应该都是谐振在465k附近。后续测试再更换电容测试。 静态电流&#xff0c;0.5到1mA。下…

malloc和new的本质区别

目录 一、结论 二、示例 1.实现类T 2.用malloc分配类T的内存空间 3.用new分配类T的内存空间 一、结论 malloc 和 new 都是用于在运行时动态分配内存的机制。但它们之间存在一些本质的区别&#xff0c;主要是在使用方面&#xff0c;现在我们直接说结论&#xff0c;然后在通过…

ArcGIS与Excel分区汇总统计三调各地类面积!数据透视表与汇总统计!

​ 点击下方全系列课程学习 点击学习—>ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放 点击学习——>遥感影像综合处理4大遥感软件ArcGISENVIErdaseCognition 01 需求说明 介绍一下ArcGIS与Excel统计分区各地类的三调地类面积。 ArcGIS统计分析不会&#x…

Unity客户端的Http通讯实战

背景知识 在Unity游戏开发中&#xff0c;一个常见场景是&#xff0c;后端扔过来一个Swagger后端接口网页&#xff0c;需要你使用对应的接口对应的接口发送和接收数据&#xff0c;如图所示为发起Get请求&#xff1a; 我们可以通过点击Try it out按钮直接在网页上测试收发数据&a…

spring整合openAI大模型之Spring AI

文章目录 一、SpringAI简介1.什么是SpringAI2.SpringAI支持的大模型类型&#xff08;1&#xff09;聊天模型&#xff08;2&#xff09;文本到图像模型&#xff08;3&#xff09;转录&#xff08;音频到文本&#xff09;模型&#xff08;4&#xff09;嵌入模型&#xff08;5&…

Guava-EventBus 源码解析

EventBus 采用发布订阅者模式的实现方式&#xff0c;它实现了泛化的注册方法以及泛化的方法调用,另外还考虑到了多线程的问题,对多线程使用时做了一些优化&#xff0c;观察者模式都比较熟悉&#xff0c;这里会简单介绍一下&#xff0c;重点介绍的是如何泛化的进行方法的注册以及…

dial tcp 10.96.0.1:443: connect: no route to host

1、创建Pod一直不成功&#xff0c;执行kubectl describe pod runtime-java-c8b465b98-47m82 查看报错 Warning FailedCreatePodSandBox 2m17s kubelet Failed to create pod sandbox: rpc error: code Unknown desc failed to setup network for…

数据挖掘与分析——数据预处理

数据探索 波士顿房价数据集&#xff1a;卡内基梅隆大学收集&#xff0c;StatLib库&#xff0c;1978年&#xff0c;涵盖了麻省波士顿的506个不同郊区的房屋数据。 一共含有506条数据。每条数据14个字段&#xff0c;包含13个属性&#xff0c;和一个房价的平均值。 数据读取方法…