一、RocketMQ的介绍
RocketMQ是阿里巴巴开源的一个消息中间件,一个分布式消息队列系统,具有高可用性、高性能、可扩展性和可靠性等特点,可以在大规模分布式系统中处理海量的消息数据。在阿里内部历经了双十一等很多 高并发场景的考验,能够处理亿万级别的消息。还可以与各种应用程序和框架集成。RocketMQ还提供了丰富的管理和监控工具,方便用户管理和监控消息队列系统的运行状态。2016年开源后捐赠给Apache,现在是Apache的一个顶级项目。
二、RocketMQ快速实战
2.1 RocketMQ的下载
我们可以登录RocketMQ
官网:https://rocketmq.apache.org/
GitHub地址:https://github.com/apache/rocketmq
我本文中用到的RocketMQ版本是4.9.4
2.2 启动NameServer
然后把下载的安装包解压后上传Linux服务器上,在我们安装软件的目录下执行下面命令
### 启动namesrv
$ nohup sh bin/mqnamesrv &
然后通过下面命令验证是否启动成功
### 验证namesrv是否启动成功
```shell
$ tail -f ~/logs/rocketmqlogs/namesrv.log
The Name Server boot success...
###或者用less nohup.out,看出日志后按 q 退出
$ less nohup.out
这是安装了JDK并且是使用下面命令进行配置
$ vi ~/.bash_profile
export ROCKETMQ_HOME=/home/oper/app/rocketmq/rocketmq-all-4.9.4-bin-release
export JAVA_HOME=/home/oper/app/jdk/jdk1.8.0_162
export NAMESRV_ADDR=localhost:9876
PATH=$ROCKETMQ_HOME/bin:$JAVA_HOME/bin:$PATH:$HOME/bin
从上面两个图片能够看出是启动成功了
2.3 启动Broker
##如果上面的 ~/.bast_profile里配置了端口号,可以这样启动
$ nohup sh bin/mqbroker &
//如果没有配置 用这个命令
$ nohup sh bin/mqbroker -n localhost:9876 &
###用less nohup.out,看日志后按 q 退出
$ less nohup.out
这样输出则证明启动成功
2.4 消息的发送和消费
##消息发送
$ sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
##消息消费
$ sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
2.5 关闭NameServer、Broker
$ sh bin/mqshutdown broker
$ sh bin/mqshutdown namesrv
2.6 搭建控制台
RocketMQ源代码中并没有提供控制台,但是有一个Rocket的社区扩展项目中提供 了一个控制台,地址: https://github.com/apache/rocketmq-dashboard
下载下来后,解压并进入对应的目录,使用maven进行编译
mvn clean package -Dmaven.test.skip=true
那我们可以在jar包的当前目录下增加一个application.yml文件,覆盖jar包中默认的 一个属性:
rocketmq:
config:
namesrvAddrs:
##RocketMQ安装地址,如果搭建集群的话可以填写多个
- ip地址:9876
然后执行,然后就可以通过访问192.168.112.78:8080
$ java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar
备注:
如果想搭建集群的话,可以通过conf目录下的配置文件来实现,文件中有多种选择。
三、RocketMQ的基本概念
下图是一个RocketMQ集群模式下一个基础概念总结
我们可以从上图中发现,有Name server、broker、message queue、producer、consumer,但是他们到底是什么意思?他们分别在RocketMQ的整体框架下分别发挥了什么作用?下面我会对他们的作用进行一些介绍。
3.1 NameServer
NameServer是一个简单的路由注册中心,支持Topic和Broker的动态注册和发现。作用主要包括两点:
- Broker管理:Broker会把集群信息注册到NameServer上,NameServer会把这些信息记录下来,作为路由信息的基本数据。然后还会提供心态检测机制,检查Broker是否还存活
- 路由信息:因为NameServer存放的有Broker集群的基本信息(例如有哪些Borker可用,以及Broker下的队列信息),所以Producer和Consumer就可以通过NameServer知道整个Broker集群信息,生产者生产的信息就可以知道往哪个Broker下的哪个Message queue队列中投递消息,消费者也可以通过自己的配置信息去哪个broker下哪个队列中去拉取消息进行消费。
NameServer是无状态的,且NameServer集群下各个NameServer是互相不通信的,没有任何信息同步操作。每个Broker都会与NameServer集群下的每一个NameServer节点建立长链接,然后会向每一个NameServer注册自己的路由信息,所以每个NamerServer下都保存了Broker集群的完整路由信息。当某个NameServer节点挂掉后,消费者和生产者也可以通过其他NameServer获取到Broker的完整信息,所以大部分情况下NameServer通常会部署多个实例
3.2 Broker代理服务器
主要负责生产者生产消息的存储,为消费者拉取信息提供查询,也会存储消息相关的一些其他数据,例如Topic信息、队列信息、消费进度偏移量等。
而且Broker是服务高可用的保证:相对于NameServer来说,Broker的部署会相对于复杂一些。
- 普通主从集群模式:
这种集群下会给每个节点分配特定的角色,分为Master和Slave,一个Master可以对应多个Slave但是一个Slave只能对应一个Master,master负责响应生成存储消息的请求,并存储消息。slave负责储存从主节点同步过来的数据(可以选择同步或者异步),我们可以通过配置conf目录下的配置文件来选择如何配置。我们可以通过指定相同的BrokerName,不同的BrokerId来制定一个Broker集群。BrokerId中0代表master,非0代表slave。但是这种模式下的弊端就是各个节点的角色无法切换,如果一个master挂掉后,这一组的Broker就不可用了 - Dledger高可用集群:
这个是RocketMQ4.5版本后提供的一种集群高可用模式,这个模式下会随机选举出一个节点作为master,当master节点挂了后,会通过Raft机制然后会从slave节点中选择一个节点升级为master。
3.3 Topic
表示一类消息的集合,每个主题包含若干条消息,但是每个消息只能属于一个Topic,且Topic只是一个逻辑概念并不负责存储消息,Topic是RocketMQ进行消息订阅的基本单位
3.4 Message queue
因为Topic只是一个逻辑概念,并不负责存储消息。同一个Topic下的消息会分片保存到不同的Broker上,而这样一个分片保存的单位就叫做Message queue,MessageQueue是一个具有FIFO特性的队列结构,生产者发送消息与消费者消费消息的最小单位
3.5 生产者 Producer
就是消息的发送者,负责生产消息。会与NameServer集群中的其中一个节点建立长连接,定期从NameServer获取Broker路由信息(当前Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在Broker的Master建立长连接,定时向Master发送心跳),然后把生产的消息发送到broker服务器,发送消息方式可以选择同步发送、异步发送、单向发送等,同步发送和异步发送方式均需要Broker返回确认信息,单向发送不需要。
生产者中,会把同一类Producer组成一个集合,叫做生产者组。同一组的 Producer被认为是发送同一类消息且发送逻辑一致。
3.6 消费者 Consumer
负责消费消息,Consumer会与NameServer集群中的某一个节点建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Broker的Master和Slave都建立长连接,且定时向Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息,然后开始消费。从用户的角度来看消费消息可以分为两种方式,拉取式和推送式消费:
- 拉取式消费的应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
- 推动式消费模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高
消费者会把同一类Consumer组成一个集合叫做消费者组,消费者组里的Consumer通常消费同一类消息且消费逻辑一致。消费者组的消费者实例必须订阅相同的Topic,RocketMQ支持两种消息模式:集群消费和广播消费 - 集群模式:在集群模式下同一个消费者组中的Consumer实例是负载均衡消费的
- 广播模式:在这个模式下的每一个消息会被每一个Consumer实例消费,所以这种模式推荐在通知推送、配置同步类等流量比较小的场景下使用。
四、RocketMQ原生API使用
上面我们应该大致知道一些生产者的作用,用来发送消息到订阅的Topic所在的Broker,那么发送的消息到底是由哪些基础概念组成的?消息、Tag、keys
- 消息
- topic:表示要发送的消息的主题。
- body:表示消息的存储内容
- properties:表示消息属性
- transactionId:会在事务消息中使用
- Tag:同一个Topic下的二级分类,如果消息对应的Topic是同一个,我们可以通过不同的Tag来区分消息。
- Keys:我们可以业务侧为每一个消息设置一个唯一的标识keys字段,可以方便我们后续处理消息丢失的情况,可以快速定位到问题。因为Broker会为每个消息创建索引(哈希索引)。我们可以通过topic、key查询到这条信息,以及这条信息被谁消费了,但是因为是哈希索引,我们要尽量避免潜在的哈希冲突,所以我们要选择唯一性的属性来作为我们的key,例如订单ID。
消息可以设置的属性如下:
字段名 | 解释说明 | 默认值 | 是否必填 |
---|---|---|---|
Topic | 消息所属Topic名称 | null | 是 |
Body | 消息体 | null | 是 |
Tags | 标签,方便过滤消息时使用,目前只支持每个消息配置一个 | null | 否 |
keys | 唯一标识,可以选择代表业务唯一属性 | null | 否 |
Flag | 完全由应用来设置,RocketMQ 不做干预 | 0 | 否 |
DelayTimeLevel | 延时级别,默认0延时,大于0是一段特定时间后才会被消费,级别可以根据业务进行配置 | 0 | 否 |
WaitStoreMsgOK | 表示消息是否在服务器落盘后才返回应答。 | true | 否 |
4.2 生产者发送消息实战
第一步我们首先要在idea上创建一个Maven项目,然后添加依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.4</version>
</dependency>
4.2.1 普通消息
发送消息的方式有同步、异步、单向三种方式
- 同步方式:同步方式是最常用的方式,通常是发送一条消息后早收到服务端同步响应之后,然后再发送下一条消息,可以用于一些比较重要的场景,例如:短信发送
- 异步方式:发送一条消息到服务端后不需要等到服务端响应,就可以继续发送下一条消息,但是需要注册回调接口,然后通过回调接口获取服务端的响应,经常用于一些链路执行过长的场景
- 单向方式:不需要等待服务端响应也不需要注册回调接口,就可以继续发送下一条消息,耗时非常短,通常适用于耗时短但是可靠性要求没那么高的场景。
发送消息的步骤大概如下:
- 创建生产者:普通消息通常都是DefaultProducer,然后设置一个produceGroup的名字
- 注册NameServer地址:可以setNameServer(),如果是多个中间以;分开
- 构建消息体:Topic、tag、keys、消息体等信息
- 发送消息:通过send()方法发送消息
4.2.1.1 同步方式
public class SyncProducer {
public static void main(String[] args) {
//创建DefaultMQProducer
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("producerName");
//设置NameServer地址
defaultMQProducer.setNamesrvAddr("192.168.112.78:9876");
try {
defaultMQProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
Message message = new Message();
message.setTopic("TopicTest");
message.setKeys("OrderId"+i);
message.setBody("test".getBytes());
try {
SendResult sendResult = defaultMQProducer.send(message);
System.out.println(sendResult);
} catch (MQClientException e) {
e.printStackTrace();
} catch (RemotingException e) {
e.printStackTrace();
} catch (MQBrokerException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
defaultMQProducer.shutdown();
}
}
发送结果如下:SEND_OK代表发送成功
然后去服务器上看一下消费者是否消费到数据:
4.2.1.2 异步方式
public class AsyncProducer {
public static void main(String[] args) {
//创建DefaultMQProducer
DefaultMQProducer asyncMQProducer = new DefaultMQProducer("AsyncProducerName");
//设置NameServer地址
asyncMQProducer.setNamesrvAddr("192.168.112.81:9876");
try {
asyncMQProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
final CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
try {
Message message = new Message("TopicTest",
"tags",
"OrderId"+i,
"test".getBytes(RemotingHelper.DEFAULT_CHARSET));
//异步方式,要注册回调方法
asyncMQProducer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.println(sendResult.getSendStatus()+sendResult.getMsgId());
}
@Override
public void onException(Throwable throwable) {
countDownLatch.countDown();
System.out.println(throwable.getMessage());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
try {
countDownLatch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
asyncMQProducer.shutdown();
}
}
运行结果如下:
4.2.1.3 单向方式
public class OneWayProducer {
public static void main(String[] args) {
//创建DefaultMQProducer
DefaultMQProducer oneWayMQProducer = new DefaultMQProducer("oneWayMQProducer");
//设置NameServer地址
oneWayMQProducer.setNamesrvAddr("192.168.112.78:9876");
try {
oneWayMQProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
try {
Message message = new Message("TopicTest",
"tags",
"OrderId"+i,
"test".getBytes(RemotingHelper.DEFAULT_CHARSET));
//没有和服务端的回应,所以发生失败了是不能进行重试的
oneWayMQProducer.sendOneway(message);
} catch (Exception e) {
e.printStackTrace();
}
}
oneWayMQProducer.shutdown();
}
}
4.2.2 顺序消息
顺序消息是对生产者生产消息和消费者消费消息的顺序有严格要求的。
但是RocketMQ并不能保证所有消息的有序性,因为默认情况下一个Topic下的消息会发送到不同的Message queue上,消费者也会从不同的Message queue上拉取消息,这种情况下是不能保证有序的。
RocketMQ的有序性是要保证Producer、Broker、Consumer三者的有序性,严格按照FIFO方式来对消息进行处理,我们可以从生产者把消息发送到同一个Message queue上,所以只能有一个生产者,因为多个生产者的生产的消息是无法有序的,并且生成者发送消息不能选择多线程的方式。然后消费者可以注册一个MessageListenerOrderly(),在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队列来取的。
生产者代码:
public class OrderMessageProducer {
public static void main(String[] args) {
DefaultMQProducer orderMessageProducer = new DefaultMQProducer("OrderMessageProducer");
orderMessageProducer.setNamesrvAddr("192.168.112.78:9876");
try {
orderMessageProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 100; i++) {
//代表一类消息
int orderId = i & 10;
try {
Message message = new Message("TopicTest",
tags[i % tags.length],
"KEYS"+i,
"BODY".getBytes(RemotingHelper.DEFAULT_CHARSET));
// 设置一个MessageQueueSelector队列选择器
SendResult sendResult = orderMessageProducer.send(message, new MessageQueueSelector() {
//list 消息队列列表,args 我们传入的orderId
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object args) {
Integer id = (Integer)args;
//这样可以确保相同orderId的消息总是发送到相同的队列,实现消息的顺序性
return list.get( id%list.size() );
}
}, orderId);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
消费者代码:
public class OrderMessageConsumer {
public static void main(String[] args) {
DefaultMQPushConsumer orderMessageConsumer = new DefaultMQPushConsumer("OrderMessageConsumer");
//设置消费者的消费起点。这里设置为从队列的第一个偏移量开始消费
orderMessageConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
orderMessageConsumer.setNamesrvAddr("192.168.112.78:9876");
try {
//并指定只消费带有"TagA"、"TagC"或"TagD"标签的消息
orderMessageConsumer.subscribe("TopicTest", "TagA || TagC || TagD");
} catch (MQClientException e) {
e.printStackTrace();
}
//注册一个顺序消息监听器。一个队列一个队列获取消息,这意味着消息会按照发送的顺序被消费
orderMessageConsumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> messageExtList, ConsumeOrderlyContext context) {
//设置自动提交消费进度
context.setAutoCommit(true);
System.out.printf("%s 接受的消息: %s %n", Thread.currentThread().getName(), messageExtList);
this.consumeTimes.incrementAndGet();
//这段代码的含义是模拟了不同的处理逻辑,是决定消费成功还是代表挂起队列
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
//暂时挂起当前队列
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
try {
orderMessageConsumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
}
4.2.3 延迟消息
延时消息是生产者生产的消息发送到服务端后,并不希望马上被消费,而是希望延迟一段时间后才被消费。延迟消息的级别如下图所示(从官网偷截的)
生产者代码:
public class ScheduledMessageProducer {
public static void main(String[] args) {
DefaultMQProducer scheduledMessageProducer = new
DefaultMQProducer("scheduledMessageProducer");
scheduledMessageProducer.setNamesrvAddr("192.168.112.78:9876");
try {
scheduledMessageProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
try {
Message message = new Message("TopicTest",
"tags",
"OrderId" + i,
"test".getBytes(RemotingHelper.DEFAULT_CHARSET));
//设置延迟级别
message.setDelayTimeLevel(3);
SendResult sendResult = scheduledMessageProducer.send(message);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
}
scheduledMessageProducer.shutdown();
}
}
消费者代码:
public class ScheduledMessageConsumer {
public static void main(String[] args) {
DefaultMQPushConsumer scheduledMessageConsumer = new
DefaultMQPushConsumer("scheduledMessageConsumer");
scheduledMessageConsumer.setNamesrvAddr("192.168.112.78:9876");
try {
scheduledMessageConsumer.subscribe("TopicTest", "*");
} catch (MQClientException e) {
e.printStackTrace();
}
scheduledMessageConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
for (MessageExt message : messages) {
System.out.printf("接受的消息[消息ID=%s %d ms]\n", message.getMsgId(),
System.currentTimeMillis() - message.getStoreTimestamp());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
try {
scheduledMessageConsumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
}
4.2.4 批量消息
可以把消息整合到一批后在进行发送,可以增加吞吐率,并减少API和网络调用次数
public class SimpleBatchProducer {
public static void main(String[] args) {
DefaultMQProducer batchProducer = new DefaultMQProducer("batchProducer");
batchProducer.setNamesrvAddr("192.168.112.78:9876");
try {
batchProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
ArrayList<Message> messages = new ArrayList<Message>();
messages.add(new Message("TopicTest", "tag1", "1", "test1".getBytes()));
messages.add(new Message("TopicTest", "tag2", "2", "test".getBytes()));
messages.add(new Message("TopicTest", "tag3", "3", "test3".getBytes()));
try {
SendResult sendResult = batchProducer.send(messages);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2.5 事务消息
- 事务消息是在分布式系统中 保证最终一致性的两阶段提交的消息实现。他可以保证本地事务执行与消息发送两 个操作的原子性,也就是这两个操作一起成功或者一起失败。
- 对于一些数据具有强一致性场景的情况下,例如上游订单付款成功后,下游才可以进行积分变更、物流发货、购物车状态变更。这种类似场景下可以选择事务消息
- RocketMQ中普通消息不能像数据库事务一样具有提交、回滚、统一协调能力(原子性、顺序性、一致性),RocketMQ为了实现事务消息,是把本地事务与两阶段提交相结合来实现的,生产者先开始生产的消息发送到Broker后,并不会马上让消费者消费,而是先设置一个半事务状态,然后根据根据本地事务的执行结果,来决定对消息进行提交和回滚,如果生产者重启或者其他原因导致Broker长时间没有得到第二阶段的结果,Broker 端会通过扫描发现某条消息长期处于“半事务消息”时,那么Broker就会进行回查,向生产者询问这个消息的最终状态(commit/roolback),回查的次数默认最多不超过15次,如果想要修改的话可以修改Broker配置文件中的transactionCheckMax,默认情况下,如果回查超过这个次数就会把这个消息丢弃掉,如果想修改这个行为可以实现AbstractTransactionCheckListener类来修改这个行为。
事务消息机制的关键是在发送消息时,会将消息转为一个half半消息,并存入RocketMQ内部的一个 RMQ_SYS_TRANS_HALF_TOPIC 这个Topic,这样对消费者是不可见的。再经过一系列事务检查通过后,再将消息转存到目标Topic,这样对消费者就可见了。
生产者代码:
public class TransactionProducer {
public static void main(String[] args) {
/**
*事务消息不能在使用默认的DefaultMQProducer,而应该使用TransactionMQProducer
* 并且生产者组的名字不能随意设置,因为如果发送消息的生产者挂掉后,Broker会通过
* 同一个生产者组的其他生产者来回查本地事务执行结果
*/
TransactionMQProducer transactionProducer = new
TransactionMQProducer("transactionProducer");
transactionProducer.setNamesrvAddr("192.168.112.78:9876");
TransactionListener transactionListener = new TransactionListenerImpl();
//注册事务监听者,内部实现了本地事务执行结果的方法,还有回查本地事务的方法
transactionProducer.setTransactionListener(transactionListener);
try {
transactionProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message message = new Message("TopicTest",tags[i%tags.length],
"keys"+i,
"transaction".getBytes(RemotingHelper.DEFAULT_CHARSET)
);
TransactionSendResult sendResult = transactionProducer.sendMessageInTransaction(message, null);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
}
transactionProducer.shutdown();
}
static class TransactionListenerImpl implements TransactionListener {
/**
* 本地事务执行
* @param message 消息
* @param o 参数
* @return 结果 COMMIT_MESSAGE(commit),ROLLBACK_MESSAGE(rollback),UNKNOW(暂时无法判断状态,
* 等待固定时间以后Broker端根据回查规则向生产者进行消息回查);
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
String tags = message.getTags();
if (StringUtils.isNotBlank(tags)){
switch (tags){
case "TagA":
return LocalTransactionState.COMMIT_MESSAGE;
case "TagB":
return LocalTransactionState.ROLLBACK_MESSAGE;
case "TagC":
case "TagD":
return LocalTransactionState.UNKNOW;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
/**
* 回查本地事务结果
* @param messageExt 消息体
* @return 回查结果
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String tags = messageExt.getTags();
if (StringUtils.isNotBlank(tags)){
switch (tags){
case "TagC":
return LocalTransactionState.COMMIT_MESSAGE;
default:
return LocalTransactionState.UNKNOW;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
}
4.3 消费者基本概念
如果生产者发送消息到某一个Topic的话,如果需要Topic下的消息被消费,那么就需要创建对应的消费者进行消费。
4.3.1 消费者组
如果多个消费者设置了相同的Consumer Group,我们认为这些消费者在同一个消费组内。
对于消费者组来说,RocketMQ有两种消费方式:
- 集群模式:如果使用集群消费的话,RocketMQ认为任何一个消息只需要被消费者组内任何一个消费者消费就可以,这时消息是选择什么样的策略分配到消费者的,平均分配、一致性hash分配等。在这个模式下例如如果是平均分配的策略的话,增加消费者是可以提高消费速度的。但是如果消费者的数量超过Message queue的数量的话,那么增加消费这也没什么作用。
- 广播模式:会把每条消息推送给消费者组的所有消费者,所以在广播模式下增加或减少消费者并不会增加或者减少消费速度。
4.3.2 消费位点
如上图所示每个Message queue都会记录自己的最小位点、最大位点。针对于消费者组还有消费位点的概念,在集群模式下,是由消费者端提交给服务端保存的,如果是广播模式下,消费位点是由消费者端自己保存的。
一般情况下消费位点都是可以正常更新的,但是如果消费者发生崩溃或者增加了新的消费者加入群组,就会触发重平衡,消费者就可以被分配到新的队列,然后消费者通过消费位点开始消费消息,但是由于消费者端提交消费位点不是实时的,就有可能发生少量消息重复。
4.3.3 推、拉、长轮询
MQ的消费模式可以分为两种
- Push推模式:服务端主动推送消息到客户端,这种及时性比较好,但是如果客户端流控没有做好的话,可能突然大量消息推送到客户端的话,就会导致客户端消息堆积甚至崩溃
- Pull拉模式:客户端主动从服务端拉取数据,优点是客户端可以依据自己的消费能力进行消费,但拉取的频率也需要用户自己控制,拉取频繁容易造成服务端和客户端的压力,拉取间隔长又容易造成消费不及时。
4.3.3.1 push消费
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
// 初始化consumer,同一个消费组的ConsumerGroupName是相同的,这是判断消费者是否属于同一个
//消费组的重要属性。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("pushConsumer");
// 设置NameServer地址
consumer.setNamesrvAddr("localhost:9876");
//订阅一个或多个topic,并指定tag过滤条件,这里指定*表示接收所有tag的消息
consumer.subscribe("TopicTest", "*");
//consumer.subscribe("TopicTest", "TagA || TagC || TagD");也可以这样写过滤条件
//注册回调接口来处理从Broker中收到的消息,MessageListenerConcurrently
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
// 返回消息消费状态,ConsumeConcurrentlyStatus.CONSUME_SUCCESS为消费成功
//RECONSUME_LATER表示消费失败,一段时间后再重新消费。
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动Consumer
consumer.start();
}
}
- 怎么设置集群或者广播,push模式下默认是集群模式,我们可以通过
//集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
//广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
- 上文注册的回调接口中MessageListenerConcurrently是并发处理的,这样是不能保证消费消息的有序性,如果想要有序的话可以选择这个MessageListenerOrderly
//注册一个顺序消息监听器。一个队列一个队列获取消息,这意味着消息会按照发送的顺序被消费
orderMessageConsumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> messageExtList, ConsumeOrderlyContext context) {
//设置自动提交消费进度
context.setAutoCommit(true);
return ConsumeOrderlyStatus.SUCCESS;
}
});
4.3.3.2 消息过滤
消费者订阅Topic时若未设置过滤条件,无论消息发送时是否有设置过滤属性,Topic中的所有消息都将被投递到消费端进行消费。
过滤方式 | 说明 | 场景 |
---|---|---|
Tag过滤 | 消费者设置的过滤Tag要与生产者设置的tag保持一致,然后消费者就会消费配置tag的消息consumer.subscribe(“TagFilterTest”, “TagA”); | 适合一些简单的过滤场景,同一个Topic进行二次过滤 |
SQL92过滤 | 发送者设置Tag或消息属性,消费者订阅满足SQL92过滤表达式的消息被投递给消费端进行消费 | 复杂过滤场景。一条消息支持设置多个属性,可根据SQL语法自定义组合多种类型的表达式 |
SQL92过滤
SQL92过滤是在消息发送时设置消息的Tag或自定义属性,消费者订阅时使用SQL语法设置过滤表达式,根据自定义属性或Tag过滤消息。
Tag属于一种特殊的消息属性,在SQL语法中,Tag的属性值为TAGS。 开启属性过滤首先要在Broker端设置配置enablePropertyFilter=true,该值默认为false。
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"and (a is not null and a between 0 and 3)"));
4.3.3.2 消息重试和死信队列
- 消息重试:如果消息消费失败后,RocketMQ会在隔一段时间后再次对消息进行消费,消息重试只针对集群消费模式生效;广播消费模式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
- 并发消费和顺序消费方式也不一样,顺序消费为了不打乱消费消息的顺序,都是在本地进行重试。而并发消费是把消费失败的消息重新投递到服务端,然后再次等待服务端投递过来
消费类型 | 重试间隔 | 最大重试次数 |
---|---|---|
顺序消费 | 间隔时间可通过自定义设置,SuspendCurrentQueueTimeMillis | 最大重试次数可通过自定义参数MaxReconsumeTimes取值进行配置。该参数取值无最大限制。若未设置参数值,默认最大重试次数为Integer.MAX |
并发消费 | 间隔时间根据重试次数阶梯变化,取值范围:1秒~2小时。不支持自定义配置 最大重试次数可通过 | 自定义参数MaxReconsumeTimes取值进行配置。默认值为16次,该参数取值无最大限制,建议使用默认值 |
- 最大重试次数:消息消费失败后,可被重复投递的最大次数
consumer.setMaxReconsumeTimes(10);
- 重试间隔:消息消费失败后再次被投递给Consumer消费的间隔时间,只在顺序消费中起作用。
consumer.setSuspendCurrentQueueTimeMillis(5000);
- 死信队列:如果达到重试次数后,还没有成功,将会放到死信队列中,死信队列的消息将不会再被消费。可以利用RocketMQ Admin工具或者RocketMQ Dashboard上查询到对应死信消息的信息。
五、SpringBoot整合RocketMQ
创建一个MAVEN项目,引入依赖:
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
</dependencies>
创建一个配置类:application.properties
#NameServer地址
rocketmq.name-server=ip地址:9876
#默认的消息生产者组
rocketmq.producer.group=springBootGroup
创建一个启动类:
@SpringBootApplication
public class RocketMqApplication {
public static void main(String[] args) {
SpringApplication.run(RocketMqApplication.class,args);
}
}
创建一个生产者类:
@Component
public class SpringProducer {
@Resource
RocketMQTemplate rocketMqTemplate;
public void sendMessage(String topic, String message){
rocketMqTemplate.convertAndSend(topic,message);
}
}
创建一个消费者类:
@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TopicTest")
public class SpringConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String s) {
System.out.println(s);
}
}
创建一个Controller类:
@RestController
public class MqSendController {
@Resource
private SpringProducer springProducer;
@Value("${producer.topic}")
String topic;
@GetMapping("/send")
public void sendMessage(@RequestParam("message") String message){
springProducer.sendMessage(topic,message);
}
}
然后访问:http://localhost:8080/send?message=test,这只是一个简单的demo,像上面的顺序消息、事务等,可以自己私下多加练习。