拓冰建站拓冰建站
首页 / 资讯中心 / 正文

深入剖析 RocketMQ 存储基石:MappedFile 内存映射文件源码详解

深入剖析 RocketMQ 存储基石MappedFile 内存映射文件源码详解【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter本文基于 RocketMQ 4.9.3 源码完整剖析org.apache.rocketmq.store.MappedFile的初始化、写入、提交、刷盘与销毁全生命周期并串联 CommitLog、ConsumeQueue、IndexFile 等存储组件的实际调用关系帮助读者从源码层面理解 RocketMQ 高性能顺序写与可靠落盘的底层实现原理。读完本文你将能说清为什么 MappedFile 用内存映射commit 与 flush 的区别临时存储池TransientStorePool如何工作引用计数如何保证文件安全销毁等问题。RocketMQ 的存储体系建立在文件即内存映射的模型之上CommitLog消息物理存储文件、ConsumeQueue消费逻辑队列、IndexFile消息索引文件本质上都是通过MappedFile来管理的一块块内存映射文件。可以说读懂了MappedFile就拿到了理解 RocketMQ 存储引擎的钥匙。本仓库中与之配套的 RocketMQ CommitLog 详解、RocketMQ 消息发送存储流程、RocketMQ ConsumeQueue 详解 与 RocketMQ IndexFile 详解 均以此为基座可交叉阅读。MappedFile 在 RocketMQ 存储体系中的位置在 RocketMQ Broker 端消息的存储路径为${ROCKET_HOME}/store下的多个目录commitlog消息的物理存储文件默认每个文件大小 1GmappedFileSizeCommitLog即1024 * 1024 * 1024文件名以该文件承载的起始物理偏移量命名如00000000000000000000、00000000001073741824consumequeue消费队列目录第一级目录为主题名、第二级目录为队列 id其下文件同样由 MappedFile 承载index消息索引文件通过mappedByteBuffer直接操作内存映射区域。无论是MappedFileQueue管理的 CommitLog/ConsumeQueue 文件集合还是IndexFile单个文件底层都用到了MappedFile提供的核心能力存储组件与 MappedFile 的关系CommitLogMappedFileQueue管理commitlog目录下的一组 MappedFile每个 MappedFile 对应一个 1G 的物理文件ConsumeQueueConsumeQueue内部持有mappedFileQueue条目20 字节顺序追加到 MappedFile 的映射区域IndexFile自身就是一个内存映射文件直接持有mappedByteBuffer操作 IndexHeader、哈希槽与索引条目从 RocketMQ 消息发送存储流程 可以看到消息写入时先定位当前可写的 MappedFile再通过MappedFile.appendMessagesInner完成追加从 RocketMQ CommitLog 详解 可以看到读取消息时通过MappedFileQueue.findMappedFileByOffset定位文件再调用MappedFile.selectMappedBuffer切片返回。因此MappedFile是这一切的枢纽。MappedFile 核心字段与整体结构先概览MappedFile的关键字段它们是理解后续生命周期方法的基础// 文件路径 private String fileName; // 文件大小如 CommitLog 默认 1G private int fileSize; // 该文件的起始物理偏移量由文件名解析而来 private long fileFromOffset; // 文件对象 private File file; // 文件通道用于数据提交与强制落盘 private FileChannel fileChannel; // 堆外内存映射缓冲区直接映射到文件页缓存 private MappedByteBuffer mappedByteBuffer; // 临时存储池中的堆外内存transientStorePoolEnabletrue 时使用 private ByteBuffer writeBuffer; // 临时存储池 private TransientStorePool transientStorePool; // 当前写指针 private final AtomicInteger wrotePosition new AtomicInteger(0); // 已提交指针writeBuffer 中的数据已写入 FileChannel private final AtomicInteger committedPosition new AtomicInteger(0); // 已刷盘指针数据已落到磁盘 private final AtomicInteger flushedPosition new AtomicInteger(0);此外MappedFile继承了ReferenceResource拥有available、cleanupOver、refCount等引用计数与生命周期控制字段见后文销毁一节类中还定义了统计常量// 已映射的虚拟内存总量进程维度 public static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY new AtomicLong(0); // 已映射的文件个数进程维度 public static final AtomicInteger TOTAL_MAPPED_FILES new AtomicInteger(0); // 操作系统页大小4KB public static final int OS_PAGE_SIZE 1024 * 4;三个指针wrotePosition、committedPosition、flushedPosition严格满足flushedPosition committedPosition wrotePosition它们分别刻画了数据在用户态缓冲区 / 页缓存 / 磁盘三个层次上的推进程度。MappedFile 的初始化把文件映射进内存init 方法主流程MappedFile构造时最终会走到init方法这是整个类的起点private void init(final String fileName, final int fileSize) throws IOException { this.fileName fileName; this.fileSize fileSize; this.file new File(fileName); this.fileFromOffset Long.parseLong(this.file.getName()); boolean ok false; ensureDirOK(this.file.getParent()); try { this.fileChannel new RandomAccessFile(this.file, rw).getChannel(); 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(); } } }初始化主要做四件事1. 解析fileFromOffsetLong.parseLong(this.file.getName())把文件名直接解析成 long 类型的起始偏移量。这正是因为 commitlog 目录下的文件都是以起始物理偏移量命名的如00000000001073741824表示该文件从偏移量 1073741824 开始从文件名即可快速推导文件归属无需额外元数据。2. 确保目录存在调用ensureDirOK确认父目录存在不存在则递归创建public static void ensureDirOK(final String dirName) { if (dirName ! null) { if (dirName.contains(MessageStoreConfig.MULTI_PATH_SPLITTER)) { String[] dirs dirName.trim().split(MessageStoreConfig.MULTI_PATH_SPLITTER); for (String dir : dirs) { createDirIfNotExist(dir); } } else { createDirIfNotExist(dirName); } } }注意这里对MessageStoreConfig.MULTI_PATH_SPLITTER逗号,做了特殊处理RocketMQ 支持配置多个 commitlog 存储路径多目录负载均衡当目录名包含分隔符时按多目录逐一创建。从源码结构可以推断这是为了支持 CommitLog 多路径部署场景。3. 打开文件通道new RandomAccessFile(this.file, rw).getChannel()以读写模式打开文件并获取FileChannel这是后续提交与刷盘的基础。4. 建立内存映射this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize)使用 NIO 内存映射将文件从偏移 0 开始的fileSize字节映射到进程虚拟地址空间返回MappedByteBuffer。此后对mappedByteBuffer的读写操作系统会通过缺页中断机制与文件页缓存Page Cache交互数据写入最终由内核在合适的时机刷回磁盘这正是 RocketMQ 顺序写高吞吐的根本保障。初始化成功后TOTAL_MAPPED_VIRTUAL_MEMORY累加映射大小、TOTAL_MAPPED_FILES自增这两个进程级计数器会被 DefaultMessageStore 的周期任务用于统计已映射虚拟内存总量与已映射文件个数。如果中途失败finally块会及时关闭已打开的fileChannel避免资源泄漏。一个值得注意的细节fileChannel.map映射的是虚拟内存映射过程本身并不立刻占用等量的物理内存RAM物理页只在真正读写时按页4KB加载。因此TOTAL_MAPPED_VIRTUAL_MEMORY反映的是虚拟地址空间占用而非物理内存占用这也解释了为何多个 1G 的 CommitLog 文件可以同时处于映射状态而不至于立刻 OOM。MappedFile 的数据写入wrotePosition 的推进消息追加的入口是appendMessagesInner这在 RocketMQ 消息发送存储流程 中有完整调用链。其核心逻辑如下public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb, PutMessageContext putMessageContext) { assert messageExt ! null; assert cb ! null; int currentPos this.wrotePosition.get(); if (currentPos this.fileSize) { ByteBuffer byteBuffer writeBuffer ! null ? writeBuffer.slice() : this.mappedByteBuffer.slice(); byteBuffer.position(currentPos); AppendMessageResult result; if (messageExt instanceof MessageExtBrokerInner) { result cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt, putMessageContext); } else if (messageExt instanceof MessageExtBatch) { result cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt, putMessageContext); } else { return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); } this.wrotePosition.addAndGet(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); }要点拆解写入位置由wrotePosition原子变量驱动int currentPos this.wrotePosition.get()读取当前写指针doAppend回调负责在byteBuffer的currentPos处编码并写入整条消息返回AppendMessageResult包含写入字节数、写入偏移量等随后wrotePosition.addAndGet(result.getWroteBytes())原子推进写指针。原子变量保证了多线程并发追加时的线程安全。双缓冲选择writeBuffer ! null ? writeBuffer.slice() : this.mappedByteBuffer.slice()。开启临时存储池transientStorePoolEnabletrue时数据先写入堆外内存writeBuffer之后由 commit 阶段批量搬到文件通道否则直接写入mappedByteBuffer页缓存。两种模式在后续提交与刷盘两节中体现差异。storeTimestamp记录每次追加后记录该文件的最后存储时间戳供 ConsumeQueue 的按时间定位文件RocketMQ ConsumeQueue 详解 中的getMappedFileByTime使用。追加前会校验currentPos this.fileSize文件写满后拒绝追加并返回UNKNOWN_ERROR由上层CommitLog触发创建新文件。实际的消息体编码发生在CommitLog.DefaultAppendMessageCallback#doAppend中计算wroteOffset fileFromOffset byteBuffer.position()写入消息总长度、消息体、topic、属性等字段并对事务消息做特殊处理PREPARED/ROLLBACK 消息不进 ConsumeQueuequeueOffset置 0。MappedFile 的提交writeBuffer 与 FileChannel 之间提交commit解决的是临时存储池数据 → 文件通道页缓存的搬运问题。当transientStorePoolEnabletrue时消息先落在堆外writeBuffer此时消费者如果直接读mappedByteBuffer是读不到数据的必须等 commit 把数据写入 FileChannel 后才可见因此提交是开启临时存储池时保证读写一致性的关键环节。commit 主方法public int commit(final int commitLeastPages) { if (writeBuffer null) { //no need to commit data to file channel, so just regard wrotePosition as committedPosition. return this.wrotePosition.get(); } if (this.isAbleToCommit(commitLeastPages)) { if (this.hold()) { commit0(); this.release(); } else { log.warn(in commit, hold failed, commit offset this.committedPosition.get()); } } // All dirty data has been committed to FileChannel. if (writeBuffer ! null this.transientStorePool ! null this.fileSize this.committedPosition.get()) { this.transientStorePool.returnBuffer(writeBuffer); this.writeBuffer null; } return this.committedPosition.get(); }分支一writeBuffer null未开启临时存储池时数据直接写在mappedByteBuffer页缓存上不存在写缓冲区到文件通道的中间层因此无需 commit直接以wrotePosition作为已提交位置返回。注释中的no need to commit data to file channel正是此意。分支二满足提交条件调用isAbleToCommit(commitLeastPages)判断是否值得提交值得提交时先hold()持有资源引用计数 1防止提交过程中文件被销毁再执行commit0()最后release()释放。分支三文件已写满的收尾当writeBuffer ! null fileSize committedPosition.get()说明整个文件的数据都已搬运到 FileChannel此时把writeBuffer归还给transientStorePool复用并将writeBuffer置空此后该文件退化为直接映射模式。isAbleToCommit如何判断该提交了提交阈值由参数commitLeastPages控制CommitLog 场景由commitCommitLogLeastPages传入默认 4 页。判断逻辑分三层第一层文件已满直接提交public boolean isFull() { return this.fileSize this.wrotePosition.get(); }文件写满后不再有新数据一次性把残余脏数据全部提交出去避免留尾巴。第二层脏页数达到阈值if (commitLeastPages 0) { return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) commitLeastPages; }其中write为当前写指针wrotePositionflush为已提交指针committedPosition。二者分别除以OS_PAGE_SIZE4KB得到页号相减即待提交的脏页数量。只有当脏页数达到commitLeastPages默认 4 页 16KB时才执行提交这是典型的攒批提交策略——用延迟换取更少的系统调用与更高的吞吐。第三层只要存在脏数据就提交return write flush;当commitLeastPages 0时如文件回收等强制场景只要wrotePosition committedPosition就提交。commit0具体的搬运过程protected void commit0() { int writePos this.wrotePosition.get(); int lastCommittedPosition this.committedPosition.get(); if (writePos - lastCommittedPosition 0) { try { ByteBuffer byteBuffer writeBuffer.slice(); byteBuffer.position(lastCommittedPosition); byteBuffer.limit(writePos); this.fileChannel.position(lastCommittedPosition); this.fileChannel.write(byteBuffer); this.committedPosition.set(writePos); } catch (Throwable e) { log.error(Error occurred when commit data to FileChannel., e); } } }过程与文档中描述一致先创建writeBuffer的共享子缓冲区slice()把 position 设为上次提交位置committedPosition、limit 设为当前写指针wrotePosition然后fileChannel.position(lastCommittedPosition)定位通道写位置fileChannel.write(byteBuffer)将[committedPosition, wrotePosition)区间一次性写入文件通道最后把committedPosition原子更新为writePos。注意slice()与原缓冲区共享底层数据但拥有独立的 position/limit因此这里只是视图操作不会破坏writeBuffer本身的读写位置。fileChannel.write之后数据进入 OS 页缓存但尚未落盘落盘由下一节的 flush 负责。MappedFile 的刷盘把数据真正落到磁盘刷盘flush解决的是页缓存 → 磁盘的持久化问题直接决定消息是否会因宕机而丢失。它由 FlushRealTimeService 线程周期调用CommitLog 场景默认flushIntervalCommitLog为 500ms 触发一次传入flushLeastPagesCommitLog 场景默认flushCommitLogLeastPages为 4。是否值得刷盘刷盘判断与提交判断结构完全对称文件已满isFull()返回 true 则刷盘flushLeastPages 0((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) flushLeastPages即写数据指针与上次刷盘指针的页号差达到阈值才刷flushLeastPages 0write flush只要存在未刷盘数据就刷。if (flushLeastPages 0) { return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) flushLeastPages; } return write flush;获取最大可读指针刷盘前需要确定刷到哪里这依赖getReadPosition()public int getReadPosition() { return this.writeBuffer null ? this.wrotePosition.get() : this.committedPosition.get(); }未开启临时存储池可读位置就是wrotePosition数据已直接写入页缓存开启临时存储池可读位置是committedPosition只有提交到 FileChannel 的数据才允许被读到、被刷出。这个读位置同时被消费端读取selectMappedBuffer与刷盘线程使用保证了未提交的数据不可见、不落盘。刷盘动作刷盘的实际执行逻辑文档中的核心结论为如果writeBuffer不为空或者文件通道的 position 不等于 0即文件通道上已有数据写过通过fileChannel.force(false)将文件通道中的数据强制刷出到磁盘否则未开启临时存储池且通道未写过直接调用mappedByteBuffer.force()将内存映射缓冲区的内容强制刷出到磁盘。两种方式最终都依赖内核的fsync类语义保证数据持久化。开启临时存储池时数据是writeBuffer → FileChannelcommit→ 磁盘flush三段式未开启时则是mappedByteBuffer → 磁盘flush两段式。commit 与 flush 的职责边界维度commit提交flush刷盘搬运方向writeBuffer → FileChannel页缓存页缓存 → 磁盘只读指针影响getReadPosition()影响getFlushedPosition()触发条件commitCommitLogLeastPages默认 4 页flushCommitLogLeastPages默认 4 页周期CommitLog 默认commitIntervalCommitLog为 200msCommitLog 默认flushIntervalCommitLog为 500ms开启前提仅transientStorePoolEnabletrue时才有意义始终需要在同步刷盘flushDiskTypeSYNC_FLUSH模式下消息返回成功前必须完成 flush异步刷盘ASYNC_FLUSH模式下则允许先返回、后落盘配合主从同步复制可兼顾性能与可靠性的权衡。MappedFile 的引用计数与销毁文件不会无限增长RocketMQ 会按过期策略删除旧 CommitLog 文件同时在 Broker 异常恢复如 RocketMQ CommitLog 详解 中的recoverNormally截断多余文件时也会销毁 MappedFile。销毁是先关引用、再关通道、最后删文件的严谨过程public boolean destroy(final long intervalForcibly) { this.shutdown(intervalForcibly); if (this.isCleanupOver()) { try { this.fileChannel.close(); log.info(close file channel this.fileName OK); long beginTime System.currentTimeMillis(); boolean result this.file.delete(); log.info(delete file[REF: this.getRefCount() ] this.fileName (result ? OK, : Failed, ) W: this.getWrotePosition() M: this.getFlushedPosition() , UtilAll.computeElapsedTimeMilliseconds(beginTime)); } catch (Exception e) { log.warn(close file channel this.fileName Failed. , e); } return true; } else { log.warn(destroy mapped file[REF: this.getRefCount() ] this.fileName Failed. cleanupOver: this.cleanupOver); } return false; }第一步shutdown 关闭 MappedFilepublic void shutdown(final long intervalForcibly) { if (this.available) { this.available false; this.firstShutdownTimestamp System.currentTimeMillis(); this.release(); } else if (this.getRefCount() 0) { if ((System.currentTimeMillis() - this.firstShutdownTimestamp) intervalForcibly) { this.refCount.set(-1000 - this.getRefCount()); this.release(); } } }shutdown 采用优雅关闭 强制兜底的双阶段策略第一次调用available为 true置为 false对外不再可用记录firstShutdownTimestamp为当前时间调用release()。release()只有在引用计数小于 1 时才真正执行资源清理cleanupOvertrue。后续调用且仍有引用如果refCount 0说明仍有线程在读写该文件如正在 append、selectMappedBuffer、commit 中的hold()持有此时比较当前时间与 firstShutdownTimestamp 的差值是否超过最大拒绝存活期intervalForcibly。一旦超期将引用计数强制设置为-1000 - refCount负数大值再调用release()使引用计数立刻跌破 0 从而强制执行清理。这是优雅等待优先、超期强制回收的经典模式既给在途读写留了缓冲时间又防止文件迟迟无法回收。第二步isCleanupOver 判断是否清理完成public boolean isCleanupOver() { return this.refCount.get() 0 this.cleanupOver; }清理完成的标准有两个引用计数refCount 0且清理标记cleanupOver为 true即release()已把资源释放逻辑执行完毕。只有两者同时满足才允许继续关闭通道、删除文件。第三步关闭通道并删除文件this.fileChannel.close()关闭文件通道随后this.file.delete()删除物理文件。日志中会打印引用计数、写位置W、刷盘位置M以及删除耗时便于运维排查。如果isCleanupOver()为 false例如强制关闭后仍在等待destroy 返回 false由上层如MappedFileQueue.deleteExpiredFileByTime等回收线程在下一轮再次尝试。引用计数在并发路径中的配合在提交与读取两节中我们已经看到hold()/release()的配合commit0前先hold()防止提交期间文件被销毁selectMappedBuffer读取前同样先hold()见 RocketMQ CommitLog 详解。销毁流程的shutdown正是通过同一套引用计数机制感知还有多少人在用我从而决定是立即清理还是等待超时强制清理。MappedFileQueueMappedFile 的管理容器单个 MappedFile 只能承载一段连续区间大量 MappedFile 的有序组织则由MappedFileQueue完成内部持有CopyOnWriteArrayListMappedFile mappedFiles与统一的mappedFileSize。几个与 MappedFile 强相关的方法获取最后一个文件写入入口来自 RocketMQ 消息发送存储流程public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) { long createOffset -1; MappedFile mappedFileLast getLastMappedFile(); if (mappedFileLast null) { createOffset startOffset - (startOffset % this.mappedFileSize); } if (mappedFileLast ! null mappedFileLast.isFull()) { createOffset mappedFileLast.getFileFromOffset() this.mappedFileSize; } if (createOffset ! -1 needCreate) { return tryCreateMappedFile(createOffset); } return mappedFileLast; }队列为空时新文件的起始偏移量为startOffset - startOffset % mappedFileSize向下对齐到文件大小整数倍最后一个文件已满isFull()即fileSize wrotePosition时新文件偏移量为lastFile.getFileFromOffset() mappedFileSize正好衔接。根据物理偏移量定位文件findMappedFileByOffset先取第一个与最后一个 MappedFile 做区间校验若偏移量在[firstOffset, lastOffset mappedFileSize)内则利用偏移量对文件大小取整快速计算索引index offset / mappedFileSize - firstOffset / mappedFileSize命中后还要校验该文件区间是否真正覆盖目标偏移量不满足则退化为遍历查找。这正是 CommitLog 随机读消息时按偏移量秒定位文件的实现基础。按时间定位文件ConsumeQueue 的getMappedFileByTime遍历所有 MappedFile利用getLastModifiedTimestamp()即每次追加更新的storeTimestamp找到第一个最后修改时间不小于目标时间戳的文件用于按消息存储时间回溯消费位置。与存储配置项的联动MappedFile的行为受 Broker 端MessageStoreConfig一系列配置影响常用配置归纳如下以 RocketMQ 4.9.3 默认值为准配置项默认值作用mappedFileSizeCommitLog1024 * 1024 * 10241GCommitLog 单个文件大小同时决定文件名起始偏移量的跨度mappedFileSizeConsumeQueue300000 * CQ_STORE_UNIT_SIZE约 5.4MBConsumeQueue 单个文件可容纳 30 万个条目transientStorePoolEnablefalse是否启用临时存储池堆外 writeBuffer commit 机制commitCommitLogLeastPages4CommitLog 提交的最小脏页数flushCommitLogLeastPages4CommitLog 刷盘的最小脏页数commitIntervalCommitLog200msCommitLog 提交周期flushIntervalCommitLog500msCommitLog 刷盘周期flushDiskTypeASYNC_FLUSH异步/同步刷盘SYNC_FLUSH时返回前必须落盘osPageCacheBusyTimeOutMills1000ms页缓存繁忙判定阈值影响写入是否返回OS_PAGECACHE_BUSY其中OS_PAGE_SIZE 1024 * 44KB是MappedFile类中声明的常量提交与刷盘的脏页数计算都以它为粒度mappedFileSizeConsumeQueue的条目单位CQ_STORE_UNIT_SIZE 20字节8 字节 CommitLog 偏移量 4 字节消息长度 8 字节 tag 哈希码见 RocketMQ ConsumeQueue 详解。总结MappedFile是 RocketMQ 存储引擎的最小却最关键的单位本文可以浓缩为一条主线初始化文件按起始偏移量命名RandomAccessFileFileChannel.map建立内存映射写指针从 0 开始写入wrotePosition原子推进消息顺序追加到mappedByteBuffer或writeBuffer顺序写换来高吞吐提交仅临时存储池模式下有意义把[committedPosition, wrotePosition)批量搬入 FileChannelcommittedPosition前移刷盘把页缓存强制落盘flushedPosition前移持久化程度取决于flushDiskType与刷盘周期销毁通过available、refCount、cleanupOver、intervalForcibly的层层配合安全地关通道、删文件。理解MappedFile之后再读 RocketMQ CommitLog 详解文件定位与恢复截断、RocketMQ 消息发送存储流程消息如何找到 MappedFile 并追加、RocketMQ ConsumeQueue 详解 与 RocketMQ IndexFile 详解二级索引如何建立在映射缓冲区之上就能把 RocketMQ 存储的读、写、刷、删完整串起来——这也是面试中高频的RocketMQ 为什么快消息什么时候真正落盘临时存储池的作用等问题的答案所在。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门