Spring Boot RocketMQ 多集羣客戶端使用小坑記錄

一、前情

今兒聽說業務小夥伴需要在項目中使用多個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集羣。

三、解決

復現了問題,那就來解決吧,翻了翻源代碼進行查看消息發送流程,把關鍵點標註下。

  1. rocketMQTemplate.syncSend();

  2. producer.send(rocketMsg, timeout);

  3. this.defaultMQProducerImpl.send(msg, timeout);

  4. this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);

  5. this.tryToFindTopicPublishInfo(msg.getTopic());

當調用到第5步的時候,問題出現了,這貨返回的根本就不是我配置的MQ業務集羣,而是canal的分區信息。呵呵,麻麥皮。進入 tryToFindTopicPublishInfo 方法,看了下關鍵點在於mQClientFactory 這個對象,居然是canal創建的對象,而不是我業務集羣創建的對象。

所以,問題就在於mQClientFactory,那就來看下這貨是怎麼創建的就可以了。

  1. 首先我們一眼就看到 mQClientFactory 是DefaultMQProducerImpl的屬性。

  2. 類的的依賴關係 RocketMQTemplate -> DefaultMQProducer -> DefaultMQProducerImpl -> mQClientFactory

  3. 在我們進行創建RocketMQTemplate的時候,因爲其實現了InitializingBean,所以afterPropertiesSet方法會執行.

  4. 這個時候就會調用DefaultMQProducer.start()。在DefaultMQProducer內又會調用 DefaultMQProducerImpl.start();

  5. 在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使用錯亂。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章