文章目錄
1.RocketMQ簡介
(1) RocketMQ 特點
- 是一個隊列模型的消息中間件,具有高性能、高可靠、高實時、分佈式特點。
- Producer、Consumer、隊列都可以分佈式。
- Producer向一些隊列輪流發送消息,隊列集合稱爲Topic,Consumer如果做廣播消費,則一個consumer實例消費這個Topic對應的所有隊列,如果做集羣消費,則多個Consumer實例平均消費這個topic對應的隊列集合。
- 能夠保證嚴格的消息順序
- 提供豐富的消息拉取模式
- 高效的訂閱者水平擴展能力
- 實時的消息訂閱機制
- 億級消息堆積能力
- 較少的依賴
(2) RocketMQ 物理部署結構
如上圖所示, 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 邏輯部署結構
如上圖所示,RocketMQ的邏輯部署結構有Producer和Consumer兩個特點。
Producer Group
- 用來表示一個發送消息應用,一個Producer Group下包含多個Producer實例,可以是多臺機器,也可以是一臺機器的多個進程,或者一個進程的多個Producer對象。一個Producer Group可以發送多個Topic消息
Producer Group作用如下:
- 標識一類Producer
- 可以通過運維工具查詢這個發送消息應用下有多個Producer實例
- 發送分佈式事務消息時,如果Producer中途意外宕機,Broker會主動回調Producer Group內的任意一臺機器來確認事務狀態。
Consumer Group
- 用來表示一個消費消息應用,一個Consumer Group下包含多個Consumer實例,可以是多臺機器,也可以是多個進程,或者是一個進程的多個Consumer對象。一個Consumer Group下的多個Consumer以均攤方式消費消息,如果設置爲廣播方式,那麼這個Consumer Group下的每個實例都消費全量數據。
(4) RocketMQ 數據存儲結構
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 引入相應的包