本文基于rocketmq4.0版本,结合CommitlLog的刷盘过程,对消息队列的刷盘过程源码进行分析,进而对RocketMQ的刷盘原理和过程进行了解。
 
rocketmq 4.0版本中刷盘类型和以前的版本一样有两种:
public enum FlushDiskType {
// 同步刷盘
SYNC_FLUSH,
// 异步刷盘
ASYNC_FLUSH
}
 
刷盘方式有三种:
线程服务 场景 写消息性能
CommitRealTimeService 异步刷盘 && 开启内存字节缓冲区 第一
FlushRealTimeService 异步刷盘  第二
GroupCommitService 同步刷盘 第三

其中CommitRealTimeService是老一些版本中没有的,它为开启内存字节缓存的刷盘服务。

介绍各个线程工作之前,先需要重点了解一下waitForRunning方法,因为在三个刷盘服务线程中都频繁使用该方法:

protected void waitForRunning(long interval) {
if (hasNotified.compareAndSet(true, false)) {
this.onWaitEnd();
return;
}
//entry to wait
waitPoint.reset(); try {
waitPoint.await(interval, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("Interrupted", e);
} finally {
hasNotified.set(false);
this.onWaitEnd();
}
}
这里要注意一下 waitPoint 这个共享变量,它是CountDownLatch2类型,,没有细看CountDownLatch2的原理,猜测它和CountDownLatch类似,根据CountDownLatch的使用原理,大致可以猜测waitPoint的作用。
 
回顾一下CountDownLatch相关知识:
CountDownLatch能够使一个线程等待其他线程完成各自的工作后再执行自己的任务,CountDownLatch是通过一个计数器来实现的,计数器的初始值为需要等待的线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
 
因此,可以猜测waitForRunning的业务逻辑大致为:
(1). 通过闭锁没执行依次waitPoint.countDown(),当计数器值到达0时,结束阻塞状态,开始执行等待线程的任务;
(2). 等待一定时间之后,结束阻塞状态,开始执行等待线程的任务。
 
在rocketmq4.0版本中,调用了waitPoint.countDown()的地方有三处:
shutdown()
stop()
wakeup()

这里我们关心的是wakeup()方法,调用wakeup方法的几处如下

其中与commitLog刷盘相关的有:

service.wakeup()、flushCommitLogService.wakeup()、commitLogService.wakeup(),其中service.wakeup()的service是GroupCommitService类型。
 
由此引入了本文所要讲述的FlushRealTimeService、CommitRealTimeService以及GroupCommitService三个线程刷盘服务。
 
 
GroupCommitService
broker启动后,会启动许多服务线程,包括刷盘服务线程,如果刷盘服务线程类型是SYNC_FLUSH (同步刷盘类型:对写入的数据同步刷盘,只在broker==master时使用),则开启GroupCommitService服务,该服务线程启动后每隔10毫秒或该线程调用了wakeup()方法后停止阻塞,执行doCommit()方法。doCommit里面执行具体的刷盘逻辑业务。GroupCommitService服务线程体如下:

public void run() {
CommitLog.log.info(this.getServiceName() + " service started"); while (!this.isStopped()) {
try {
this.waitForRunning(10);
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
} // Under normal circumstances shutdown, wait for the arrival of the
// request, and then flush
try {
Thread.sleep(10);
} catch (InterruptedException e) {
CommitLog.log.warn("GroupCommitService Exception, ", e);
} synchronized (this) {
this.swapRequests();
} this.doCommit(); CommitLog.log.info(this.getServiceName() + " service end");
}
 
当broker类型为master时,每写入一条消息成功写入mapedFile文件后,调用handleDiskFlush方法,如果该消息满足messageExt.isWaitStoreMsgOK(),则将这一条成功写入的消息生成GroupCommitRequest对象,将该对像放入GroupCommitService的requestsWrite列表中(List<GroupCommitRequest>),等待刷盘线程调用doCommit,对列表中的消息进行刷盘,doCommit中每对一个request处理完成后,会调用wakeupCustomer。等待时间5s后或者request的countDownLatch记数为0时,则将这条消息是否已经刷盘成功进行汇报,如果没有刷盘成功,则再日志中记录错误,并将putMessageResult设置为FLUSH_DISK_TIMEOUT。代码如下:

if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
if (messageExt.isWaitStoreMsgOK()) {
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
service.putRequest(request);
boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
if (!flushOK) {
log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
+ " client address: " + messageExt.getBornHostString());
putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
}
} else {
service.wakeup();
}
}

这条消息是否已经刷盘成功进行汇报的逻辑 -- waitForFlush方法:

public static class GroupCommitRequest {
private final long nextOffset;
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private volatile boolean flushOK = false; public GroupCommitRequest(long nextOffset) {
this.nextOffset = nextOffset;
} public long getNextOffset() {
return nextOffset;
} public void wakeupCustomer(final boolean flushOK) {
this.flushOK = flushOK;
this.countDownLatch.countDown();
} public boolean waitForFlush(long timeout) {
try {
// 阻塞当前工作线程,等待时间5s后或者countDownLatch记数为0时,停止阻塞,执行下一条语句
this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS);
return this.flushOK;
} catch (InterruptedException e) {
log.error("Interrupted", e);
return false;
}
}
}
对GroupCommitRequest类中的两个方法的说明:
在waitForFlush方法阻塞的时候,doCommit方法对写入requestsWrite列表中(List<GroupCommitRequest>)所有GroupCommitRequest对象依次进行了 wakeupCustomer方法调用,wakeupCustomer调用后,countDownLatch 闭锁记数减一,等待时间5s后或者countDownLatch记数为0时,返回调用wakeupCustomer的GroupCommitRequest对应的消息的刷盘结果。
 
GroupCommitService的doCommit方法
说明一下:分析doCommit方法之前,先提及一下swapRequests这个方法,之前提过,GroupCommitService服务线程该每隔10毫秒或调用了该线程的wakeup()方法后执行doCommit()方法,具体地要涉及到waitForRunning方法,waitForRunning方法中onWaitEnd的作用在这里就可以提及一下了,它的作用就是将requestsWrite 转换为requestsRead ,这个与消息存储过程中处理dispatchRequest是类似的。
 private void swapRequests() {
List<GroupCommitRequest> tmp = this.requestsWrite;
this.requestsWrite = this.requestsRead;
this.requestsRead = tmp;
}
 doCommit代码:
private void doCommit() {
//
synchronized (this.requestsRead) {
if (!this.requestsRead.isEmpty()) {
for (GroupCommitRequest req : this.requestsRead) {
// There may be a message in the next file, so a maximum of
// two times the flush
boolean flushOK = false;
for (int i = 0; i < 2 && !flushOK; i++) {
flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset(); if (!flushOK) {
CommitLog.this.mappedFileQueue.flush(0);
}
} req.wakeupCustomer(flushOK);
} long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) {
CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
} this.requestsRead.clear();
} else {
// Because of individual messages is set to not sync flush, it
// will come to this process
CommitLog.this.mappedFileQueue.flush(0);
}
}
}
 
其中,具体的刷盘核心代码:CommitLog.this.mappedFileQueue.flush(0); 接下来看其他两个刷盘服务线程,对CommitLog.this.mappedFileQueue.flush(0)下文将具体讲解。
 
 

FlushRealTimeService
// 刷新策略(默认是实时刷盘)
flushCommitLogTimed
// 刷盘时间间隔(默认0.5s)
interval = flushIntervalCommitLog
// 每次刷盘至少需要多少个page(默认是4个)
flushPhysicQueueLeastPages
// 彻底刷盘间隔时间(默认10s)
flushPhysicQueueThoroughInterval
 
大致逻辑:
-- 如果 当前时间 >(最后一次刷盘时间 + 彻底刷盘间隔时间(10s)),则将最新一次刷盘时间更新为当前时间
 
-- 如果是实时刷盘,每隔一定时间间隔,该线程休眠500毫秒
如果不是实时刷盘,则调用waitForRunning,即每隔500毫秒或该刷盘服务线程调用了wakeup()方法之后结束阻塞。
 
-- 调用 CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
 
 
CommitRealTimeService
CommitRealTimeService 比较特殊,它会包含提交和异步刷盘逻辑,专门为开启内存字节缓冲区的刷盘服务。
// 提交到FileChannel的时间间隔,只在TransientStorePool 打开的情况下使用,默认0.2s
interva l= commitIntervalCommitLog
//每次提交到File至少需要多少个page(默认是4个)
commitDataLeastPages = commitCommitLogLeastPages
/ 提交完成间隔时间(默认0.2s)
commitDataThoroughInterval
 
大致逻辑:
如果 当前时间 >(最后一次提交时间 + 提交完成间隔时间),更新lastCommitTimestamp之后,执行提交的核心逻辑:
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
如果result == false 则意味着有新的数据 committed,此时需要wakeup刷盘线程,即:
flushCommitLogService.wakeup(); 进行异步刷盘处理。
 
可知道,刷盘的下一层核心逻辑:
mappedFileQueue.flush
mappedFileQueue.commit
 
  flush
 public boolean flush(final int flushLeastPages) {
boolean result = true;
MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, false);
if (mappedFile != null) {
long tmpTimeStamp = mappedFile.getStoreTimestamp();
int offset = mappedFile.flush(flushLeastPages);
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.flushedWhere;
this.flushedWhere = where;
if (0 == flushLeastPages) {
this.storeTimestamp = tmpTimeStamp;
}
} return result;
}

  commit

public boolean commit(final int commitLeastPages) {
boolean result = true;
MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, false);
if (mappedFile != null) {
int offset = mappedFile.commit(commitLeastPages);
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.committedWhere;
this.committedWhere = where;
} return result;
}
 
从上代码可以看出,刷盘过程与MappedFile有很大关系,通过findMappedFileByOffset方法找到要刷盘的MappedFile,然后MappedFile中采用数据刷盘技术将数据刷入到磁盘

MappedFile的刷盘方式有两种:
1. 写入内存字节缓冲区(writeBuffer) ----> 从内存字节缓冲区(write buffer)提交(commit)到文件通道(fileChannel) ----> 文件通道(fileChannel)flush到磁盘
2.写入映射文件字节缓冲区(mappedByteBuffer) ----> 映射文件字节缓冲区(mappedByteBuffer)flush
 
(MappedFile的刷盘方式待具体分析,待补充...)
 
 

rocketmq刷盘过程的更多相关文章

  1. RocketMQ消息丢失解决方案:同步刷盘+手动提交

    前言 之前我们一起了解了使用RocketMQ事务消息解决生产者发送消息时消息丢失的问题,但使用了事务消息后消息就一定不会丢失了吗,肯定是不能保证的. 因为虽然我们解决了生产者发送消息时候的消息丢失问题 ...

  2. RocketMQ中Broker的刷盘源码分析

    上一篇博客的最后简单提了下CommitLog的刷盘  [RocketMQ中Broker的消息存储源码分析] (这篇博客和上一篇有很大的联系) Broker的CommitLog刷盘会启动一个线程,不停地 ...

  3. 【RocketMQ】消息的刷盘机制

    刷盘策略 CommitLog的asyncPutMessage方法中可以看到在写入消息之后,调用了submitFlushRequest方法执行刷盘策略: public class CommitLog { ...

  4. HTC A510C电信手机刷机过程

    HTC A510C电信手机刷机过程记录 Writed by Peter Hu(2014.6.7) ON WIN7_64 刷机需要的步骤: 1)  将S-ON加密保护式去掉,改成S-OFF模式,这样才能 ...

  5. mq刷盘方式

    Broker 在收到Producer发送过来的消息后,会存入CommitLog对应的内存映射区中,见CommitLog类的putMessage方法.该方法执行OK后,会判断存储配置中刷盘模式:同步or ...

  6. Rocket重试机制,消息模式,刷盘方式

    一.Consumer 批量消费(推模式) 可以通过 consumer.setConsumeMessageBatchMaxSize(10);//每次拉取10条 这里需要分为2种情况 Consumer端先 ...

  7. MySQL InnoDB 日志管理机制中的MTR和日志刷盘

    1.MTR(mini-transaction) 在MySQL的 InnoDB日志管理机制中,有一个很重要的概念就是MTR.MTR是InnoDB存储擎中一个很重要的用来保证物理写的完整性和持久性的机制. ...

  8. 【软件安装与环境配置】TX2刷机过程

    前言 使用TX2板子之前需要进行刷机,一般都是按照官网教程的步骤刷机,无奈买不起的宝宝只有TX2核心板,其他外设自己搭建,所以只能重新制作镜像,使用该镜像进行刷机. 系统需求 1.Host Platf ...

  9. 从CM刷机过程和原理分析Android系统结构

    前面101篇文章都是分析Android系统源代码,似乎不够接地气. 假设能让Android系统源代码在真实设备上跑跑看效果,那该多好.这不就是传说中的刷ROM吗?刷ROM这个话题是老罗曾经一直避免谈的 ...

随机推荐

  1. js 正则表达式符号含义

    \ 做为转意,即通常在"\"后面的字符不按原来意义解释,如/b/匹配字符"b",当b前面加了反斜杆后/\b/,转意为匹配一个单词的边界. -或- 对正则表达式功 ...

  2. vue数据已渲染成 但还是报错 变量 undefined

    问题:页面上的数据已渲染出来,但是控制台还是报错变量未undefined,主要是当页面加载完成后,数据并未加载完,所以会报次错误. 解决办法:在数据渲染的主节点(最外层的div)添加 v-if=“da ...

  3. cocos2d

    http://www.jetbrains.com/webstorm/download/index.html 运行又有下面错误 Fatal signal 11 (SIGSEGV) at 0x000000 ...

  4. golang回调函数的例子

    package main import "fmt" type TestStruct struct { } func (object *TestStruct) test(msg st ...

  5. 浅谈浅克隆(shallow clone)和 深克隆(deep clone)

    区别就在于是否对对象中的引用变量所指向的对象进行拷贝. 1.浅克隆/浅复制/浅拷贝 浅拷贝是指在拷贝对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量只是对引用进行拷贝,没有对引用指向 ...

  6. appium在android7.0上无法启动问题

    前言 由于最近很多android手机升级到7.0系统了,有些小伙伴的appium版本用的还是1.4版本,在运行android7.0的app自动化时候遇到无法启动问题:WebDriverExceptio ...

  7. Solr入门和实践以及我对Solr的8点理解

    友情提示Solr的内容还是比较多的,一篇文章只能讲解一部分.全面介绍,没兴趣,没时间,也没能力,回报还不大.本文只写点我认为比较重要的知识点,独特的个人想法.仅供参考哦,更多细节需要自己去琢磨. 概述 ...

  8. HDU 2669 Romantic(裸的拓展欧几里得)

    Romantic Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  9. B. T-primes

    /* PROBLEMSSUBMITSTATUSSTANDINGSCUSTOM TEST B. T-primes time limit per test2 seconds memory limit pe ...

  10. 千万别在UI线程上调用Control.Invoke和Control.BeginInvoke,因为这些是依然阻塞UI线程的,造成界面的假死

    原文地址:https://www.cnblogs.com/wangchuang/archive/2013/02/20/2918858.html .c# Invoke和BeginInvoke 区别 Co ...