同一个Topic可以在多个Broker中创建,同一个Broker中的某一个Topic可以拥有多个队列。那么如何保证消息在队列中顺序呢?
AllocateMessageQueueStrategy 消费者均衡 MessageQueueSelector 生产者均衡 topicRouteData2TopicPublishInfo
在RocketMQ 中在 org.apache.rocketmq.client.producer.selector 包中,负责Producer发送负载均衡。在创建topic的时候,每一个分区每一个topic都有会topicConf信息。
在topic config中都有writeQueueNum. 在producer 从name获取topic config信息后,会把这个信息转化成MessageQueue信息。在Producer 发送信息时,会通过MessageQueueSelector 来进行负载均衡。
在RocketMQ 中消息只保证在同一个分区同一个Queue队列的顺序性。 所以,要保证业务逻辑的顺序性,可以通过MessageQueue 来完成操作。
Producer发送负载均衡逻辑代码:
//MessageQueue 队列选择器
public interface MessageQueueSelector {
/**
* 选择出发送消息的MessageQueue
* @param mqs 与该topic相关联的所有MessageQueue队列
* @param msg 发送的消息
* @param arg 业务对象
* @return 发送消息的消息队里
*/
MessageQueue select(final List mqs, final Message msg, final Object arg);
}
RocketMQ 提供几种负载均衡策略: 随机、散列等
public class SelectMessageQueueByRandoom implements MessageQueueSelector {
private Random random = new Random(System.currentTimeMillis());
@Override
public MessageQueue select(List mqs, Message msg, Object arg) {
int value = random.nextInt();
if (value < 0) {
value = Math.abs(value);
}
value = value % mqs.size();
return mqs.get(value);
}
}
发送负载均衡源代码如下:
private SendResult sendSelectImpl(//
Message msg, //
MessageQueueSelector selector, //
Object arg, //
final CommunicationMode communicationMode, //
final SendCallback sendCallback, final long timeout//
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
Validators.checkMessage(msg, this.defaultMQProducer);
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
MessageQueue mq = null;
try {
mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
} catch (Throwable e) {
throw new MQClientException("select message queue throwed exception.", e);
}
if (mq != null) {
return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout);
} else {
throw new MQClientException("select message queue return null.", null);
}
}
throw new MQClientException("No route info for this topic, " + msg.getTopic(), null);
}
在Consumer端负载均衡,当使用pull Consumer 时,当订阅topic时会调用subscriptionAutomatically 方法。
public void subscriptionAutomatically(final String topic) {
if (!this.rebalanceImpl.getSubscriptionInner().containsKey(topic)) {
try {
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(), //
topic, SubscriptionData.SUB_ALL);
this.rebalanceImpl.subscriptionInner.putIfAbsent(topic, subscriptionData);
} catch (Exception ignore) {
}
}
}
public void subscribe(String topic, String subExpression) throws MQClientException {
try {
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), //
topic, subExpression);
this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
if (this.mQClientFactory != null) {
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}
} catch (Exception e) {
throw new MQClientException("subscription exception", e);
}
}
实质上由RebalanceService 服务类定时执行rebalance方法。
public class RebalanceService extends ServiceThread {
private static long waitInterval =
Long.parseLong(System.getProperty(
"rocketmq.client.rebalance.waitInterval", "20000"));
private final Logger log = ClientLogger.getLog();
private final MQClientInstance mqClientFactory;
public RebalanceService(MQClientInstance mqClientFactory) {
this.mqClientFactory = mqClientFactory;
}
@Override
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
this.waitForRunning(waitInterval);
this.mqClientFactory.doRebalance();
}
log.info(this.getServiceName() + " service end");
}
@Override
public String getServiceName() {
return RebalanceService.class.getSimpleName();
}
}
当Consumer获取所有topic config 信息后,RebalanceService进行负载均衡,然后订阅指定的MessageQueue信息。
Consumer端负载均衡代码如下:
//把同一个topic中不同的MessageQueue 分配给消费端
private void rebalanceByTopic(final String topic, final boolean isOrder) {
//根据模式来分配
switch (messageModel) {
//广播模式
case BROADCASTING: {
//在广播模式下,consumer 消费每一个topic信息
Set mqSet = this.topicSubscribeInfoTable.get(topic);
if (mqSet != null) {
boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
if (changed) {
this.messageQueueChanged(topic, mqSet, mqSet);
log.info("messageQueueChanged {} {} {} {}", //
consumerGroup, //
topic, //
mqSet, //
mqSet);
}
} else {
log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
}
break;
}
//集群模式下
case CLUSTERING: {
Set mqSet = this.topicSubscribeInfoTable.get(topic);
List cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
if (null == mqSet) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
}
}
if (null == cidAll) {
log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic);
}
//把同一个Topic下面的所有MessageQueue 和该Topic下面的所有Client进行分配
if (mqSet != null && cidAll != null) {
List mqAll = new ArrayList();
mqAll.addAll(mqSet);
//对All MessageQueue 和All Client 进行排序
Collections.sort(mqAll);
Collections.sort(cidAll);
//消费者均衡策略
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
List allocateResult = null;
try {
//执行Consumer端负载均衡策略
allocateResult = strategy.allocate(//
this.consumerGroup, //
this.mQClientFactory.getClientId(), //
mqAll, //
cidAll);
} catch (Throwable e) {
log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
e);
return;
}
//分配结果
Set allocateResultSet = new HashSet();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}
//对分配结果进行订阅
boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
if (changed) {
log.info(
"rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
allocateResultSet.size(), allocateResultSet);
this.messageQueueChanged(topic, mqSet, allocateResultSet);
}
}
break;
}
default:
break;
}
}
@Override
public void dispatchPullRequest(List pullRequestList) {
for (PullRequest pullRequest : pullRequestList) {
this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);
log.info("doRebalance, {}, add a new pull request {}", consumerGroup, pullRequest);
}
}
在org.apache.rocketmq.client.consumer 包中AllocateMessageQueueStrategy 类在负载分配topic中所有MessageQueue 与所有客户端的关系。
/**
* Strategy Algorithm for message allocating between consumers
*/
public interface AllocateMessageQueueStrategy {
/**
* Allocating by consumer id
*
* @param consumerGroup current consumer group
* @param currentCID current consumer id
* @param mqAll message queue set in current topic
* @param cidAll consumer set in current consumer group
* @return The allocate result of given strategy
*/
List allocate(
final String consumerGroup,
final String currentCID,
final List mqAll,
final List cidAll
);
/**
* Algorithm name
*
* @return The strategy name
*/
String getName();
}