RocketMQ實踐-簡介+部署+Client開發(producer&consumer)

1.RocketMQ簡介

(1) RocketMQ 特點

  • 是一個隊列模型的消息中間件,具有高性能、高可靠、高實時、分佈式特點。
  • Producer、Consumer、隊列都可以分佈式。
  • Producer向一些隊列輪流發送消息,隊列集合稱爲Topic,Consumer如果做廣播消費,則一個consumer實例消費這個Topic對應的所有隊列,如果做集羣消費,則多個Consumer實例平均消費這個topic對應的隊列集合。
  • 能夠保證嚴格的消息順序
  • 提供豐富的消息拉取模式
  • 高效的訂閱者水平擴展能力
  • 實時的消息訂閱機制
  • 億級消息堆積能力
  • 較少的依賴

(2) RocketMQ 物理部署結構

image

如上圖所示, RocketMQ的部署結構有以下特點:

  • Name Server是一個幾乎無狀態節點,可集羣部署,節點之間無任何信息同步。
  • Broker部署相對複雜,Broker分爲Master與Slave,一個Master可以對應多個Slave,但是一個Slave只能對應一個Master,Master與Slave的對應關係通過指定相同的BrokerName,不同的BrokerId來定義,BrokerId爲0表示Master,非0表示Slave。Master也可以部署多個。每個Broker與Name Server集羣中的所有節點建立長連接,定時註冊Topic信息到所有Name Server。
  • Producer與Name Server集羣中的其中一個節點(隨機選擇)建立長連接,定期從Name Server取Topic路由信息,並向提供Topic服務的Master建立長連接,且定時向Master發送心跳。Producer完全無狀態,可集羣部署。
  • Consumer與Name Server集羣中的其中一個節點(隨機選擇)建立長連接,定期從Name Server取Topic路由信息,並向提供Topic服務的Master、Slave建立長連接,且定時向Master、Slave發送心跳。Consumer既可以從Master訂閱消息,也可以從Slave訂閱消息,訂閱規則由Broker配置決定。

(3) RocketMQ 邏輯部署結構

image

如上圖所示,RocketMQ的邏輯部署結構有Producer和Consumer兩個特點。

Producer Group

  • 用來表示一個發送消息應用,一個Producer Group下包含多個Producer實例,可以是多臺機器,也可以是一臺機器的多個進程,或者一個進程的多個Producer對象。一個Producer Group可以發送多個Topic消息

Producer Group作用如下:

  1. 標識一類Producer
  2. 可以通過運維工具查詢這個發送消息應用下有多個Producer實例
  3. 發送分佈式事務消息時,如果Producer中途意外宕機,Broker會主動回調Producer Group內的任意一臺機器來確認事務狀態。

Consumer Group

  • 用來表示一個消費消息應用,一個Consumer Group下包含多個Consumer實例,可以是多臺機器,也可以是多個進程,或者是一個進程的多個Consumer對象。一個Consumer Group下的多個Consumer以均攤方式消費消息,如果設置爲廣播方式,那麼這個Consumer Group下的每個實例都消費全量數據。

(4) RocketMQ 數據存儲結構

image

2.CentOS7 部署

(1) 下載&安裝

依賴安裝

# 安裝 jdk1.8
> yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel

配置JDK環境

> vim /etc/profile

# set jdk path
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib:$CLASSPATH
export JAVA_PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin
export PATH=$PATH:${JAVA_PATH}

> source /etc/profile

否則執行 bin/mqadmin 等RocketMQ命令會報錯

> sh bin/mqadmin topicList -n 192.168.160.2:9876
RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.PlatformDependent0).
RocketMQLog:WARN Please initialize the logger system properly.
org.apache.rocketmq.tools.command.SubCommandException: TopicListSubCommand command failed
	at org.apache.rocketmq.tools.command.topic.TopicListSubCommand.execute(TopicListSubCommand.java:113)
	at org.apache.rocketmq.tools.command.MQAdminStartup.main0(MQAdminStartup.java:139)
	at org.apache.rocketmq.tools.command.MQAdminStartup.main(MQAdminStartup.java:90)

RocketMQ安裝

# https://mirrors.tuna.tsinghua.edu.cn/apache/rocketmq 查看版本 目前使用 4.6.1

> wget https://mirrors.tuna.tsinghua.edu.cn/apache/rocketmq/4.6.1/rocketmq-all-4.6.1-bin-release.zip

> uzip rocketmq-all-4.6.1-bin-release.zip

> cd rocketmq-all-4.6.1-bin-release

(2) 配置

使用默認提供的 conf/broker.conf 配置文件

> vim conf/broker.conf

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH

指定 broker 自己監聽的 IP 地址,以及 namesrv 的地址和端口,不然 rocketmq-console、mqadmin 客戶端等會因爲跨域而連接不上,只需要在conf/broker.conf 配置文件中添加 brokerIP1 和 namesrvAddr 配置即可:

# 支持跨域
brokerIP1 = 192.168.160.2
namesrvAddr = 192.168.160.2:9876

關閉selinux

# 首先查看 SELinux功能是否開啓

> getenforce

# 如果顯示Permissive 或者 Disabled 該步驟直接跳過,如果是enforcing ,進行下一步

> vim /etc/selinux/config # 或者 vim /etc/sysconfig/selinux

# 設置 SELINUX=enforcing 爲 SELINUX=permissive(/disabled)

setenforce 0 # 讓設置生效

getenforce # 查看

(3) 啓動&測試&停止 服務

Start Name Server

> nohup sh bin/mqnamesrv &
> tail -f ~/logs/rocketmqlogs/namesrv.log

Start Broker

> nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf &
> tail -f ~/logs/rocketmqlogs/broker.log 

Send & Receive Messages

> export NAMESRV_ADDR=localhost:9876
> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
SendResult [sendStatus=SEND_OK, msgId= ...

> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
ConsumeMessageThread_%d Receive New Messages: [MessageExt...

Shutdown Servers

> sh bin/mqshutdown broker
The mqbroker(36695) is running...
Send shutdown request to mqbroker(36695) OK

> sh bin/mqshutdown namesrv
The mqnamesrv(36664) is running...
Send shutdown request to mqnamesrv(36664) OK

(4) 端口相關問題

端口說明

  • 9876 : NameServer 端口
  • 10909 : vip 通道端口 | fastListenPort
  • 10910 :
  • 10911 : 核心端口 | Broker 對外服務的監聽端口 | 非 vip 通道端口 | listenPort
  • 10912 : xxx | haListenPort
> netstat -ntlp | grep java
tcp6       0      0 :::9876                 :::*                    LISTEN      2247/java           
tcp6       0      0 :::10909                :::*                    LISTEN      2277/java           
tcp6       0      0 :::10911                :::*                    LISTEN      2277/java           
tcp6       0      0 :::10912                :::*                    LISTEN      2277/java

防火牆設置

# 根據實際情況放開端口,NameServer 端口、Broker 監聽端口,必設置
firewall-cmd --zone=public --add-port=9876/tcp --permanent
firewall-cmd --zone=public --add-port=10911/tcp --permanent
firewall-cmd --reload

外網端口映射

NameServer 的 9876 端口可正常映射

Broker 的 4 個端口需要內外網一致,即監聽 13174,則該端口映射出去的端口也是 13174

broker 還會監聽另外幾個端口,同樣的都是本地監聽和映射的端口相同即可。

# 公網 IP
brokerIP1 = xx.xx.xx.xx
# 對應 broker 監聽端口,默認爲10911,調整後,影響4個端口
# 10909/10910/10911/10912 -> 13172/13173/13174/13175
listenPort = 13174

namesrvAddr = 192.168.160.2:9876

(5) 待研究

client - consumer 端斷開,broker log 會有如下Error日誌,強迫症表示後續繼續研究

> tail -f ~/logs/rocketmqlogs/broker.log 
2020-03-05 10:24:35 INFO NettyEventExecutor - NETTY EVENT: remove channel[ClientChannelInfo [channel=[id: 0x63017fd9, L:/192.168.160.2:10911 ! R:/192.168.150.144:54648], clientId=192.168.126.1@DEFAULT, language=JAVA, version=335, lastUpdateTimestamp=1583375060273]][192.168.150.144:54648] from ProducerManager groupChannelTable, producer group: CLIENT_INNER_PRODUCER
2020-03-05 10:24:35 WARN NettyEventExecutor - NETTY EVENT: remove not active channel[ClientChannelInfo [channel=[id: 0x63017fd9, L:/192.168.160.2:10911 ! R:/192.168.150.144:54648], clientId=192.168.126.1@DEFAULT, language=JAVA, version=335, lastUpdateTimestamp=1583375060273]] from ConsumerGroupInfo groupChannelTable, consumer group: Producer
2020-03-05 10:24:35 INFO NettyEventExecutor - unregister consumer ok, no any connection, and remove consumer group, Producer
2020-03-05 10:24:51 WARN PullMessageThread_4 - the consumer's group info not exist, group: Producer
2020-03-05 10:24:51 WARN PullMessageThread_5 - the consumer's group info not exist, group: Producer
2020-03-05 10:24:51 WARN PullMessageThread_1 - the consumer's group info not exist, group: Producer
2020-03-05 10:24:51 WARN PullMessageThread_3 - the consumer's group info not exist, group: Producer
2020-03-05 10:24:51 ERROR NettyServerNIOSelector_3_1 - processRequestWrapper response to /192.168.150.144:54648 failed
java.nio.channels.ClosedChannelException: null
	at io.netty.channel.AbstractChannel$AbstractUnsafe.write(...)(Unknown Source) ~[netty-all-4.0.42.Final.jar:4.0.42.Final]
2020-03-05 10:24:51 ERROR NettyServerNIOSelector_3_1 - RemotingCommand [code=11, language=JAVA, version=335, opaque=1040, flag(B)=0, remark=null, extFields={queueId=1, maxMsgNums=32, sysFlag=2, suspendTimeoutMillis=15000, commitOffset=0, topic=newTopic, queueOffset=75, expressionType=TAG, subVersion=1583374069385, consumerGroup=Producer}, serializeTypeCurrentRPC=JSON]
2020-03-05 10:24:51 ERROR NettyServerNIOSelector_3_1 - RemotingCommand [code=24, language=JAVA, version=335, opaque=1040, flag(B)=1, remark=the consumer's group info not exist
See http://rocketmq.apache.org/docs/faq/ for further details., extFields=null, serializeTypeCurrentRPC=JSON]

3.RocketMQ-client java demo

(1) 創建 Spring boot 項目

在pom.xml中添加(使用jdk1.8)

<!-- RocketMq客戶端相關依賴 -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.6.1</version>
</dependency>

resources文件中properties中寫入

# NameServer地址
apache.rocketmq.namesrvAddr=192.168.160.2:9876

# 生產者的組名
apache.rocketmq.producer.producerGroup=Producer
# 生產者的topic
apache.rocketmq.producer.topic=newTopic
# 生產者的tags
apache.rocketmq.producer.tags=newTag

# 消費者的組名
apache.rocketmq.consumer.consumerGroup=Producer
# 消費者的topic
apache.rocketmq.consumer.topic=newTopic
# 消費者的subExpression
apache.rocketmq.consumer.subExpression=newTag

(2) 代碼編寫

創建 Producer.java,代碼如下

@Component
public class Producer {

    /**
     * 生產者的組名
      */

    @Value("${apache.rocketmq.producer.producerGroup}")
    private String producerGroup;

    /**
      * NameServer 地址
      */

    @Value("${apache.rocketmq.namesrvAddr}")
    private String namesrvAddr;

    @Value("${apache.rocketmq.producer.topic}")
    private String topic;

    @Value("${apache.rocketmq.producer.tags}")
    private String tags;

    @PostConstruct
    public void defaultMQProducer() {

        System.out.println("---------- RocketMQ Producer Start ----------");

        System.out.println("producerGroup:" + producerGroup + ",namesrvAddr:" + namesrvAddr);

        //生產者的組名
        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);

        //指定NameServer地址,多個地址以 ; 隔開
        producer.setNamesrvAddr(namesrvAddr);

        try {
        /**
         * Producer對象在使用之前必須要調用start初始化,初始化一次即可
         * 注意:切記不可以在每次發送消息時,都調用start方法
         */

            producer.start();

            for (int i = 0; i < 100; i++) {
                String messageBody = "我是消息內容:" + i;
                String message = new String(messageBody.getBytes(), "utf-8");
                //構建消息
                Message msg = new Message(topic, tags, "key_" + i /* Keys */, message.getBytes());
                //發送消息
                SendResult result = producer.send(msg);
                System.out.println("[Producer] 發送響應:MsgId:" + result.getMsgId() + ", SendStatus:" + result.getSendStatus());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            producer.shutdown();
        }
    }
}

創建 Consumer.java,代碼如下

@Component
public class Consumer {
    /**
     * 消費者的組名
     */

    @Value("${apache.rocketmq.consumer.consumerGroup}")
    private String consumerGroup;

    /**
     * NameServer地址
     */

    @Value("${apache.rocketmq.namesrvAddr}")
    private String namesrvAddr;

    @Value("${apache.rocketmq.consumer.topic}")
    private String topic;

    @Value("${apache.rocketmq.consumer.subExpression}")
    private String subExpression;

    @PostConstruct
    public void defaultMQPushConsumer() {

        System.out.println("---------- RocketMQ Consumer Start ----------");

        System.out.println("consumerGroup:" + consumerGroup + ",namesrvAddr:" + namesrvAddr);

        //消費者的組名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);

        //指定NameServer地址,多個地址以 ; 隔開
        consumer.setNamesrvAddr(namesrvAddr);
        try {
            //訂閱PushTopic下Tag爲push的消息
            consumer.subscribe(topic, subExpression);

            //設置Consumer第一次啓動是從隊列頭部開始消費還是隊列尾部開始消費
            //如果非第一次啓動,那麼按照上次消費的位置繼續消費
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

            //可以修改每次消費消息的數量,默認設置是每次消費一條
            consumer.setConsumeMessageBatchMaxSize(1);

            //設置消費模式爲廣播
            consumer.setMessageModel(MessageModel.BROADCASTING);

            consumer.registerMessageListener(new MessageListenerConcurrently() {

                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                    try {
                        for (MessageExt messageExt : list) {
                            //System.out.println("messageExt: " + messageExt);//輸出消息內容
                            String messageBody = new String(messageExt.getBody(), "utf-8");
                            System.out.println("[Consumer] 消費響應:MsgId:" + messageExt.getMsgId() + ", MsgBody:" + messageBody);//輸出消息內容

                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER; //稍後再試
                    }
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //消費成功
                }
            });
            consumer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

紅色標識符,alt + enter 引入相應的包

(3) Code Example

Github RocketMQDemo

4.參考鏈接

Apache RocketMQ 官方文檔

阿里中間件博客-RocketMQ

RocketMQ命令使用詳解

RocketMQ Client踩坑

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