如何在同一個Java進程中連接多個RocketMQ服務器

前言

我們都知道,RocketMQ在代碼級別對連接服務器進行了限制,基本上可以理解爲一個JVM進程中只能連接一個NameServer,但實際應用場景中,我們可能會在架構設計層面上對RocketMQ進行了職能上的劃分,規定了A服務處理A類消息,而B服務處理B類消息,這時我們應該如何解決這個問題呢?

問題的根源

我們從代碼層級來分析到底爲什麼會產生“一個JVM實例只能連接一個NameServer”。
RocketMQ Client有一個核心類MQClientManager,在我們需要使用MQ Client實例的時候,實際上都是通過它的getAndCreateMQClientInstance方法進行創建的;名稱比較拗口,同時是GetCreate,這不太符合我們所說的設計單一性原則,但這不是我們討論的重點,我們看一看這個方法的實現

    public MQClientInstance getAndCreateMQClientInstance(ClientConfig clientConfig, RPCHook rpcHook) {
        String clientId = clientConfig.buildMQClientId();
        MQClientInstance instance = (MQClientInstance)this.factoryTable.get(clientId);
        if (null == instance) {
            instance = new MQClientInstance(clientConfig.cloneClientConfig(), this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
            MQClientInstance prev = (MQClientInstance)this.factoryTable.putIfAbsent(clientId, instance);
            if (prev != null) {
                instance = prev;
                log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
            } else {
                log.info("Created new MQClientInstance for clientId:[{}]", clientId);
            }
        }

        return instance;
    }

代碼不復雜,我們可以看到它利用客戶的配置信息生成一個固定的clientId,以此去緩存factoryTable中查找,不存在纔會創建全新一個實例。
那麼,可以理解一個clientID僅能存在一個連接實例了,可這個clientId是怎麼產生的呢?繼續跟蹤看看這段代碼

    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();
    }

代碼層面上對clientId進行了約定,格式爲“ClientIp@InstanceName”格式,當unitName不爲空的時候還會在後面加上“@unitName”。

怎麼解決?

從代碼分析上我們可以知道,爲了創建多實例,我們可以

  1. 設定不同的instanceName:

    instanceName從哪來的?

    instanceName = System.getProperty("rocketmq.client.name", "DEFAULT");

    從系統屬性中讀取出來的,也就是一般在JVM啓動時設定的。。。
    可以變嗎?當然,你可以通過代碼去做到,但這麼做的話,你會失去讓人理解你代碼的能力的,哈哈
    這就是爲什麼多少RocketMQ Client都只能連接一個服務器的原因了,它根本不考慮服務器是誰,僅關心自己,自私的傢伙!

  2. 設定不同的unitName

除此之外還有其它解決方案嗎?我仔細從網絡上翻了一輪,沒看到什麼好方法,是大家都沒這個場景還是有其它好辦法解決了呢?歡迎大家討論~

方法3

在上一篇博文來自平行世界的救贖裏面,我做了個工具sandbox,我提供的方法3就是依託於這個工具。
sandbox通過代碼隔離的方式,將另一份類定義放入沙箱中運行,從而實現多個實例完全隔離的效果。
MQClientManager通過緩存方式,以clientId作爲key值存儲到自身實例當中,爲了實現多個Client,那麼前兩種方法的邏輯是修改clientId實現多個實例,而方法3的邏輯則是“既然你的緩存已經有這個key,我就換個緩存”,本質就是“你這個鍋不裝我,我就換個鍋”。

怎麼做?

這裏我使用一個springboot項目作爲演示案例。
通過springboot的Configuration將多個RocketMQ Client進行註冊,再定義一個Controller接收不同請求去發送MQ消息,最後加上啓動類。
多MQ實例
我們先從pom文件中引入包(我沒有推上maven倉庫,各位可以從github/gitee上下載),代碼如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>me.van</groupId>
    <artifactId>rocket-mq-multi-client-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>測試多個rocketmq client共存</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>me.van.App</start-class>
        <java.version>1.8</java.version>
        <lombok.version>1.14.8</lombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.4.0</version>
        </dependency>
        <dependency>
            <groupId>me.van</groupId>
            <artifactId>sandbox</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

此處引入了apache的rocketmq-client組件作爲mq客戶端,也就是存在前面所說的問題的組件。

啓動類

package me.van;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

非常的簡單,沒什麼好介紹的。

配置類

package me.van;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MQProducer;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean(autowire = Autowire.BY_NAME, value = "producer")
    MQProducer producer() throws MQClientException {
        DefaultMQProducer producer = new DefaultMQProducer();
        initProducer(producer, "a.io:9876;b.io:9876");
        return producer;
    }

    @Bean(autowire = Autowire.BY_NAME, value = "producer_sandbox1")
    MQProducer producerSandbox1() throws MQClientException, SandboxCannotCreateObjectException {
        DefaultMQProducer producer = createProducerInSandbox();
        initProducer(producer, "x.io:9876;y.io:9876");
        return producer;
    }

    @Bean(autowire = Autowire.BY_NAME, value = "producer_sandbox2")
    MQProducer producerSandbox2() throws MQClientException, SandboxCannotCreateObjectException {
        DefaultMQProducer producer = createProducerInSandbox();
        initProducer(producer, "1.io:9876;2.io:9876");
        return producer;
    }

    private DefaultMQProducer createProducerInSandbox() throws SandboxCannotCreateObjectException {
        Sandbox sandbox = new Sandbox("org.apache.rocketmq.client");
        return sandbox.createObject(DefaultMQProducer.class);
    }

    private void initProducer(DefaultMQProducer producer, String namesrvAddr) throws MQClientException {
        producer.setNamesrvAddr(namesrvAddr);
        producer.setProducerGroup("test-group");
        producer.setRetryAnotherBrokerWhenNotStoreOK(true);
        producer.start();
    }
}

這裏可以看到,producer對象是直接new 出來的DefaultMQProducer,而producer_sandbox1producer_sandbox2是通過不同的沙箱創建出來的;三個client分別連接到不同的NameServer中,同時其它屬性保持一致。

Controller

package me.van;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.MQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    @Autowired
    MQProducer producer;
    @Autowired
    MQProducer producer_sandbox1;
    @Autowired
    MQProducer producer_sandbox2;

    @GetMapping("/")
    public String hello(){
        return "hello world";
    }

    @GetMapping("/send")
    public String send(String msg){
        if(null == msg) return "msg is null";

        String returnMsg = "";
        Message message = new Message("topic-test-multi-mq-client", msg.getBytes());
        try {
            producer.send(message);
            returnMsg += "原生producer發送完成<br/>";

            producer_sandbox1.send(message);
            returnMsg += "第一個沙箱內producer發送完成<br/>";

            producer_sandbox2.send(message);
            returnMsg += "第二個沙箱內producer發送完成<br/>";
        } catch (MQClientException | InterruptedException | RemotingException | MQBrokerException e) {
            returnMsg += "發送過程出現異常:" + e.getMessage();
        }
        return returnMsg;
    }
}

通過send方法同時向三個producer發送消息。

測試一下

運行App,等幾秒鐘啓動完畢,訪問http://localhost:8080/send,返回

msg is null

訪問,http://localhost:8080/send?msg=test

訪問結果

代碼地址

github: https://github.com/vancoo/multi-mq-demo
gitee: https://gitee.com/vancoo/multi-mq-demo

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