Apache RocketMQ 自诞生以来,因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经阿里十余年的大规模场景打磨,RocketMQ 已经成为业内共识的金融级可靠业务消息首选方案,被广泛应用于互联网、大数据、移动互联网、物联网等领域的业务场景。
RocketMQ 的安装包分为两种
https://rocketmq.apache.org/zh/download/
当前最新的版本为 5.1.3
我们通过二进制下载来安装。
64位操作系统,推荐 Linux/Unix/macOS
64位 JDK 1.8+
我们的示例在linux CentOS 发行版下安装
解压后文件夹名字可能太长,可以修改一下,如我们修改为:rocketmq-5.1.3
如果是源码包,则需要执行如下操作
$ unzip rocketmq-all-5.1.3-source-release.zip $ cd rocketmq-all-5.1.3-source-release/ $ mvn -Prelease-all -DskipTests -Dspotbugs.skip=true clean install -U $ cd distribution/target/rocketmq-5.1.3/rocketmq-5.1.3
### 启动namesrv(注意:是在安装主目录执行)
$ nohup sh bin/mqnamesrv &
### 验证namesrv是否启动成功
$ tail -f ~/logs/rocketmqlogs/namesrv.log
日志打印出如下字样则说明启动成功
..... The Name Server boot success. .......
NameServer 的默认端口为9876,IP 为部署服务器IP地址
在 5.0 版本中 Proxy 和 Broker 根据实际诉求可以分为 Local 模式和 Cluster 模式。官网推荐模式为 Local 模式。
这两种模式,又对应了多种可部署的组合方式,这个我们后续章再说。我们当前先使用单组节点单副本模式启动。
### 先启动broker
## -n 后面是 nameServe 的地址,由于我们的broker和nameServer在同一台服务器上执行,所以使用localhost
$ nohup sh bin/mqbroker -n localhost:9876 --enable-proxy &
### 验证broker是否启动成功, 比如, broker的ip是192.168.1.2 然后名字是broker-a
$ tail -f ~/logs/rocketmqlogs/proxy.log
#---- 日志中出现如下内容 broker + proxy 即启动成功。
The broker[broker-a,192.169.1.2:10911] boot success...
broker 默认端口为10911,proxy 默认端口为8081
启动后 在 ~/logs/rocketmqlogs 目录下还可以看见 broker.log、namesrv.log、proxy.log
./mqshutdown proxy
./mqshutdown broker
./mqshutdown namesrv
在使用本示例时,很多概念都还没有讲解,对于初次接触的人来说有些困难,这不要紧,直接无脑按顺序操作即可,先体验结果,后续将逐步详细讲解这些内容。
5.0版本下创建主题操作,官网推荐使用 mqadmin 命令工具来创建。
$ sh bin/mqadmin updatetopic -n localhost:9876 -t TestTopic -c DefaultCluster
-n 表示 nameserver 的地址,-t 表示主题名称(用于唯一标识主题),-c 集群名称,RocketMQ 默认集群名称就是 DefaultCluster
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-client-javaartifactId>
<version>${rocketmq-client-java-version}version>
dependency>
${rocketmq-client-java-version} 使用最新的版本即可。在maven 仓库中搜索即可。本文编写时的最新版本为:5.0.5。
注:4.x 的示例是使用的 rocketmq-client 是 Remoting 协议的SDK,其随着 RocketMQ 一同发布,版本号也和 RocketMQ 相同,但它仅支持 Java 语言,这里是使用的 rocketmq-client-java,其是 gRPC 协议,他们是不同的 Client,且支持 Java/C++/.NET/Go/Rust 等语言
rocketmq-client-java 官方说明:rocketmq-client-java 客户端基于 RocektMQ 5.0 存算分离架构进行设计开发,是 RocketMQ 社区目前推荐的接入方式,也是未来客户端演进的主要方向。所以,此处我们采用 rocketmq-client-java 做为客户端进行讲解。
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientConfigurationBuilder;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.SendReceipt;
import java.io.IOException;
public class ProducerDemo {
public static void main(String[] args) throws ClientException, InterruptedException {
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoint = "192.168.1.1:8081";
// 消息发送的目标Topic名称,需要提前创建。
String topic = "TestTopic";
ClientServiceProvider provider = ClientServiceProvider.loadService();
ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(endpoint);
ClientConfiguration configuration = builder.build();
// 初始化Producer时需要设置通信配置以及预绑定的Topic。
Producer producer = provider.newProducerBuilder()
.setTopics(topic)
.setClientConfiguration(configuration)
.build();
for(int i = 0; i < 1000;i++) {
// 普通消息发送。
Message message = provider.newMessageBuilder()
.setTopic(topic)
// 设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("messageTag")
// 消息体。
.setBody(("messageBody" + i).getBytes())
.build();
try {
// 发送消息,需要关注发送结果,并捕获失败等异常。
SendReceipt sendReceipt = producer.send(message);
System.out.println("Send message successfully, messageId=" + sendReceipt.getMessageId());
} catch (ClientException e) {
System.out.println("Failed to send message");
}
// 等待10秒再次发送
Thread.sleep(10000);
}
// 发送完,关闭
try {
producer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
控制台会打印类似: Send message successfully, messageId=01005056C0000846DC04DFC51400000000 的结果
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
import java.nio.ByteBuffer;
import java.util.Collections;
public class ConsumerDemo {
public static void main(String[] args) throws InterruptedException, ClientException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoints = "192.168.1.1:8081";
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
.setEndpoints(endpoints)
.build();
// 订阅消息的过滤规则,表示订阅所有Tag的消息。
String tag = "*";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 为消费者指定所属的消费者分组。
String consumerGroup = "YourConsumerGroup";
// 指定需要订阅哪个目标Topic,Topic需要提前创建。
String topic = "TestTopic";
// 初始化PushConsumer,需要绑定消费者分组ConsumerGroup、通信参数以及订阅关系。
PushConsumer pushConsumer = provider.newPushConsumerBuilder()
.setClientConfiguration(clientConfiguration)
// 设置消费者分组。
.setConsumerGroup(consumerGroup)
// 设置预绑定的订阅关系。
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
// 设置消费监听器。
.setMessageListener(messageView -> {
System.out.println(messageView);
ByteBuffer rs = messageView.getBody();
byte[] rsByte = new byte[rs.limit()];
rs.get(rsByte);
System.out.println("Message body:" + new String(rsByte));
// 处理消息并返回消费结果。
System.out.println("Consume message successfully, messageId=" + messageView.getMessageId());;
return ConsumeResult.SUCCESS;
})
.build();
// 如果不需要再使用 PushConsumer,可关闭该实例。
// 不调用close 该程序不会关闭
// pushConsumer.close();
}
}
消费者有3中类型,这里的消费者我们使用了 PushConsumer 。后续我们会详解。打印结果如下:
MessageViewImpl{messageId=01005056C00008017404E0A8AD00000000, topic=TestTopic, bornHost=xxx, bornTimestamp=1691291309533, endpoints=ipv4:192.168.1.1:8081, deliveryAttempt=1, tag=messageTag, keys=[messageKey], messageGroup=null, deliveryTimestamp=null, properties={}}
Message body:messageBody0
Consume message successfully, messageId=01005056C00008017404E0A8AD00000000
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>${rocketmq-client-version}version>
dependency>
当前 ${rocketmq-client-version} 版本为 5.1.3
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class ProducerDemo {
/**
* 生产者分组
*/
private static final String PRODUCER_GROUP = "DEMO_PRODUCER_GROUP";
/**
* 主题
*/
private static final String TOPIC = "TestTopic";
public static void main(String[] args) throws MQClientException {
/*
* 创建生产者,并使用生产者分组初始化
*/
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
/*
* NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876
*/
producer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);
/*
* 发送消息超时时间,默认即为 3000
*/
producer.setSendMsgTimeout(3000);
/*
* 启动生产者,此方法抛出 MQClientException
*/
producer.start();
/*
* 发送消息
*/
for(int i = 1; i <= 2; i++){
try {
Message msg = new Message();
msg.setTopic(TOPIC);
// 设置消息索引键,可根据关键字精确查找某条消息。
msg.setKeys("messageKey");
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
msg.setTags("messageTag");
// 设置消息体
msg.setBody(("messageBody" + i).getBytes());
// 此为同步发送方式
SendResult rs = producer.send(msg);
System.out.printf("%s%n",rs);
}catch (Exception e){
e.printStackTrace();
System.out.println("消息发送失败!i = " + i);
}
}
// 如果生产者不再使用,则调用关闭
producer.shutdown();
}
}
控制台打印结果
SendResult [sendStatus=SEND_OK, msgId=02000001696818B4AAC21C38F4700000, offsetMsgId=AC186C8800002A9F000000009D157758, messageQueue=MessageQueue [topic=TestTopic, brokerName=broker-a, queueId=6], queueOffset=149971]
SendResult [sendStatus=SEND_OK, msgId=02000001696818B4AAC21C38F47E0001, offsetMsgId=AC186C8800002A9F000000009D15784F, messageQueue=MessageQueue [topic=TestTopic, brokerName=broker-a, queueId=7], queueOffset=150007]
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
public class ConsumerDemo {
/**
* 设置消费者分组
*/
public static final String CONSUMER_GROUP = "DEMO_CONSUMER_GROUP";
/**
* 主题
*/
public static final String TOPIC = "TestTopic";
public static void main(String[] args) throws MQClientException {
/*
* 通过消费者分组,创建消费者
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
/*
* NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876
*/
consumer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);
/*
* 指定从哪一个消费位点开始消费 CONSUME_FROM_FIRST_OFFSET 表示从第一个开始
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
/*
* 消费者订阅的主题,和过滤条件
* 我们这里使用 * 表示,消费者消费主题下的所有消息
*/
consumer.subscribe(TOPIC, "*");
/*
* 注册消费监听
*/
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
/*
* 启动消费者.
*/
consumer.start();
System.out.printf("Consumer Started.%n");
// 如果消费者不再使用,关闭
// consumer.shutdown();
}
}
消费者有3中类型,这里的消费者我们使用了 PushConsumer 。后续我们会详解。打印结果如下:
Consumer Started.
ConsumeMessageThread_DEMO_CONSUMER_GROUP_1 Receive New Messages: [MessageExt [brokerName=broker-a, queueId=6, storeSize=247, queueOffset=149971, sysFlag=0, bornTimestamp=1693971094640, bornHost=/192.168.1.1:54112, storeTimestamp=1693971094462, storeHost=/192.168.1.1:10911, msgId=AC186C8800002A9F000000009D157758, commitLogOffset=2635429720, bodyCRC=320426458, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TestTopic', flag=0, properties={CONSUME_START_TIME=1693971094653, MSG_REGION=DefaultRegion, UNIQ_KEY=02000001696818B4AAC21C38F4700000, CLUSTER=DefaultCluster, MIN_OFFSET=122953, TAGS=messageTag, KEYS=messageKey, TRACE_ON=true, MAX_OFFSET=149972}, body=[109, 101, 115, 115, 97, 103, 101, 66, 111, 100, 121, 49], transactionId='null'}]]
ConsumeMessageThread_DEMO_CONSUMER_GROUP_2 Receive New Messages: [MessageExt [brokerName=broker-a, queueId=7, storeSize=247, queueOffset=150007, sysFlag=0, bornTimestamp=1693971094654, bornHost=/192.168.1.1:54112, storeTimestamp=1693971094473, storeHost=/192.168.1.1:10911, msgId=AC186C8800002A9F000000009D15784F, commitLogOffset=2635429967, bodyCRC=168820832, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TestTopic', flag=0, properties={CONSUME_START_TIME=1693971094660, MSG_REGION=DefaultRegion, UNIQ_KEY=02000001696818B4AAC21C38F47E0001, CLUSTER=DefaultCluster, MIN_OFFSET=123009, TAGS=messageTag, KEYS=messageKey, TRACE_ON=true, MAX_OFFSET=150008}, body=[109, 101, 115, 115, 97, 103, 101, 66, 111, 100, 121, 50], transactionId='null'}]]
NameServer是一个简单的 Topic 路由注册中心,支持 Topic、Broker 的动态注册与发现。
主要包括两个功能:
NameServer通常会有多个实例部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,客户端仍然可以向其它NameServer获取路由信息。
Broker主要负责消息的存储、投递和查询以及服务高可用保证。
NameServer几乎无状态节点,因此可集群部署,节点之间无任何信息同步。Broker部署相对复杂。
在官网5.0的文档中NameServer、Broker、Proxy都没有做说明,以上关于NameServer、Broker的说明是4.x的文档说明,(或许是5.0版本2022.9.22才发布,文档没有跟上吧)。
由于没有官方说明,这里只能大胆猜测一下 Proxy 的作用,应该是替代NameServer 和 broker 的一些功能,按照我们示例的写法,以及英文直译它的作用应该是代理网络连接、以及集群下的寻址等功能等。
RocketMQ 中消息传输和存储的顶层容器,用于标识同一类业务逻辑的消息。主题通过TopicName来做唯一标识和区分。一般我们定义不同的主题来区分不同的业务逻辑。
RocketMQ 中按照消息传输特性的不同而定义的分类,用于类型管理和安全校验。 RocketMQ 支持的消息类型有普通消息、顺序消息、事务消息和定时/延时消息。
从5.0版本开始,支持强制校验消息类型,即每个主题Topic只允许发送一种消息类型的消息,这样可以更好的运维和管理生产系统,避免混乱。但同时保证向下兼容4.x版本行为,强制校验功能默认关闭,推荐通过服务端参数 enableTopicMessageTypeCheck 手动开启校验。
队列是 RocketMQ 中消息存储和传输的实际容器,也是消息的最小存储单元。通过QueueId来做唯一标识和区分。RocketMQ 的所有主题都是由多个队列组成。任意一个消息队列在逻辑上都是无限存储,即消息位点会从0到Long.MAX无限增加。
消息是按到达服务端的先后顺序存储在指定主题的多个队列中,每条消息在队列中都有一个唯一的Long类型坐标,这个坐标被定义为消息位点。
队列中最早一条消息的位点为最小消息位点(MinOffset);最新一条消息的位点为最大消息位点(MaxOffset)。
RocketMQ 通过消费位点管理消息的消费进度。每条消息被某个消费者消费完成后不会立即在队列中删除,RocketMQ 会基于每个消费者分组维护一份消费记录,该记录指定消费者分组消费某一个队列时,消费过的最新一条消息的位点,即消费位点。
消息是 RocketMQ 中的最小数据传输单元。生产者将业务数据的负载和拓展属性包装成消息发送到服务端,服务端按照相关语义将消息投递到消费端进行消费。
消息视图是 RocketMQ 面向开发视角提供的一种消息只读接口。通过消息视图可以读取消息内部的多个属性和负载信息,但是不能对消息本身做任何修改。
消息标签是 RocketMQ 提供的细粒度消息分类属性,可以在主题层级之下做消息类型的细分。消费者通过订阅特定的标签来实现细粒度过滤。比如:我们一般将标签设计为业务操作级,如果消息都是下单成功的消息,那么他们的标签为“orderSuccess”,相同标签的消息,调用相同的业务逻辑。这样我们可以减少要创建的主题的个数。
通过设置的消息索引可以快速查找到对应的消息内容。一般 MessageKey 我们会设计为当前消息的内容的唯一关键字,比如订单号、订单id、唯一标识等,这样可以方便我们更精确的查找的与某个业务有关的消息。
订阅关系是 RocketMQ 系统中消费者获取消息、处理消息的规则和状态配置。
主题由多个队列组成,队列示消息的传输和存储的实际容器,RocketMQ 通过无限流式队列来存储消息,消息的顺序性也有队列来保证。
消息是 RocketMQ 的最小传输单元。消息一旦存储不可改变(不可变性),且 RocketMQ 接受到消息会默认持久化到服务端存储文件。
在我们日常的应用开发中,如果不使用中间件异步调用,那么基本上都是通过 RPC 或 HTTP 等直接调用相应接口进行应用间的通信,这就是直接远程调用。比如下图中的订单系统调用库存系统扣减库存的操作:
这种调用方式,订单系统需要等待库存系统正确处理完成,并返回信息之后,订单系统确认无误,然后反馈用户。
就算在整个过程其实参数、流程、事务控制逻辑上都完全没问题的前提下,由于 request 、response 是远程通信,比如出现网络波动、断联,导致 request 或 response 超时或丢包,也会导致请求失败
如果库存系统有内容需要重新部署启动,这期间整个请求无法使用,两个系统间的耦合度高。而且我们此处只是最基本的两个应用,一般的大型应用可能有很多个子系统间需要相互调用,这样带来的耦合度、维护成本等就更高了。
异步调用方式所有的应用都只跟中间件进行通信,这样结构简单,每个应用之间都没有直接关联,耦合度低,各个应用可以独立启停和部署等,相互并不影响。当然 MQ 的作用远不于此。