大纲

1.Producer作为生产者是如何创建出来的

2.Producer启动时是如何准备好相关资源的

3.Producer是如何从拉取Topic元数据的

4.Producer是如何选择MessageQueue的

5.Producer与Broker是如何进行网络通信的

6.Broker收到一条消息后是如何存储的

7.Broker是如何实时更新索引文件的

8.Broker是如何实现同步刷盘以及异步刷盘的

9.Broker是如何清理存储较久的磁盘数据的

10.Consumer作为消费者是如何创建和启动的

11.消费者组的多个Consumer会如何分配消息

12.Consumer会如何从Broker拉取一批消息

1.Producer作为生产者是如何创建出来的

(1)NameServer的启动

(2)Broker的启动

(3)Broker的注册和心跳

(4)通过Producer发送消息

(1)NameServer的启动

NameServer启动后的核心架构,如下图示:

NameServer启动后,会有一个NamesrvController组件管理控制NameServer的所有行为,包括内部会启动一个Netty服务器去监听一个9876端口号,然后接收处理Broker和客户端发送过来的请求。

(2)Broker的启动

Broker启动后的核心架构,如下图示:

Broker启动后,也会有一个BrokerController组件管理控制Broker的整体行为,包括初始化Netty服务器用于接收客户端的网络请求、启动处理请求的线程池、执行定时任务的线程池、初始化核心功能组件,同时还会发送注册请求到NameServer去注册自己。

(3)Broker的注册和心跳

Broker启动后,会向NameServer进行注册和定时发送注册请求作为心跳。NameServer会有一个后台进程定时检查每个Broker的最近一次心跳时间,如果长时间没心跳就认为Broker已经故障。如下图示:

(4)通过Producer发送消息

假设RocketMQ集群已经启动好了NameServer,而且还启动了一批Broker,同时Broker都已经把自己注册到NameServer里去了,NameServer也会定时检查这批Broker是否存活。那么就可以让开发好的业务系统去发送消息到RocketMQ集群里,于是需要创建一个Producer实例。

实际上我们开发好的系统,最终都需要创建一个Producer实例,然后通过Producer实例发送消息到RocketMQ的Broker上去。

下面是使用Producer实例发送消息到RocketMQ的代码,可以看到Producer是如何构造出来的。

DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();

构造Producer的过程很简单:也就是创建一个DefaultMQProducer对象实例。在构造方法中,首先会传入所属的Producer分组,然后设置一下NameServer的地址,最后调用它的start()方法启动这个Producer即可。

创建DefaultMQProducer对象实例是一个非常简单的过程:就是创建出一个对象,然后保存它的Producer分组。设置NameServer地址也是一个很简单的过程,就是保存一下NameServer地址。

所以,最关键的还是调用DefaultMQProducer的start()方法去启动Producer这个消息生产者。

2.Producer启动时是如何准备好相关资源的

(1)DefaultMQProducer的start()方法

(2)Producer在第一次向Topic发送消息时才拉取Topic的路由数据

(3)Producer在第一次向Broker发送消息时才与Broker建立网络连接

(1)DefaultMQProducer的start()方法

接下来分析Producer在启动时是如何准备好相关资源的。Producer内部必须要有独立的线程资源,以及需要和Broker已经建立好网络连接,这样才能把消息发送出去。

在构造Producer时,它内部便会构造一个真正用于执行消息发送逻辑的DefaultMQProducerImpl组件。所以,真正的Producer生产者其实是这个DefaultMQProducerImpl组件。那么这个组件在启动的时都干了什么呢?

public class DefaultMQProducer extends ClientConfig implements MQProducer {
protected final transient DefaultMQProducerImpl defaultMQProducerImpl;
...
@Override
public void start() throws MQClientException {
this.setProducerGroup(withNamespace(this.producerGroup));
this.defaultMQProducerImpl.start();
if (null != traceDispatcher) {
try {
traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
} catch (MQClientException e) {
log.warn("trace dispatcher start failed ", e);
}
}
}
...
} public class DefaultMQProducerImpl implements MQProducerInner {
...
public void start() throws MQClientException {
this.start(true);
} public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
this.checkConfig();
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook); boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup() + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), null);
}
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
if (startFactory) {
mQClientFactory.start();
} log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(), this.defaultMQProducer.isSendMessageWithVIPChannel());
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("The producer service state not OK, maybe started once, " + this.serviceState + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), null);
default:
break;
}
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.startScheduledTask();
}
...
}

其实,上述start()方法的具体逻辑暂时不需要深入分析,因为其中的逻辑并没有直接与Producer发送消息相关联。比如拉取Topic的路由数据、选择MessageQueue、跟Broker建立长连接、发送消息到Broker等这些核心逻辑,其实都封装在发送消息的方法中。

(2)Producer在第一次向Topic发送消息时才拉取Topic的路由数据

假设后续Producer要发送消息,那么就要指定往哪个Topic发送消息。因此Producer需要知道Topic的路由数据,比如Topic有哪些MessageQueue,每个MessageQueue在哪些Broker上。如下图示:

从start()方法源码可知,在Producer启动时,并不会去拉取Topic的路由数据。实际上,Producer在第一次向Topic发送消息时,才会去拉取Topic的路由数据。包括这个Topic有几个MessageQueue、每个MessageQueue在哪个Broker上。然后从中选择一个MessageQueue,接着与对应的Broker建立网络连接,最后才把消息发送过去。

(3)Producer在第一次向Broker发送消息时才与Broker建立网络连接

从start()方法源码可知,在Producer启动时,并不会和所有Broker建立网络连接。很多核心的逻辑,包括拉取Topic路由数据、选择MessageQueue、和Broker建立网络连接等,都是在Producer第一次发送消息时才进行处理的。

3.Producer是如何从拉取Topic元数据的

(1)Producer发送消息的方法

(2)Producer拉取Topic路由数据的过程

(1)Producer发送消息的方法

当调用Producer的send()方法发送消息时,最终会调用到DefaultMQProducerImpl的sendDefaultImpl()方法。

在sendDefaultImpl()方法里,开始会有一行非常关键的代码,如下所示:

public class DefaultMQProducerImpl implements MQProducerInner {
...
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
...
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
...
}
...
}

该行代码的意思是,每次Producer发送消息时,都会先检查一下要发送消息的那个Topic的路由数据是否在本地。如果不在,才会发送请求到NameServer去拉取Topic的路由数据,然后缓存在本地。

(2)Producer拉取Topic路由数据的过程

进入tryToFindTopicPublishInfo()方法,会发现其逻辑非常简单:就是会先检查一下自己本地是否有这个Topic的路由数据的缓存,如果没有就发送网络请求到NameServer去拉取,如果有就直接返回本地Topic路由数据缓存,如下图示:

那么Producer是如何发送网络请求到NameServer去拉取Topic路由数据的呢?这其实就对应了tryToFindTopicPublishInfo()方法内的一行代码,如下所示:

public class DefaultMQProducerImpl implements MQProducerInner {
...
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
} if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
...
}

通过以下这行代码,Producer就可以从NameServer拉取某个Topic的路由数据,然后更新到自己本地的缓存里去。

this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);

Producer发送请求到NameServer的拉取Topic路由数据的过程:首先封装一个Request请求对象,然后通过Netty客户端发送请求到NameServer,接着会接收到NameServer返回的一个Response响应对象,于是就可以从Response响应对象里取出所需的Topic路由数据并更新到自己本地缓存里。更新时会做一些判断,比如Topic路由数据是否有改变过等,然后把Topic路由数据放入本地缓存。如下图示:

4.Producer是如何选择MessageQueue的

(1)Topic是由多个MessageQueue组成的

(2)选择MessageQueue的源码和算法

(1)Topic是由多个MessageQueue组成的

Producer发送消息时,会先检查一下要发送消息的Topic的路由数据是否在本地缓存。如果不在,就会通过底层的Netty网络通信模块发送一个请求到NameServer拉取Topic路由数据,然后缓存在Producer本地。当Producer拿到一个Topic的路由数据后,就应该选择要发送消息到这个Topic的哪一个MessageQueue上了。

因为Topic是一个逻辑上的概念,一个Topic的数据往往会分布式存储在多台Broker机器上,所以Topic本质是由多个MessageQueue组成的。

每个MessageQueue都可以在不同的Broker机器上,当然也可能一个Topic的多个MessageQueue在一个Broker机器上。如下图示:

只要Producer知道了要发送消息到哪个MessageQueue上去,其实就已经知道了这个MessageQueue在哪台Broker机器上,接着和该Broker机器建立连接,发送消息过去即可。

(2)选择MessageQueue的源码和算法

发送消息的源码在DefaultMQProducerImpl的sendDefaultImpl()方法中。该方法里只要Producer获取到Topic的路由数据,不管从本地缓存获取还是从NameServer拉取,就会执行下面的代码:

public class DefaultMQProducerImpl implements MQProducerInner {
...
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
...
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
...
}
...
}

selectOneMessageQueue()方法其实就是在选择Topic中的一个MessageQueue,然后发送消息到这个MessageQueue。下面是选择MessageQueue的算法:

public class MQFaultStrategy {
...
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
...
int index = tpInfo.getSendWhichQueue().incrementAndGet();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0) {
pos = 0;
}
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
return mq;
}
}
...
}
...
}

这是一种简单的负载均衡算法:首先获取一个自增长的Index,接着就用这个Index对Topic的MessageQueue列表进行取模运算,从而获取到一个MessageQueue列表的位置,最后返回这个位置的MessageQueue。

但是如果某个Broker故障了,那么就不能把消息发送到故障Broker的MessageQueue了。所以selectOneMessageQueue()方法里还有其他代码,用来实现Broker故障时的自动回避机制。

5.Producer与Broker是如何进行网络通信的

(1)Producer是如何把消息发送给Broker的

(2)Producer和Broker基于长连接进行通信

(1)Producer是如何把消息发送给Broker的

在DefaultMQProducerImpl.sendDefaultImpl()方法中,会先获取到MessageQueue所在的Broker名称,如下所示:

public class DefaultMQProducerImpl implements MQProducerInner {
...
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
...
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
...
}
...
}
...
}

获取到这个BrokerName后,就会调用sendKernelImpl()方法把消息发送到Broker上。

public class DefaultMQProducerImpl implements MQProducerInner {
...
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
...
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
...
} private SendResult sendKernelImpl(final Message msg, final MessageQueue mq, final CommunicationMode communicationMode, final SendCallback sendCallback, final TopicPublishInfo topicPublishInfo, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
long beginStartTime = System.currentTimeMillis();
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
tryToFindTopicPublishInfo(mq.getTopic());
brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}
...
}
...
}

在sendKernelImpl()方法中:

首先会通过BrokerName去本地缓存查找它的实际地址。如果找不到,就到NameServer中拉取Topic的路由数据,再次在本地缓存获取Broker的实际地址,有了这个地址才能进行网络通信。

然后会封装一个Request请求,包括请求头、发送的消息等,并且会给消息分配全局唯一ID,以及对超过4KB的消息体进行压缩。

在Request请求中,会包含生产者组、Topic名称、Topic的MessageQueue数量、MessageQueue的ID、消息发送时间、消息的flag、消息扩展属性、消息重试次数、是否批量发送等信息,如果是事务消息则带上prepared标记等。

把这些数据都封装到一个Request请求后,就会通过Netty把Request请求发送到指定的Broker上。

(2)Producer和Broker基于长连接进行通信

其中,Producer和Broker会通过Netty建立长连接,然后基于长连接进行持续通信。如下图示:

那么Broker上的Netty服务器接收到消息后,会如何进行处理?这个过程比较复杂,涉及到CommitLog、ConsumeQueue、IndexFile、Checkpoint等一系列机制,这也是RocketMQ中核心机制。

6.Broker收到一条消息后是如何存储的

(1)Broker收到消息后的处理流程

(2)Broker如何将消息写入CommitLog文件

(1)Broker收到消息后的处理流程

Broker中的Netty网络服务器获取到一条消息后:

首先,会把这条消息写入到一个CommitLog文件里。一个Broker机器上就只有一个CommitLog文件,所有Topic的消息都会写入到这个文件里。如下图示:

然后,Broker会以异步的方式把消息写入到一个ConsumeQueue文件里,因为一个Topic会有多个MessageQueue。任何一条消息都需要写入到一个MessageQueue的,一个MessageQueue其实就是对应了一个ConsumeQueue文件。所以一条写入MessageQueue的消息,必然会异步进入对应的ConsumeQueue文件,如下图示:

接着,Broker还会以异步的方式把消息写入到一个IndexFile文件里。在IndexFile文件里,会把每条消息的key和消息在CommitLog中的offset偏移量做一个索引,这样后续如果要根据消息key从CommitLog文件里查询消息,就可以根据IndexFile文件的索引来查询,如下图示:

(2)Broker如何将消息写入CommitLog文件

Broker收到一个消息后,首先会顺序写入CommitLog文件。CommitLog文件的存储目录是${ROCKETMQ_HOME}/store/commitlog,目录里会有很多CommitLog文件。每个文件默认是1GB大小,一个CommitLog文件写满了就创建一个新的CommitLog文件,文件名就是文件中的第一个偏移量。文件名如果不足20位,就用0来补齐。

00000000000000000000
000000000003052631924

Broker在把消息写入CommitLog文件时,会申请一个putMessageLock锁。也就是说,Broker写入消息到CommitLog文件时都是串行的,不会并发写入,因为并发写入文件必然会有数据错乱的问题。

下面是相关的源码片段:

public class CommitLog {
...
protected final PutMessageLock putMessageLock;
...
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
...
putMessageLock.lock();
...
result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext);
...
}
...
}

在asyncPutMessage()方法中,获取到锁之后,会对消息做出一系列处理,包括设置消息的存储时间、创建全局唯一的消息ID、计算消息的总长度等。然后会执行MappedFile的appendMessage()方法,把消息写入到MappedFile里。

public class MappedFile extends ReferenceResource {
...
public AppendMessageResult appendMessage(final MessageExtBrokerInner msg, final AppendMessageCallback cb, PutMessageContext putMessageContext) {
return appendMessagesInner(msg, cb, putMessageContext);
} public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb, PutMessageContext putMessageContext) {
...
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;
}
...
}

上述源码中,其实最关键的是cb.doAppend()这行代码。cb.doAppend()会把消息追加到MappedFile映射的一块内存里去,并没有直接刷入到磁盘上的CommitLog文件,如下图示。至于具体什么时候才会把内存里的数据刷入磁盘上的CommitLog文件,这就要看配置的刷盘策略了。

另外,不管是同步刷盘还是异步刷盘,如果配置了主从同步,一旦将消息写入到CommitLog文件之后,接下来都会进行主从同步复制。

7.Broker是如何实时更新索引文件的

(1)消息如何进入CommitLog

(2)消息如何进入ConsumeQueue和IndexFile

(1)消息如何进入CommitLog

Broker收到一条消息后,会先把消息写入到CommitLog里。但是刚开始写入也仅仅是写入到MappedFile映射的一块内存,后续才会根据刷盘策略决定是否立即把数据从内存刷入磁盘。如下图示:

(2)消息如何进入ConsumeQueue和IndexFile

Broker启动时会启动一个叫ReputMessageService的线程,这个线程会把写入CommitLog的消息转发出去,也就是将消息写入(转发)到ConsumeQueue和IndexFile。如下图示:

在DefaultMessageStore的start()方法里,会启动这个ReputMessageService线程。而DefaultMessageStore的start()方法是在Broker启动时被调用的,所以相当于Broker启动时就会启动这个ReputMessageService线程。

public class BrokerController {
...
public void start() throws Exception {
//启动消息存储组件
if (this.messageStore != null) {
this.messageStore.start();
}
...
}
...
} public class DefaultMessageStore implements MessageStore {
...
public void start() throws Exception {
...
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
this.reputMessageService.start();
...
}
...
}

下面是ReputMessageService线程的源码:

public class DefaultMessageStore implements MessageStore {
...
class ReputMessageService extends ServiceThread {
...
@Override
public void run() {
DefaultMessageStore.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
Thread.sleep(1);
this.doReput();
} catch (Exception e) {
DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
DefaultMessageStore.log.info(this.getServiceName() + " service end");
} private void doReput() {
...
DispatchRequest dispatchRequest = DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
...
DefaultMessageStore.this.doDispatch(dispatchRequest);
...
}
...
} public void doDispatch(DispatchRequest req) {
for (CommitLogDispatcher dispatcher : this.dispatcherList) {
dispatcher.dispatch(req);
}
}
...
}

由上述代码可知:在ReputMessageService线程里,每隔1毫秒就会把最近写入CommitLog的消息进行一次转发。其中会通过doReput()方法将消息转发到ConsumeQueue和IndexFile中。

在doReput()方法里,会从CommitLog中去获取到一个DispatchRequest,也就是从CommitLog中获取一份需要进行转发的消息。

接着,就会通过调用doDispatch()方法将消息转发到ConsumeQueue和IndexFile里,其中会通过遍历CommitLogDispatcher来实现。因为这个CommitLogDispatcher的实现类有两个,分别负责把消息转发到ConsumeQueue和IndexFile。

ConsumeQueueDispatcher的写入逻辑,就是找到当前Topic的messageQueueId对应的一个ConsumeQueue文件。一个MessageQueue会对应多个ConsumeQueue文件,只要找到一个即可,然后就可以把消息写入其中。

public class DefaultMessageStore implements MessageStore {
...
class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
@Override
public void dispatch(DispatchRequest request) {
final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
switch (tranType) {
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
DefaultMessageStore.this.putMessagePositionInfo(request);
break;
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
break;
}
}
} public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
cq.putMessagePositionInfoWrapper(dispatchRequest, checkMultiDispatchQueue(dispatchRequest));
}
...
}

IndexDispatcher的写入逻辑,就是在IndexFile里构建对应的索引。

public class DefaultMessageStore implements MessageStore {
...
class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
@Override
public void dispatch(DispatchRequest request) {
if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
DefaultMessageStore.this.indexService.buildIndex(request);
}
}
}
...
}

(3)总结

当Broker把消息写入CommitLog后,会有一个后台线程每隔1毫秒拉取CommitLog中最新的一批消息,然后分别转发到ConsumeQueue和IndexFile中。

8.Broker是如何实现同步刷盘以及异步刷盘的

(1)Broker收到消息后的存储流程

(2)消息的刷盘时机和策略

(3)Broker是如何处理刷盘的

(1)Broker收到消息后的存储流程

Broker首先会将消息写入CommitLog,并且是先写入MappedFile映射的一块内存,而不是先写入磁盘。然后会有一个后台线程把CommitLog里的消息写入到ConsumeQueue和IndexFile里,如下图示:

(2)消息的刷盘时机和策略

当需要写入CommitLog的数据进入到MappedFile映射的一块内存后,就会开始执行刷盘策略。如果是同步刷盘,那么就会直接把内存里的数据写入磁盘文件。如果是异步刷盘,那么就会过一段时间后再把数据刷入磁盘文件。

在往CommitLog写数据时,会调用CommitLog的asyncPutMessage()方法,在这个方法的末尾有两行很关键的代码。一个是调用submitFlushRequest()方法,用于决定如何进行刷盘。一个是调用submitReplicaRequest()方法,用于决定如何把消息同步给Slave Broker。如下所示:

public class CommitLog {
...
public CompletableFuture<PutMessageResult> asyncPutMessages(final MessageExtBatch messageExtBatch) {
...
//用于决定如何进行刷盘
CompletableFuture<PutMessageStatus> flushOKFuture = submitFlushRequest(result, messageExtBatch);
//用于决定如何把消息同步给Slave Broker
CompletableFuture<PutMessageStatus> replicaOKFuture = submitReplicaRequest(result, messageExtBatch);
return flushOKFuture.thenCombine(replicaOKFuture, (flushStatus, replicaStatus) -> {
if (flushStatus != PutMessageStatus.PUT_OK) {
putMessageResult.setPutMessageStatus(flushStatus);
}
if (replicaStatus != PutMessageStatus.PUT_OK) {
putMessageResult.setPutMessageStatus(replicaStatus);
}
return putMessageResult;
});
}
...
}

(3)Broker是如何处理刷盘的

接下来进入submitFlushRequest()方法看看Broker是如何处理刷盘的。

public class CommitLog {
...
public CompletableFuture<PutMessageStatus> submitFlushRequest(AppendMessageResult result, MessageExt messageExt) {
//Synchronization flush——同步刷盘
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(), this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
flushDiskWatcher.add(request);
service.putRequest(request);
return request.future();
} else {
service.wakeup();
return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
}
}
//Asynchronous flush——异步刷盘
else {
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
flushCommitLogService.wakeup();
} else {
commitLogService.wakeup();
}
return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
}
}
...
}

上述代码,就会根据配置的两种不同的刷盘策略,来分别进行处理的。

一.同步刷盘的策略是如何处理的

首先会构建一个GroupCommitRequest,然后提交给GroupCommitService去进行处理,接着调用request.future()等待同步刷盘成功。

具体的刷盘是由GroupCommitService执行的,它的doCommit()方法会执行同步刷盘的逻辑,代码如下:

public class CommitLog {
...
class GroupCommitService extends FlushCommitLogService {
...
private void doCommit() {
...
CommitLog.this.mappedFileQueue.flush(0);
...
}
...
}
...
}

上述代码一层一层调用下去,可发现最终刷盘其实是靠MappedByteBuffer的force()方法,如下所示:

public class MappedFile extends ReferenceResource {
...
public int flush(final int flushLeastPages) {
...
this.mappedByteBuffer.force();
...
}
...
}

这个MappedByteBuffer就是JDK NIO包下的API,MappedByteBuffer的force()方法会强迫将写入内存的数据刷入到磁盘文件里,执行完force()方法就代表同步刷盘成功了。

二.异步刷盘的策略是如何处理的

此时会唤醒一个flushCommitLogService组件。由于FlushCommitLogService是一个线程,它是一个抽象父类,它的子类是CommitRealTimeService。所以真正唤醒的是FlushCommitLogService的子类CommitRealTimeService线程。

在该线程里,会每隔一定时间执行一次刷盘,最大间隔是10s。所以一旦执行异步刷盘,那么最多10秒就会执行一次刷盘。

9.Broker是如何清理存储较久的磁盘数据的

(1)定时检查是否要删除磁盘上的文件

(2)触发删除文件的条件

(3)删除文件的具体操作

(1)定时检查是否要删除磁盘上的文件

默认情况下,Broker会启动一个后台线程,这个后台线程会自动检查CommitLog文件、ConsumeQueue文件,因为这些文件都会存在多个。如果发现比较旧的、超过72小时的文件,那么就会清理这些文件。

所以,默认情况下,Broker只会将消息保留3天,当然我们也可以通过fileReservedTime来自定义配置这个时间。

这个定时检查过期数据文件的线程,在DefaultMessageStore这个类里。在DefaultMessageStore的start()方法中,会调用addScheduleTask()方法每隔10s定时执行一个后台检查任务。如下所示:

public class DefaultMessageStore implements MessageStore {
...
public void start() throws Exception {
...
this.addScheduleTask();
...
} private void addScheduleTask() {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
DefaultMessageStore.this.cleanFilesPeriodically();
}
}, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
...
}
...
}

在这个任务中,就会执行DefaultMessageStore的cleanFilesPeriodically()方法。其实也就是会周期性地清理掉磁盘上超过72小时的CommitLog、ConsumeQueue文件。

cleanFilesPeriodically()方法中包含了清理CommitLog和ConsumeQueue文件的逻辑:

public class DefaultMessageStore implements MessageStore {
...
private void cleanFilesPeriodically() {
this.cleanCommitLogService.run();
this.cleanConsumeQueueService.run();
}
...
}

(2)触发删除文件的条件

条件一:如果当前时间是预先设置的凌晨4点,就会触发执行一次删除文件的逻辑,这个时间是默认的

条件二:如果磁盘空间不足了也就是超过了85%的使用率,就会马上触发执行一次删除文件的逻辑

条件一指的是:如果磁盘没有满 ,那么每天会进行一次删除磁盘文件的操作,默认在凌晨4点执行,因为那个时候基本是业务低峰期。

条件二指的是:如果磁盘使用率超过85%了,那么此时可以允许继续在磁盘里写入数据,但会马上触发一次删除文件的操作。

注意:如果磁盘使用率超过90%了,那么此时是不允许再往磁盘里写入新数据的,同时会马上删除文件。因为一旦磁盘满了,那么写入磁盘就会失败,此时MQ就会出现故障。

(3)删除文件的具体操作

在删除文件时,无非就是对文件进行遍历。如果一个文件超过72小时都没修改过了,此时就可以删除了,哪怕有的消息可能还没有被消费,此时也不会再让消费者去消费了,直接删除掉。

10.Consumer作为消费者是如何创建和启动的

(1)Cosumer是如何创建和启动的

(2)Consumer启动时的三个核心组件总结

(1)Cosumer是如何创建和启动的

一般会通过DefaultMQPushConsumerImpl来创建Consumer,然后调用它的start()方法进行启动。

在执行start()方法启动Consumer的过程中,就会执行如下代码让Consumer和Broker建立长连接。只有建立了长连接,Consumer才能不断地从Broker中拉取消息。其中,MQClientFactory也是基于Netty来实现的。

public class DefaultMQPushConsumerImpl implements MQConsumerInner {
...
public synchronized void start() throws MQClientException {
...
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
...
}
...
}

接着看start()方法的如下代码:

public class DefaultMQPushConsumerImpl implements MQConsumerInner {
...
public synchronized void start() throws MQClientException {
...
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
...
}
...
}

上述代码的RebalanceImpl就是专门负责Consumer重平衡的。如果ConsumerGroup中加入了一个新的Consumer,那么就会重新分配每个Consumer消费的MessageQueue。如果ConsumerGroup里某个Consumer宕机了,那么也会重新分配MessageQueue,这就是所谓的重平衡。

接着看start()方法的如下代码:

public class DefaultMQPushConsumerImpl implements MQConsumerInner {
...
public synchronized void start() throws MQClientException {
...
this.pullAPIWrapper = new PullAPIWrapper(mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
...
}
...
}

这个PullAPIWrapper就是消费者专门用来拉取消息的API组件。

接着看start()方法的如下代码:

public class DefaultMQPushConsumerImpl implements MQConsumerInner {
...
public synchronized void start() throws MQClientException {
...
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load();
...
}
...
}

上面代码中的OffsetStore其实就是用来存储和管理Consumer消费进度offset的一个组件。

(2)Consumer启动时的三个核心组件总结

DefaultMQPushConsumerImpl的start()方法最核心的就是这三个组件。

首先Consumer刚启动,需要根据Rebalancer组件进行重平衡,确定自己要分配哪些MessageQueue之后才去拉取消息。

然后在拉取消息时,需要根据PullAPI组件通过底层网络通信发送请求进行拉取。

接着在拉取消息的过程中,需要根据OffsetStore组件来维护offset消费进度。

如果ConsumerGroup中多了Consumer或者少了Consumer,那么就需要根据Rebalancer组件来进行重平衡。

11.消费者组的多个Consumer会如何分配消息

(1)Consumer的负载均衡问题

(2)重平衡组件如何分配MessageQueue

(1)Consumer的负载均衡问题

当一个业务系统部署多台机器时,每个系统里都启动了一个Consumer。多个Consumer会组成一个ConsumerGroup,也就是消费者组。此时就会有一个消费者组内的多个Consumer同时消费一个Topic,而且这个Topic是有多个MessageQueue分布在多个Broker上的。如下图示:

那么问题来了:如果一个业务系统部署在两台机器上,对应一个消费者组里就有两个Consumer。而业务系统需要消费的一个Topic有三个MessageQueue,那么应该怎么分配呢?这就涉及到Consumer的负载均衡问题了。

前面介绍Consumer启动时,就介绍了几个关键的组件,分别是:重平衡组件、消息拉取组件、消费进度组件。其中的重平衡组件,就是专门负责处理多个Consumer的负载均衡问题的。

(2)重平衡组件如何分配MessageQueue

那么这个RebalancerImpl重平衡组件是如何将多个MessageQueue均匀的分配给一个消费者组内的多个Consumer的?

实际上,每个Consumer在启动后,都会向所有的Broker进行注册,并且持续保持自己的心跳,让每个Broker都能感知到一个消费者组内有哪些Consumer。下图中没有画出Consumer向每个Broker进行注册以及心跳,只能大致示意一下。

每个Consumer在启动后,重平衡组件都会随机挑选一个Broker,从里面获取该消费者组里有哪些Consumer存在。

当重平衡组件知道了消费者组内有哪些Consumer后,接下来就好办了。无非就是把Topic下的MessageQueue均匀地分配给这些Consumer。这时候其实有几种算法可以进行分配,但比较常用的一种算法就是平均分配。

假设现在一共有3个MessageQueue,有2个Consumer。那么就会给1个Consumer分配2个MessageQueue,给另外1个Consumer分配剩余的1个MessageQueue。

假设现在一共有4个MessageQueue,有2个Consumer。那么就可以2个Consumer各自分配2个MessageQueue。

总之一切都是平均分配,尽量保证每个Consumer的负载差不多。这样,一旦MessageQueue负载确定后,Consumer就知道自己要消费哪几个MessageQueue的消息,于是就可以连接到那个Broker上,从里面不停拉取消息过来进行消费。

12.Consumer会如何从Broker拉取一批消息

(1)什么是消费者组

(2)集群模式消费 vs 广播模式消费

(3)MessageQueue和ConsumeQueue以及CommitLog之间的关系

(4)MessageQueue与消费者的关系

(5)Push消费模式 vs Pull消费模式

(6)Broker如何读取消息返回给消费者

(7)消费者如何处理消息、进行ACK响应以及提交消费进度

(8)消费者组出现宕机或扩容应如何处理

(9)消费源码的流程

(1)什么是消费者组

一.消费者组举例

消费者组的意思就是给一组消费者起一个名字。比如有一个Topic叫TopicOrderPaySuccess,库存系统、积分系统、营销系统、仓储系统都要去消费这个Topic中的数据,那么此时应该给这四个系统分别起一个消费者组名字,如下所示:

stock_consumer_group、marketing_consumer_group、
credit_consumer_group、wms_consumer_group

设置消费者组的方式如下所示:

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("stock_consumer_group");

假设库存系统部署了4台机器,每台机器上的消费者组的名字都是stock_consumer_group,那么这4台机器就同属于一个消费者组。以此类推,每个系统的几台机器都是属于各自的消费者组。

下图展示了两个系统,每个系统都有2台机器,每个系统都有一个自己的消费者组。

二.不同消费者组之间的关系

假设库存系统和营销系统作为两个消费者组,都订阅了TopicOrderPaySuccess这个订单支付成功消息的Topic,此时如果订单系统作为生产者发送了一条消息到这个Topic,那么这条消息会被如何消费呢?

一般情况下,这条消息进入Broker后,库存系统和营销系统作为两个消费者组,每个组都会拉取到这条消息。也就是说,这个订单支付成功的消息,库存系统会获取到一条,营销系统也会获取到一条,它们俩都会获取到这条消息。

但库存系统这个消费者组里有两台机器,是两台机器都获取到这条消息、还是只有一台机器会获取到这条消息?

一般情况下,库存系统的两台机器中只有一台机器会获取到这条消息,营销系统也是同理。

下图展示了对于同一条订单支付成功的消息,库存系统的一台机器获取到了、营销系统的一台机器也获取到了。所以在消费时,不同的系统应该设置不同的消费者组。如果不同的消费者组订阅了同一个Topic,对Topic里的同一条消息,每个消费者组都会获取到这条消息。

(2)集群模式消费 vs 广播模式消费

对于一个消费者组而言,它获取到一条消息后,如果消费者组内部有多台机器,到底是只有一台机器可以获取到这个消息,还是每台机器都可以获取到这个消息?这就是集群模式和广播模式的区别。

默认情况下都是集群模式:即一个消费者组获取到一条消息,只会交给组内的一台机器去处理,不是每台机器都可以获取到这条消息的。

但是可以通过如下设置来改变为广播模式:

consumer.setMessageModel(MessageModel.BROADCASTING);

如果修改为广播模式,那么对于消费者组获取到的一条消息,组内每台机器都可以获取到这条消息。但是相对而言广播模式用的很少,基本上都是使用集群模式来进行消费的。

(3)MessageQueue和ConsumeQueue以及CommitLog之间的关系

在创建Topic时,需要设置Topic有多少个MessageQueue。Topic中的多个MessageQueue会分散在多个Broker上,一个Broker上的一个MessageQueue会有多个ConsumeQueue文件。但在一个Broker运行过程中,一个MessageQueue只会对应一个ConsumeQueue文件。

对于Broker而言,存储在一个Broker上的所有Topic及MessageQueue数据都会写入一个统一的CommitLog文件,一个Broker收到的所有消息都会往CommitLog文件里面写。

对于Topic的各个MessageQueue而言,则是通过各个ConsumeQueue文件来存储属于MessageQueue的消息在CommitLog文件中的物理地址(即offset偏移量)。

(4)MessageQueue与消费者的关系

一个Topic上的多个MessageQueue是如何让一个消费者组中的多台机器来进行消费的?可以简单理解为,它会均匀将MessageQueue分配给消费者组的多台机器来消费。

举个例子,假设TopicOrderPaySuccess有4个MessageQueue,这4个MessageQueue分布在两个Master Broker上,每个Master Broker上有2个MessageQueue。然后库存系统作为一个消费者组,库存系统里有两台机器。那么正常情况下,最好就是让这两台机器各自负责2个MessageQueue的消费。比如库存系统的机器01从Master Broker01上消费2个MessageQueue,库存系统的机器02从Master Broker02上消费2个MessageQueue。这样就能把消费的负载均摊到两台Master Broker上。

所以大致可以认为一个Topic的多个MessageQueue会均匀分摊给消费者组内的多个机器去消费。

这里的一个原则是:一个MessageQueue只能被一个消费者机器去处理,但是一台消费者机器可以负责多个MessageQueue的消息处理。

(5)Push消费模式 vs Pull消费模式

一.一般选择Push消费模式

既然一个消费者组内的多台机器会分别负责一部分MessageQueue的消费的,那么每台机器都必须要连接到对应的Broker,尝试消费里面MessageQueue对应的消息。于是就涉及到两种消费模式了,一个是Push模式、一个是Pull模式。

这两个消费模式本质上是一样的,都是消费者主动发送请求到Broker去拉取一批消息进行处理。

Push消费模式是基于Pull消费模式来实现的,只不过它的名字叫做Push而已。在Push模式下,Broker会尽可能实时把新消息交给消费者进行处理,它的消息时效性会更好。

一般我们使用RocketMQ时,消费模式通常都选择Push模式来,因为Pull模式的代码写起来更加复杂和繁琐,而且Push模式底层本身就是基于Pull模式来实现的,只不过时效性更好而已。

二.Push消费模式的实现思路

当消费者发送请求到Broker去拉取消息时,如果有新的消息可以消费,那么就马上返回一批消息到消费机器去处理。消费者处理完之后,会接着发送请求到Broker机器去拉取下一批消息。

所以,消费者机器在Push模式下处理完一批消息,会马上发起请求拉取下一批消息,消息处理的时效性非常好,看起来就像Broker一直不停的推送消息到消费机器一样。

此外,Push模式下有一个请求挂起和长轮询的机制:当拉取消息的请求发送到Broker,Broker却发现没有新的消息可以处理时,就会让处理请求的线程挂起,默认是挂起15秒。然后在挂起期间,Broker会有一个后台线程,每隔一会就检查一下是否有新的消息。如果有新的消息,就主动唤醒被挂起的请求处理线程,然后把消息返回给消费者。

可见,常见的Push消费模式,本质也是消费者不断发送请求到Broker去拉取一批一批的消息。

(6)Broker如何读取消息返回给消费者

Broker在收到消费者的拉取请求后,是如何将消息读取出来,然后返回给消费者的?这涉及到ConsumeQueue和CommitLog。

假设一个消费者发送了拉取请求到Broker,表示它要拉取MessageQueue0中的消息,然后它之前都没拉取过消息,所以就从这个MessageQueue0中的第一条消息开始拉取。

于是,Broker就会找到MessageQueue0对应的ConsumeQueue0,从里面找到第一条消息的offset。接着Broker就需要根据ConsumeQueue0中找到的第一条消息的地址,去CommitLog中根据这个offset地址读取出这条消息的数据,然后把这条消息的数据返回给消费者。

所以消费者在消费消息时,本质就是:首先根据要消费的MessageQueue以及开始消费的位置,去找到对应的ConsumeQueue。然后在ConsumeQueue中读取要消费的消息在CommitLog中的offset偏移量。接着到CommitLog中根据offset读取出完整的消息数据,最后将完整的消息数据返回给消费者。

(7)消费者如何处理消息、进行ACK响应以及提交消费进度

消费者拉取到一批消息后,就会将这批消息传入注册的回调函数,如下所示:

consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//处理消息
//标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

当消费者处理完这批消息后,消费者就会提交目前的一个消费进度到Broker上,然后Broker就会存储消费者的消费进度。

比如现在对ConsumeQueue0的消费进度就是在offset=1的位置,那么Broker会记录下一个ConsumeOffset来标记该消费者的消费进度。这样下次这个消费者组只要再次拉取这个ConsumeQueue的消息,就可以从Broker记录的消费位置开始继续拉取,不用重头开始拉取了。

(8)消费者组出现宕机或扩容应如何处理

此时会进入一个Rebalance环节,也就是重新给各个消费者分配各自需要处理的MessageQueue。

比如现在机器01负责MessageQueue0和MessageQueue1,机器02负责MessageQueue2和MessageQueue3。如果现在机器02宕机了,那么机器01就会接管机器02之前负责的MessageQueue2和MessageQueue3。如果此时消费者组加入了一台机器03,那么就可以把机器02负责的MessageQueue3转移给机器03,然后机器01只负责一个MessageQueue2的消费,这就是负载重平衡。

(9)消费源码的流程

拉取消息的源码入口在DefaultMQPushConsumerImpl类的pullMessage()方法,里面涉及了:拉取请求、消息流量控制、通过PullAPIWrapper与服务端进行网络交互、服务端根据ConsumeQueue文件拉取消息等事情。

RocketMQ原理—3.源码设计简单分析下的更多相关文章

  1. RocketMQ原理及源码解析

    RocketMQ原理深入: 一.定义: RocketMQ是一款分布式.队列模型的消息中间件,有以下部分组成: 1.NameServer: 一个几乎无状态的节点,可集群部署,节点之间无任何信息同步 2. ...

  2. p2p技术之n2n源码核心简单分析一

    首先在开篇之前介绍下内网打洞原理 场景:一个服务器S1在公网上有一个IP,两个私网机器C1,C2 C1,C2分别由NAT1和NAT2连接到公网,我们需要借助S1将C1,C2建立直接的TCP连接,即由C ...

  3. HashMap和ConcurrentHashMap实现原理及源码分析

    HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...

  4. (转)ReentrantLock实现原理及源码分析

    背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...

  5. 【转】HashMap实现原理及源码分析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...

  6. 每天学会一点点(HashMap实现原理及源码分析)

    HashMap实现原理及源码分析   哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希 ...

  7. OpenMP Parallel Construct 实现原理与源码分析

    OpenMP Parallel Construct 实现原理与源码分析 前言 在本篇文章当中我们将主要分析 OpenMP 当中的 parallel construct 具体时如何实现的,以及这个 co ...

  8. OpenMP 线程同步 Construct 实现原理以及源码分析(上)

    OpenMP 线程同步 Construct 实现原理以及源码分析(上) 前言 在本篇文章当中主要给大家介绍在 OpenMP 当中使用的一些同步的 construct 的实现原理,如 master, s ...

  9. OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波

    http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ...

  10. ConcurrentHashMap实现原理及源码分析

    ConcurrentHashMap实现原理 ConcurrentHashMap源码分析 总结 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对Ha ...

随机推荐

  1. C#中的9个“黑魔法”

    C#中的9个"黑魔法"与"骚操作" 我们知道C#是非常先进的语言,因为是它很有远见的"语法糖".这些"语法糖"有时过于好 ...

  2. MySQL无开通SQL全审计下的故障分析方法

    几年前MySQL数据库出现突然的从库延迟故障和CPU爆高时,如何排查具体原因,可能说已在腾讯云的MySQL库里开启了SQL全审计,记录了全部执行的SQL,再通过下面的方法就可以很容易找到原因: 1,实 ...

  3. delphi Image 32 动画演示1

    Image 32 自带的Demo,添加一些注解. unit uFrmAnimation; interface uses Winapi.Windows, Winapi.Messages, System. ...

  4. 6、oracle网络(监听)

    oracle包含 1.软件 2.数据库 3.实例 4.监听(listener) 监听的特点 可以独立启动,就是说,数据库没有启动,监听可以启动:数据库启动,监听也可以不启动:数据库启动,监听也启动 监 ...

  5. Apache Shiro 721反序列化漏洞复现

    目录 漏洞原理 复现 修复方式 漏洞原理 Shiro 的RememberMe Cookie使用的是 AES-128-CBC 模式加密.其中 128 表示密钥长度为128位,CBC 代表Cipher B ...

  6. Educational Codeforces Round 90 (Rated for Div2)

    Donut Shops 现在有两个超市,第一个超市的物品按件卖,每件商品的售价为\(a\)元:第二个超市的物品按箱卖,每箱有\(b\)件物品,每箱售价为\(c\)元,现在要让你买\(x\)和\(y\) ...

  7. 调用非托管dll常出现的bug及解决办法

    转自http://www.51testing.com/html/00/n-832200.html C和C++有很多好的类库的沉淀,在.NET中,完全抛弃它们而重头再来是非常不明智的.也是不现实的,所以 ...

  8. windows版 nvm 1.1.7 安装(填坑)

    参考https://www.jianshu.com/p/cbf4f76ba0bb安装,注意事项: 1. 最好下载Setup安装版本,带安装界面,这样可以填写安装路径以及Nodejs路径,省去了改文件的 ...

  9. vscode 你想要的配置

    配置用户代码片段 文件 → 首选项 → 配置用户代码片段 比如配置一个vue3的代码片段: { "vue3-code": { "prefix": "v ...

  10. 【前端】【H5 API】addEventListener监听网络状态的变动

    WebviewObject Webview窗口对象,用于操作加载HTML页面的窗口 属性 id:webview窗口的标识 方法:监听 addEventListener 添加事件监听器 wobj.add ...