MQ基础
MQ 的作用
解耦
引入队列前, 某些服务依赖于上一个服务的调用接口, 只有某个服务完成才能执行, 引入队列后, 将服务的接口放入到队列中, 下游的服务需要就去队列中调用即可
异步
左图, 当服务的链路长的时候, 响应的开始后就变长, 右图引入消息队列, 将更新订单状态之外的行为放入队列中, 使用异步处理, 降低响应时间
削峰填谷
当某些时刻流量激增, 可以将请求放入队列中, 当不繁忙的时候, 就进行处理
mq之间对比以及选择
-
RabbitMQ
- 优点:轻量,迅捷,容易部署和使用,拥有灵活的路由配置
- 缺点:性能和吞吐量不太理想,不易进行二次开发
-
RocketMQ
- 优点:性能好,高吞吐量,稳定可靠,有活跃的中文社区
- 缺点:和其它语言兼容性上不是太好
-
Kafka
- 优点:拥有强大的性能及吞吐量,兼容性很好
- 缺点:由于“攒一波再处理”导致延迟比较高
Rocket MQ作为重点, 所以对Rocket MQ 进行一个单独的优缺点补充
RocketMQ优点:
- 单机吞吐量:十万级
- 可用性:非常高,分布式架构
- 消息可靠性:经过参数优化配置,消息可以做到0丢失
- 功能支持:MQ功能较为完善,还是分布式的,扩展性好
- 支持10亿级别的消息堆积,不会因为堆积导致性能下降
- 源码是Java,方便结合公司自己的业务二次开发
- 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况
- RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择RocketMQ
RocketMQ缺点:
- 支持的客户端语言不多,目前是Java及c++,其中c++不成熟
- 没有在MQ核心中去实现JMS等接口,有些系统要迁移需要修改大量代码
Rocket MQ 的基本核心概念
- NameServer:可以理解为是一个注册中心,主要是用来保存topic路由信息,管理Broker。在NameServer的集群中,NameServer与NameServer之间是没有任何通信的。
- Broker:核心的一个角色,主要是用来保存topic的信息,接受生产者产生的消息,持久化消息。在一个Broker集群中,相同的BrokerName可以称为一个Broker组,一个Broker组中,BrokerId为0的为主节点,其它的为从节点。BrokerName和BrokerId是可以在Broker启动时通过配置文件配置的。每个Broker组只存放一部分消息。
- Producer(生产者):生产消息的一方就是生产者
- 生产者组:一个生产者组可以有很多生产者,只需要在创建生产者的时候指定生产者组,那么这个生产者就在那个生产者组
- Consumer(消费者):用来消费生产者消息的一方
- 消费者组:跟生产者一样,每个消费者都有所在的消费者组,一个消费者组可以有很多的消费者,不同的消费者组消费消息是互不影响的。
- topic(主题) :消息的目的地,往哪里发送消息,按照不同的业务划分不同的主题里面,生产者在发送消息的时候需要指定发到哪个topic下,消费者消费消息的时候也需要知道自己消费的是哪些topic底下的消息。
- Tag(标签) :比topic低一级,可以用来区分同一topic下的不同业务类型的消息,发送消息的时候也需要指定, 在消费者端可以通过表达式对消息进行过滤
- MessageQueue(消息队列):实际存储消息的数据结构,一个主题下有多个多队列
- 消息(Message): 存储了以前同步调用传递的参数, 需要把参数封装到Message对象
执行流程
RocketMQ大致的工作流程:
- Broker启动的时候,会往每台NameServer(因为NameServer之间不通信,所以每台都得注册)注册自己的信息,这些信息包括自己的ip和端口号,自己这台Broker有哪些topic等信息。
- Producer在启动之后会跟会NameServer建立连接,定期从NameServer中获取Broker的信息,当发送消息的时候,会根据消息需要发送到哪个topic去找对应的Broker地址,如果有的话,就向这台Broker发送请求;没有找到的话,就看根据是否允许自动创建topic来决定是否发送消息。
- Broker在接收到Producer的消息之后,会将消息存起来,持久化,如果有从节点的话,也会主动同步给从节点,实现数据的备份
- Consumer启动之后也会跟会NameServer建立连接,定期从NameServer中拉去Broker和对应topic的信息,然后根据自己需要订阅的topic信息找到对应的Broker的地址,然后跟Broker建立连接,获取消息,进行消费
环境搭建
RocketMQ中有NameServer、Broker、生产者、消费者四种角色。而生产者和消费者实际上就是业务系统,所以这里不需要搭建,真正要搭建的就是NameServer和Broker
- Linux服务器
- JDK安装
- 采用1.8版本的JDK
- RocketMQ
- 采用的是rocketmq4.4
# 新建目录, 将mq上传
mkdir /opt/RocketMQ
# 将rocketmq-all-4.4.0-bin-release.zip 上传到 /opt/RocketMQ
# 使用解压命令进行解压
cd /opt/RocketMQ
unzip /opt/RocketMQ/rocketmq-all-4.4.0-bin-release.zip
# 软件文件名重命名
mv /opt/RocketMQ/rocketmq-all-4.4.0-bin-release/ /opt/RocketMQ//rocketmq4.4/
# 将Java 和 RocketMQ 添加到环境变量中
# vim /etc/profile
export JAVA_HOME=/opt/JDK/jdk8
export ROCKETMQ_HOME=/opt/RocketMQ/rocketmq4.4
export PATH=$JAVA_HOME/bin:$ROCKETMQ_HOME/bin:$PATH
# 刷新配置
source /etc/profile
启动RocketMQ
# 修改脚本中的JVM相关参数,修改文件如下
vi /opt/RocketMQ/rocketmq4.4/bin/runbroker.sh
vi /opt/RocketMQ/rocketmq4.4/bin/runserver.sh
# 修改启动参数配置
# JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
# 启动NameServer
# 1.启动NameServer
nohup sh mqnamesrv &
# 2.查看启动日志
tail -f ~/logs/rocketmqlogs/namesrv.log
启动Broker
# 1.启动Broker
nohup sh mqbroker -n 192.168.132.98:9876 -c /opt/RocketMQ/rocketmq4.4/conf/broker.conf &
# 2.查看启动日志
tail -f ~/logs/rocketmqlogs/broker.log
关闭RocketMQ
# 关闭nameserver:
sh mqshutdown namesrv
# 关闭broker
sh mqshutdown bro ker
安装rocketmq-console可视化
-
将rocketmq-console上传到/opt/RocketMQ/rocketmq-console目录
-
使用命令启动
-
java -jar /opt/RocketMQ/rocketmq-console/rocketmq-console-ng-1.0.1.jar &
-
-
访问RocketMQ可视化页面
- http://192.168.132.98:9999/#/message
RocketMQ 使用的消息模型
前置知识-消息队列的消息模型
消息队列有两种模型:队列模型和发布/订阅模型。
队列模型
这是最初的一种消息队列模型,对应着消息队列“发-存-收”的模型。生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,也就是说每条消息只能被一个消费者消费。
发布/订阅模型
如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。解决的方式就是发布/订阅模型。
在发布 - 订阅模型中
-
三个角色
- 发布者(Publisher): 消息的发送方
- 订阅者(Subscriber): 消息的接收方
- 主题(Topic): 服务端存放消息的容器
-
执行流程
发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。
“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。
队列 和 发布/订阅 模型之前对比
生产者就是发布者,
队列就是主题,
消费者就是订阅者,无本质区别。
唯一的不同点在于:一份消息数据是否可以被多次消费
Rocket MQ消息模型
结论: Rocket MQ 使用的是发布/订阅模型
Rocket MQ 消息的结构
Message
Q: 什么是Message?
A: Message就是指要传输的信息, 例如下游的日志服务需要用到订单的id, 那么就可以将订单的id放在Message中, 然后放入队列, 然后下游的服务就可以通过队列获取到订单那的id
Q: 消息和主题之间的关系怎么样的?
A: 一个消息必须要有一个主题, 一个主题下可以有多个消息, 也就收消息和主题之间的关系是一对多. 可以将消息理解成信件, 那么主题就是一个邮寄的地址
Q: Tag又是什么?
A:Tag是比主题更加细微的一个地址, 一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 Key 并在 Broker 上查找此消息以便在开发期间查找问题。
Topic
Topic(主题)可以看做消息的归类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic
Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。
一个 Topic 也可以被 0个、1个、多个消费者订阅。
Tag
Tag(标签)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。
标签有助于保持你的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。
Group
RocketMQ中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被Consumer Group1消费过,也会再给Consumer Group2消费。
消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。默认情况,如果一条消息被消费者Consumer1消费了,那同组的其他消费者就不会再收到这条消息。
组和组之间不影响, 组内消费者影响
Queue
Message Queue(消息队列),一个 Topic 下可以设置多个消息队列,Topic 包括多个 Message Queue ,如果一个 Consumer 需要获取 Topic下所有的消息,就要遍历所有的 Message Queue。
RocketMQ还有一些其它的Queue——例如ConsumerQueue。
Offset
在Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。
也可以这么说,Queue
是一个长度无限的数组,Offset 就是下标。
RocketMQ 的消费模型
消息发送方式
同步消息
可靠性高, 自带消息重发
异步消息
发送消息的代码很快,适合对响应时间比较敏感的场景,但是有可能出现,业务已经执行完了,但是消息回调发现消息没有存储成功(自己实现消息重新发送)
一致性消息
只管发不管接受, 效率最高
特殊消息 分类
顺序消息
延迟消息
消息过滤
消息消费方式
消息消费模式有两种:Clustering(集群消费)和Broadcasting(广播消费)
集群模式(默认)
默认情况下就是集群消费,这种模式下一个消费者组共同消费一个主题的多个队列,一个队列只会被一个消费者消费
,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费(抢食物)
广播模式
广播消费消息会发给消费者组中的每一个消费者进行消费
Rocket MQ基本架构
RocketMQ 一共有四个部分组成:
-
NameServer: 用于做服务发现
-
Broker: 用于存储生产者生产的消息, 以及将消息推送给消费者
-
Producer: 生产者
-
Consumer: 消费者
一般每一部分都是集群部署的
NameServer
Q: 什么是NameServer?
A: NameServer 是一个无状态的服务器,角色类似于 Kafka使用的 Zookeeper,但比 Zookeeper 更轻量。
Q: NameServer有什么特点?
A:
- 每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。
- Nameserver 被设计成几乎是无状态的,通过部署多个结点来标识自己是一个伪集群
- 路由转发: Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker
- 定时更新路由: Consumer 也会定时从 NameServer 拉取 Topic 的路由信息
- 心跳检测: Broker 在启动时会向 NameServer 注册,并定时进行心跳连接,且定时同步维护的 Topic 到NameServer。
功能
- 和Broker 结点保持长连接
- 维护 Topic 的路由信息
Broker
Q: Broker是什么?
A: 消息存储和中转角色,负责存储消费者发送的信息, 以及将存储的消息转发给消费者进行消费
Q: Broker内部是怎么实现的?
A:
- Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)
- 单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过Netty实现的。
RocketMQ Broker 内部维护 Consumer Queue 和 CommitLog 的机制(流程)如下:
-
Producer 发送消息到 Broker。
-
Broker 将消息内容持久化到 CommitLog 日志文件中。
-
根据消息主题Topic和队列Queue, Broker会在Consumer Queue中保存此消息的逻辑索引。
-
Consumer 根据订阅的 Topic 从 Broker 拉取消息。
-
Broker 根据 Consumer Queue 的消息索引, 从 CommitLog读取消息内容返回给Consumer。
-
消息内容本身始终保存在CommitLog中,Consumer Queue只保存索引
-
这样即使Consumer Queue丢失,也可以通过CommitLog恢复。
-
CommitLog定期进行归档删除已消费消息。
综上,RocketMQ使用CommitLog保存消息实体内容,Consumer Queue保存消息索引。
这种设计兼顾了消息存储的性能、可靠性和查找的高效性
Producer
消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。
- Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败
- RocketMQ提供了三种方式发送消息:同步、异步和单向
- 同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信
- 异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务
- 单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集
Consumer
消息消费者,负责消费消息,一般是后台系统负责异步消费
- Consumer也由用户部署,支持PUSH和PULL两种消费模式,支持集群消费和广播消费,提供实时的消息订阅机制。
- Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
- Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。
RocketMQ 核心 API
入门案例
消息发送方式
同步消息
这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
生产者
public static void syncProducer() throws MQClientException, UnsupportedEncodingException, MQBrokerException, RemotingException, InterruptedException {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("syncGroup");
// 设置NameServer的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 10; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("syncTopic", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送消息到一个Broker
SendResult sendResult = producer.send(msg);
// 通过sendResult返回消息是否成功送达
log.info("消息发送状态: {}", sendResult.getSendStatus());
}
// 关闭资源
producer.shutdown();
}
消费者
public static void syncConsumer() throws MQClientException {
// 实例化消息生产者Producer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("syncGroup");
// 设置NameServer的地址
consumer.setNamesrvAddr("192.168.132.98:9876");
// 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息
consumer.subscribe("syncTopic", "*");
consumer.setMessageListener((MessageListenerConcurrently) (msgs, context) -> {
msgs.forEach(messageExt ->
log.info("消费线程: {}, 消息id: {}, 消息内容: {}",
Thread.currentThread().getName(),
messageExt.getMsgId(),
new String(messageExt.getBody())));
// 返回消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者实例
consumer.start();
}
异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
生产者
相比于同步, 在发送消息的时,新增回调即可
public static void asyncCProducer() throws UnsupportedEncodingException, MQClientException, RemotingException, InterruptedException {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("asyncGroup");
// 设置NameServer的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 10; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("asyncTopic", ("异步消息" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送消息到一个Broker
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("消费线程: {}, 发送成功,消息id: {}", Thread.currentThread().getName(), sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
log.info("消费线程: {}, 发送失败, 失败原因", Thread.currentThread().getName(), e);
}
});
}
// 等待5s, 如果不等待的话, 看不到提示信息
TimeUnit.SECONDS.sleep(5);
// 关闭资源
producer.shutdown();
}
消费者
public static void asyncCProducer() throws UnsupportedEncodingException, MQClientException, RemotingException, InterruptedException {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("asyncGroup");
// 设置NameServer的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 10; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("asyncTopic", ("异步消息" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送消息到一个Broker
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("消费线程: {}, 发送成功,消息id: {}", Thread.currentThread().getName(), sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
log.info("消费线程: {}, 发送失败, 失败原因", Thread.currentThread().getName(), e);
}
});
}
// 等待5s, 如果不等待的话, 看不到提示信息
TimeUnit.SECONDS.sleep(5);
// 关闭资源
producer.shutdown();
}
一次性消息
这种方式主要用在不特别关心发送结果的场景,例如日志发送。
生产者
public static void oneWayProducer() throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("onwWayGroup");
// 设置NameServer的地址
producer.setNamesrvAddr("192.168.20.161:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 10; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("TopicTest" ,("一次性消息 " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送单向消息,没有任何返回结果
producer.sendOneway(msg);
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
消费者
随便接受即可, 这里不在赘述
消息消费方式
集群模式
消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同
消费模式设置成MessageModel.CLUSTERING即可, 默认也是集群模式
这里在同步消息上进行修改, 修改消费者即可
public static void syncConsumer() throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("syncGroup");
consumer.setNamesrvAddr("192.168.132.98:9876");
// 将消费模式设置成集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.subscribe("syncTopic", "*");
consumer.setMessageListener((MessageListenerConcurrently) (msgs, context) -> {
msgs.forEach(messageExt ->
log.info("消费线程: {}, 消息id: {}, 消息内容: {}",
Thread.currentThread().getName(),
messageExt.getMsgId(),
new String(messageExt.getBody())));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
广播模式
消费者采用广播的方式消费消息,每个消费者消费的消息都是相同的
MessageModel.BROADCASTING
public static void syncConsumer() throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("syncGroup");
consumer.setNamesrvAddr("192.168.132.98:9876");
// 将消费模式设置成集群模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("syncTopic", "*");
consumer.setMessageListener((MessageListenerConcurrently) (msgs, context) -> {
msgs.forEach(messageExt ->
log.info("消费线程: {}, 消息id: {}, 消息内容: {}",
Thread.currentThread().getName(),
messageExt.getMsgId(),
new String(messageExt.getBody())));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
不同消费组对同一Topic的消费情况
顺序模式
数据准备
模拟订单数据工具类
public class OrderUtil {
/**
* 生成模拟订单数据
*/
public static List<OrderStep> buildOrders() {
List<OrderStep> orderList = new ArrayList<OrderStep>();
OrderStep orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
return orderList;
}
}
订单步骤对象
@Data
public class OrderStep {
/**
* 订单id
*/
private long orderId;
/**
* 订单描述
*/
private String desc;
}
生产者
public static void orderProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("orderGroup");
// 设置name server的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动消费者
producer.start();
// 使用工具类生成订单列表
List<OrderStep> orderList = OrderUtil.buildOrders();
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(date);
for (OrderStep orderStep : orderList) {
// 加个时间前缀
String body = dateStr + " Hello RocketMQ " + orderStep;
Message message = new Message("orderTopic", body.getBytes());
SendResult sendResult = producer.send(message, (mqs, msg, arg) -> {
// 根据订单id选择发送queue
Long id = (Long) arg;
// 这里使用取模法
long index = id % mqs.size();
return mqs.get((int) index);
}, orderStep.getOrderId()); // 订单id
log.info("消息发送状态: {}, 消息所在队列id: {}, 消息内容: {}",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId(),
body);
}
producer.shutdown();
}
消费者
public static void orderConsumer() throws MQClientException {
// 消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderGroup");
consumer.setNamesrvAddr("192.168.132.98:9876");
/**
* 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费, 如果非第一次启动,那么按照上次消费的位置继续消费
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("orderTopic", "*");
consumer.setMessageListener(new MessageListenerOrderly() {
final Random random = new Random();
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
try {
//模拟业务逻辑处理中...
TimeUnit.SECONDS.sleep(random.nextInt(3));
} catch (Exception e) {
e.printStackTrace();
}
msgs.forEach(msg ->
// 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
log.info("消费者线程: {}, 队列id: {}, 内容: {}",
Thread.currentThread().getName(),
msg.getQueueId(),
new String(msg.getBody())));
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
}
延迟消息
生产者
public static void delayProducer() throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
// 实例化一个生产者来产生延时消息
DefaultMQProducer producer = new DefaultMQProducer("delayGroup");
// 设置name server的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动生产者
producer.start();
int totalMessagesToSend = 10;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("delayTopic", ("发送延时消息" + i + ",发送时间:" + new Date()).getBytes());
// 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
message.setDelayTimeLevel(3);
// 发送消息
SendResult sendResult = producer.send(message);
log.info("消息发送状态: {}", sendResult.getSendStatus());
}
// 关闭生产者
producer.shutdown();
}
消费者
public static void delayConsumer() throws MQClientException {
// 实例化消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delayGroup");
consumer.setNamesrvAddr("192.168.132.98:9876");
// 订阅 Topics
consumer.subscribe("delayTopic", "*");
// 注册消息监听者
consumer.setMessageListener((MessageListenerConcurrently) (messages, context) -> {
messages.forEach(message -> log.info("消息topic: {},消息内容: {}, 消费时间: {}", message.getTopic(), new String(message.getBody()), new Date()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者
consumer.start();
}
消息过滤
在大多数情况下,TAG是一个简单而有用的设计,其可以来选择您想要的消息, 但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。
在这种情况下,可以使用SQL表达式筛选消息。SQL特性可以通过发送消息时的属性来进行计算。在RocketMQ定义的语法下
tag标签过滤
生产者
public static void tagFilterProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("tagFilterGroup");
// 设置name server的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动name server
producer.start();
// 设置发送的主题
String topic = "tagFilterTopic";
String[] tags = {"tag1", "tag2", "tag3"};
for (int totalCount = 0; totalCount < tags.length; totalCount++) {
// 为每个消息都设置一个tag
Message message = new Message(topic, tags[totalCount], ("order" + totalCount).getBytes());
SendResult sendResult = producer.send(message);
log.info("消息发送状态: {}", sendResult.getSendStatus());
}
// 关闭资源
producer.shutdown();
}
消费者
public static void tagFilterConsumer() throws MQClientException {
// 消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tagFilterGroup");
consumer.setNamesrvAddr("192.168.132.98:9876");
// 设置 主题
String topic = "tagFilterTopic";
// 设置 子标题
consumer.subscribe(topic, "tag1 || tag2 || tag3");
consumer.setMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
list.forEach(messageExt -> log.info("当前topic: {}, 消息内容: {}", messageExt.getTopic(), new String(messageExt.getBody())));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者
consumer.start();
}
SQL92过滤
概念介绍
RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
数值比较,比如:>,>=,<,<=,BETWEEN,=;
字符比较,比如:=,<>,IN;
IS NULL** 或者 IS NOT NULL;
逻辑符号 AND,OR,NOT;
常量支持类型为:
数值,比如:**123,3.1415;
字符,比如:‘abc’,必须用单引号包裹起来;
NULL,特殊的常量
布尔值,TRUE 或 FALSE
只有使用push模式的消费者才能用使用SQL92标准的sql语句,接口如下:
public void subscribe(finalString topic, final MessageSelector messageSelector)
注意: 在使用SQL过滤的时候, 需要配置参数enablePropertyFilter=true
配置enablePropertyFilter=true参数
- 修改配置: 在broker.conf配置文件中新增enablePropertyFilter=true参数
- 关闭brocker: 使用 mqshutdown broker 关闭brocker
- 重启brocker: 使用 nohup mqbroker -n localhost:9876 -c /opt/RokectMQ/rocketmq4.4/conf/broker.conf & 启动broker, 然后新增的配置就生效了
生产者
public void sql92FilterProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("slq92FilterGroup");
// 设置NameServer的地址
producer.setNamesrvAddr("192.168.132.98:9876");
// 启动Producer实例
producer.start();
// 设置topic
String topic = "sql92FilterTopic";
// 发送10条消息
for (int i = 0; i < 10; i++) {
Message message = new Message(topic, ("过滤消息" + i).getBytes(StandardCharsets.UTF_8));
message.putUserProperty("i", String.valueOf(i));
SendResult sendResult = producer.send(message);
log.info("消息发送状态: {}", sendResult.getSendStatus());
}
// 关闭消费者
producer.shutdown();
}
消费者
- 所有的消息都被消费了! 所谓的过滤也是为了过滤后的消息
public static void sql92FilterConsumer() throws MQClientException {
// 实例化消息生产者Producer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("slq92FilterGroup");
// 设置NameServer的地址
consumer.setNamesrvAddr("192.168.132.98:9876");
// 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息
consumer.subscribe("sql92FilterTopic", MessageSelector.bySql("i > 3 and i < 8"));
consumer.setMessageListener((MessageListenerConcurrently) (msgs, context) -> {
msgs.forEach(messageExt -> log.info("消息内容: {}" ,new String(messageExt.getBody())));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者实例
consumer.start();
}
SpringBoot集成RocketMQ
前期准备
引入依赖
- JDK 11
- RocketMQ 4.4.0
- Spring Boot 2.7.3
2.2.2版本的rocketmq-springboot-start对应的rocketmq-client版本是4.9.3
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.3</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
生产者yaml配置
server:
port: 8081
rocketmq:
name-server: 192.168.132.98:9876
producer:
group: my-group
消费者yaml配置
server:
port: 8082
rocketmq:
name-server: 192.168.132.98:9876
消息发送方式
同步消息
生产者
@Test
public void syncSendTest() {
// 同步发送
SendResult sendResult = rocketMQTemplate.syncSend("springboot-syncTopic", "同步消息-hello");
log.info("发送状态: {}", sendResult.getSendStatus());
}
消费者
使用步骤
-
创建一个类, 实现RocketMQListener接口, 设置泛型
-
实现onMessage方法
-
贴上注解@RocketMQMessageListener, 对于consumerGroup 和 topic为必填, 其中consumerGroup 必须唯一, 否则启动报错
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-asyncTopic", messageModel = MessageModel.CLUSTERING)
public class AsyncMessageListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("同步消息内容为: {}", new String(message.getBody()));
}
}
在这个RocketMQ的消息监听器示例代码中,MessageExt代表了RocketMQ中的MessageExt对象。
MessageExt在RocketMQ中是消息的实现类,它包含了消息体和属性等完整信息。
当使用RocketMQListener接口进行消息监听时,onMessage方法的参数需要传入MessageExt对象,然后可以通过它获取到消息内容。
MessageExt类的几个常用方法包括:
- getBody() - 获取消息体内容
- getTopic() - 获取消息主题
- getTags() - 获取消息标签
- getKeys() - 获取消息关键词
- getProperties() - 获取用户自定义属性
- getBornTimestamp() - 获取消息生成时间戳
异步消息
生产者
@Test
public void asyncSendTest() throws InterruptedException {
// 异步发送
rocketMQTemplate.asyncSend("springboot-asyncTopic", "异步消息-hello", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("onSuccess发送成功: {}", sendResult.getSendStatus());
}
@Override
public void onException(Throwable e) {
log.info("onException发送出现异常: {}", e.getMessage());
}
});
// 睡眠5秒, 接受回调
TimeUnit.SECONDS.sleep(5);
}
消费者
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-asyncTopic")
public class AsyncMessageListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("异步消息内容为: {}", new String(message.getBody()));
}
}
一次性消息
生产者
@Test
public void sendOneWayTest() {
// 一次性发送
for (int i = 0; i < 10; i++) {
rocketMQTemplate.sendOneWay("springboot-oneWayTopic", "一次性消息" + i + ",发送时间:" + new Date());
}
}
消费者
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-oneWayTopic")
public class OneWayMessageListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("一次性消息内容为: {}", new String(message.getBody()));
}
}
消息消费模式
集群模式
@RocketMQMessageListener注解中messageModel设置消费模式, messageModel如果不填写, 那么默认就是MessageModel.CLUSTERING, 也就是说默认为集群模式
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-oneWayTopic", messageModel = MessageModel.CLUSTERING)
public class OneWayMessageListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("一次性消息内容为: {}", new String(message.getBody()));
}
}
广播模式
这里以一次性消息作为例子机型进行展开
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-oneWayTopic", messageModel = MessageModel.BROADCASTING)
public class OneWayMessageListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("一次性消息内容为: {}", new String(message.getBody()));
}
}
配置多一个消费者, 方便观察广播模式
启动一个生产者, 两个消费者, 执行下代码, 发送10条一次性消息, 发现两个消费者都收到了10条消息
@Test
public void sendOneWayTest() {
// 一次性发送
for (int i = 0; i < 10; i++) {
rocketMQTemplate.sendOneWay("springboot-oneWayTopic", "一次性消息" + i + ",发送时间:" + new Date());
}
}
顺序消息
生产者
- 在消息选择器中定义消息发送规则
@Test
public void orderlyMessageTest() throws Exception {
// 调用之前的工具了生产订单
List<OrderStep> orderSteps = OrderUtil.buildOrders();
// 在消息选择器, 自定义消息发往哪个队列的规则
// 后续在发送的过程会使用自定义的消息选择器选择指定的队列
rocketMQTemplate.setMessageQueueSelector((mqs, msg, arg) -> {
String orderId = (String) arg;
Long index = Long.parseLong(orderId) % mqs.size();
return mqs.get(index.intValue());
});
// 将消息进行发送
// sendOneWayOrderly(topic, 消息, 往消息选择器传递的参数)
orderSteps.forEach(orderStep ->
// 发送顺序消息的方法通常携带了Orderly后缀
rocketMQTemplate.sendOneWayOrderly(
"springboot-orderTopic",
orderStep,
orderStep.getOrderId() + ""));
}
消费者
- 注解@RocketMQMessageListener需要添加属性consumeMode = ConsumeMode.ORDERLY, 表示使用的顺序模式
- 现在发送的是一个对象, 所以消费者接受的时候也是一个对象
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-orderTopic", consumeMode = ConsumeMode.ORDERLY)
public class OrderMessageListener implements RocketMQListener<OrderStep> {
@Override
public void onMessage(OrderStep orderStep) {
log.info("顺序消息内容为: {}", orderStep);
}
}
延迟消息
生产者
- syncSend的第三个参数发送超时时间 ,单位是毫秒, 当超过这个时间还未发送出去的话, 那么就不等了
- 延迟等级为3
@Test
public void delayMessageTest() throws Exception {
// 这种方式构造的消息和原来new message等价, new message中的底层就是基于这个进行封装的
org.springframework.messaging.Message<String> msg = MessageBuilder.withPayload("springboot发送延迟" + ",发送时间:" + new Date()).build();
rocketMQTemplate.syncSend("springboot-delayTopic", msg, 3000, 3);
}
消费者
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot",topic = "springboot-delayTopic")
public class DelayMessageConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
log.info("延时消息内容: {}, 消费时间: {}", message, new Date());
}
}
消息过滤
tag标签过滤
生产者
- Springboot集成中的RocketMQ没有提供单独设置Tag的参数或者非法, 它将topic和tag统一视为destination, 使用一个topic:tag的方式进行一个区分, 前面写toPic, 后面写tag, 中间使用一个英文冒号进行一个分割
@Test
public void filterByTagMessageTest() throws Exception {
rocketMQTemplate.sendOneWay("springboot-filter-tag:Tag1","我是Tag1消息");
rocketMQTemplate.sendOneWay("springboot-filter-tag:Tag2","我是Tag2消息");
rocketMQTemplate.sendOneWay("springboot-filter-tag:Tag3","我是Tag3消息");
rocketMQTemplate.sendOneWay("springboot-filter-tag:Tag1","Hello--->Tag1");
rocketMQTemplate.sendOneWay("springboot-filter-tag:Tag1:mykey","Hello--->Tag1:mykey");
}
消费者
- @RocketMQMessageListener注解中添加selectorExpression属性, 添加过滤表达式, 将想要显示的消息进行一个过滤
- 需要额外说明的是, 消息都已经被被消费了, 这里过滤的就是消息后的消息
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-filter-tag", selectorExpression = "Tag1 || Tag2")
public class FilterByTagConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("消息内容: {}", new String(message.getBody()));
}
}
sql92过滤
生产者
@Test
public void filterBySQL92MessageTest() throws Exception {
for (int i = 0; i < 10; i++) {
rocketMQTemplate.sendOneWay("springboot-filter-sql92", MessageBuilder.withPayload("我是消息" + i).setHeader("i", i));
}
}
消费者
- 需要修改broker.conf配置文件, 添加enablePropertyFilter=true, 并重新启动
- @RocketMQMessageListener注解中添加了selectorType属性, 默认是SelectorType.TAG, 是这里指定过滤的类型为SQL92
- 如果没有指定上述的selectorType为SQL92的话, 那么默认使用tag进行过滤, 此时的话, selectorExpression中的过滤表达式就被当成了一个tag
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "helloGroupBoot", topic = "springboot-filter-sql92", selectorType = SelectorType.SQL92, selectorExpression = "i>3 and i<8")
public class FilterBySQL92Consumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.info("消息内容: {}", new String(message.getBody()));
}
}