一、前情
今兒聽說業務小夥伴需要在項目中使用多個RocketMQ集羣,當前業務有一個集羣做canal消費使用(此MQ集羣開啓了ACL),需要在增加一個MQ集羣做業務數據發送,項目使用了Spring Boot組件。
好了,問題描述完了,概括下,就是當前有個MQ集羣在進行數據消費,需要在像另一個MQ集羣發送數據。整明白需求,搞起來,這不是分分鐘的事兒嗎,嗖嗖嗖,我就寫了下面的Config。
public class RocketMqConfig {
@Value("${rocketmq.mall.name-server}")
private String mallServer;
@Value("${rocketmq.mall.producer.group}")
private String producerGroup;
public DefaultMQProducer liveMQProducer() {
DefaultMQProducer producer;
producer = new DefaultMQProducer(producerGroup);
producer.setNamesrvAddr(mallServer);
return producer;
}
@Bean("mallMQTemplate")
public RocketMQTemplate mallMQTemplate( ObjectMapper rocketMQMessageObjectMapper) {
RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
rocketMQTemplate.setProducer(liveMQProducer());
rocketMQTemplate.setObjectMapper(rocketMQMessageObjectMapper);
return rocketMQTemplate;
}
}
看看,分分鐘搞定,使用的時候直接注入mallMQTemplate就可以了,交付完成後我就飄走了。
二、問題
然而,天有不測風雲,業務小夥伴緊急來電,測試環境報錯了,這玩意不好使啊,WTF?不能夠啊。
趕緊跑過去看了下異常。。。
Caused by: org.apache.rocketmq.client.exception.MQClientException: Send [3] times, still failed, cost [14]ms, Topic: SELL_xxx_TOPIC, BrokersSent: [broker-a, broker-a, broker-a]
See http://rocketmq.apache.org/docs/faq/ for further details.
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:638)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1310)
at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:358)
at org.apache.rocketmq.spring.core.RocketMQTemplate.syncSend(RocketMQTemplate.java:188)
... 36 common frames omitted
Caused by: org.apache.rocketmq.client.exception.MQBrokerException: CODE: 1 DESC: org.apache.rocketmq.acl.common.AclException: No accessKey is configured, org.apache.rocketmq.acl.plain.PlainPermissionManager.validate(PlainPermissionManager.java:371)
For more information, please visit the url, http://rocketmq.apache.org/docs/faq/
at org.apache.rocketmq.client.impl.MQClientAPIImpl.processSendResponse(MQClientAPIImpl.java:671)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageSync(MQClientAPIImpl.java:467)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:449)
at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:403)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendKernelImpl(DefaultMQProducerImpl.java:831)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:557)
... 39 common frames omitted
關鍵點 No accessKey is configured,嗯? 這玩意我業務集羣沒開ACL啊,設置個毛線。但冥冥中感覺那裏少配置啥了,但開發環境又沒有問題。
經過我這大腦一頓分析和測試,發現這發送的消費根本就沒到達測試環境的MQ業務集羣(這裏有個自身問題就是我們測試環境業務和canal MQ集羣是分開的,開發是在一起的)。馬上切換到開發環境測試一把,發現不管怎麼配置最後都會發送到canal集羣。
三、解決
復現了問題,那就來解決吧,翻了翻源代碼進行查看消息發送流程,把關鍵點標註下。
-
rocketMQTemplate.syncSend();
-
producer.send(rocketMsg, timeout);
-
this.defaultMQProducerImpl.send(msg, timeout);
-
this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
-
this.tryToFindTopicPublishInfo(msg.getTopic());
當調用到第5步的時候,問題出現了,這貨返回的根本就不是我配置的MQ業務集羣,而是canal的分區信息。呵呵,麻麥皮。進入 tryToFindTopicPublishInfo 方法,看了下關鍵點在於mQClientFactory 這個對象,居然是canal創建的對象,而不是我業務集羣創建的對象。
所以,問題就在於mQClientFactory,那就來看下這貨是怎麼創建的就可以了。
-
首先我們一眼就看到 mQClientFactory 是DefaultMQProducerImpl的屬性。
-
類的的依賴關係 RocketMQTemplate -> DefaultMQProducer -> DefaultMQProducerImpl -> mQClientFactory
-
在我們進行創建RocketMQTemplate的時候,因爲其實現了InitializingBean,所以afterPropertiesSet方法會執行.
-
這個時候就會調用DefaultMQProducer.start()。在DefaultMQProducer內又會調用 DefaultMQProducerImpl.start();
-
在DefaultMQProducerImpl start方法內就會發現mQClientFactory 的創建過程了。
MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
通過 getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook);方法得知,這貨搞了個單例把我們DefaultMQProducer都給緩存起來了。而其中關鍵代碼如下:
public String buildMQClientId() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClientIP());
sb.append("@");
sb.append(this.getInstanceName());
if (!UtilAll.isBlank(this.unitName)) {
sb.append("@");
sb.append(this.unitName);
}
return sb.toString();
}
這就是獲取key的方式,就是我們的IP加上ClientConfig 的屬性unitName得到的。所以如果我們沒有設置unitName,就算你再怎麼創建DefaultMQProducer,都只會獲得相同的一個。
所以,最後只需要加上一行代碼 producer.setUnitName(“mall”),就完美解決了這個問題,完整如下:
@Configuration
public class RocketMqConfig {
@Value("${rocketmq.mall.name-server}")
private String mallServer;
@Value("${rocketmq.mall.producer.group}")
private String producerGroup;
public DefaultMQProducer mallMQProducer() {
DefaultMQProducer producer;
producer = new DefaultMQProducer(producerGroup);
producer.setUnitName("mall");
producer.setNamesrvAddr(mallServer);
return producer;
}
@Bean("mallMQTemplate")
public RocketMQTemplate mallMQTemplate( ObjectMapper rocketMQMessageObjectMapper) {
RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
rocketMQTemplate.setProducer(mallMQProducer());
rocketMQTemplate.setObjectMapper(rocketMQMessageObjectMapper);
return rocketMQTemplate;
}
}
四、總結
在使用Spring Boot RocketMQTemplate 多集羣發送消息時,因爲DefaultMQProducerImpl內部會通過MQClientManager維護一個defaultMQProducer的緩存,而key是IP加unitName拼接的,所以一定要設置unitName,防止defaultMQProducer使用錯亂。