=====安装与基础配置=====
一、MQ介绍
1.1 为什么要用MQ
消息队列是一种“先进先出”的数据结构
其应用场景主要包含以下3个方面:
1)应用解耦
系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
使用消息队列解耦合,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中,用户的下单操作正常完成。当物流系统回复后,补充处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。
2)流量削峰
应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。
一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验,而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总比不能下单体验要好。
处于经济考量目的:
业务系统正常时段的QPS如果是1000,流量最高峰是10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰。
3)数据分发 
通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。
1.2 MQ的优点和缺点
优点:解耦、削峰、数据分发。
缺点包含以下几点:
- 系统可用性降低
-
- 系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
- 如何保证MQ的高可用?
- 系统复杂度提高
-
- MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
- 如何保证消息没有被重复消费?怎么 处理消息丢失情况?那么保证消息传递的顺序性?
- 一致性问题
-
- A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。
- 如何保证消息数据处理的一致性?
1.3 各种MQ产品的比较
常见的MQ产品包括Kafka、ActiveMQ、RabbitMQ、RocketMQ。
二、RocketMQ快速入门
RocketMQ 是阿里巴巴2016年MQ中间件,使用 Java 语言开发,在阿里内部,RocketMQ 承接了例如“双11”等高并发场景的消息流转,能够处理万亿级别的消息。
2.1 准备工作
2.1.1 下载RocketMQ
这里选择的 RocketMQ 的版本:4.8.0
下载地址:下载地址
官方文档:rocketmq.apache.org/docs/quick-…
2.1.2 环境要求
- Linux64位系统
- JDK1.8(64位)
2.2 安装RocketMQ
2.2.1 安装步骤
我这里是以二进制包方式来安装的:
- 解压安装包
- 进入安装目录
2.2.2 目录介绍
bin:启动脚本,包括 shell 脚本和 CMD 脚本conf:实例配置文件 ,包括 broker 配置文件、logback 配置文件等lib:依赖 jar 包,包括Netty、commons-lang、FastJSON等
2.3 启动RocketMQ
RocketMQ默认的虚拟机内存较大,启动Broker或者NameServer可能会因为内存不足而导致失败,所以需要编辑如下两个配置文件,修改 JVM 内存大小。
# 编辑 runbroker.sh 和 runserver.sh 修改默认 JVM 大小
$ vi bin/runbroker.sh
# 参考设置
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"
$ vi bin/runserver.sh
# 参考设置
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
2.启动 NameServer
# 1.启动NameServer
nohup sh bin/mqnamesrv &
# 2.查看启动日志
tail -f ~/logs/rocketmqlogs/namesrv.log
3.启动 Broker
# 1.启动Broker
nohup sh bin/mqbroker -n localhost:9876 &
# 2.查看启动日志
tail -f ~/logs/rocketmqlogs/broker.log
bin/mqbroker 的一些可选参数:
-c:指定配置文件路径-n:NameServer 的地址
注意:
如果mqbroker 日志中提示找不到/root/store/commitlog时要手动创建。
创建完毕后,又提示找不到/root/store/consumequeue,再次创建即可。
2.4 测试RocketMQ
2.4.1 发送消息
# 1.设置环境变量
export NAMESRV_ADDR=localhost:9876
# 2.使用安装包的Demo发送消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
2.4.2 接收消息
# 1.设置环境变量
export NAMESRV_ADDR=localhost:9876
# 2.接收消息
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
2.5 关闭RocketMQ
# 1.关闭NameServer
sh bin/mqshutdown namesrv
# 2.关闭Broker
sh bin/mqshutdown broker
2.6 各角色介绍
Producer:消息的发送(生产)者;举例:发件者。Consumer:消息接收者;举例:收件人。Consumer Group:消费组;每一个 consumer 实例都属于一个 consumer group,每一条消息只会被同一个 consumer group 里的一个 consumer 实例消费。(不同consumer group可以同时消费同一条消息)。Broker:暂存和传输消息;举例:快递公司。NameServer:管理 Broker;举例:快递公司的管理机构。Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个 Topic;一个消息的接收者可以订阅一个或者多个 Topic 消息。Message Queue:相当于是 Topic 的分区;用于并行发送和接收消息。
2.7 broker配置文件详解
broker 默认的配置文件位置在:conf/broker.conf
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=10911
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/usr/local/rocketmq/store
#commitLog 存储路径
storePathCommitLog=/usr/local/rocketmq/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
#消息索引存储路径
storePathIndex=/usr/local/rocketmq/store/index
#checkpoint 文件存储路径
storeCheckpoint=/usr/local/rocketmq/store/checkpoint
#abort 文件存储路径
abortFile=/usr/local/rocketmq/store/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
2.8 可视化监控平台搭建
2.8.1 概述
RocketMQ ****有一个对其扩展的开源项目 incubator-rocketmq-externals,这个项目中有一个子模块叫 rocketmq-console,这个便是管理控制台项目了,先将 incubator-rocketmq-externals 拉到本地,因为我们需要自己对 rocketmq-console 进行编译打包运行。
2.8.2 下载并编译打包
将配置文件中的端口等进行修改,然后打包上传至服务器。
2.8.3 调整rocketmq配置并启动rocketmq-console控制台
先关闭broker,namesrv
sh mqshutdown broker
sh mqshutdown namesrv
进行配置文件调整
conf/broker.conf
brokerIP1=ip
重新启动
export NAMESRV_ADDR=ip:9876
nohup sh mqnamesrv -n ip:9876 &
nohup sh mqbroker -n ip:9876 -c ../conf/broker.conf autoCreateTopicEnable=true &
启动控制台
nohup java -jar rocketmq-console-ng-1.0.0.jar > tmp.log &
注意:当启动控制台后报错,需根据提示开放端口10909等。
启动成功后,我们就可以通过浏览器访问 http://IP地址:端口 进入控制台界面了,如下图:
**
=====架构设计&工作流程=====
一、RocketMq架构
1.Producer:消息生产者,负责将消息发送到Broker。
2.Broker:RocketMQ的核心组件,负责存储、传输和路由消息。它接收Producer发送的消息,并将其存储在内部存储中。并且还负责处理Consumer的订阅请求,将消息推送给订阅了相应Topic的Consumer。RocketMQ支持多个Broker构成集群,每个Broker都拥有独立的存储空间和消息队列。
3.Consumer:消息消费者,负责从Broker消费消息。
4.NameServer:NameServer是RocketMQ的路由和寻址中心,它维护了Broker和Topic的路由信息,并且提供了Producer和Consumer与正确的Broker建立连接的能力。NameServer不负责监控Broker的状态,并提供自动发现和故障恢复的功能。Producer和Consumer在启动时需要连接到NameServer获取Broker的地址信息。
5.Topic:消息主题,是消息的逻辑分类单位。Producer将消息发送到特定的Topic中,Consumer从指定的Topic中消费消息。
6.Message Queue:消息队列,是Topic的物理实现。一个Topic可以有多个Queue(通过broker配置文件配置),每个Queue都是独立的存储单元。Producer发送的消息会被存储到对应的Queue中,Consumer从指定的Queue中消费消息。
二、RocketMq集群方式
3种,分别是单Master模式、多Master模式以及多Master多Slave模式
单Master集群:这是一种最简单的集群方式,只包含一个Master节点和若干个Slave节点。所有的写入操作都由
Master节点负责处理,Slave节点主要用于提供读取服务。当Master节点宕机时,集群将无法继续工作。
多Master集群:这种集群方式包含多个Master节点,不部署Slave节点。这种方式的优点是配置简单,单个
Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁
盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高:缺点是单台机器宕机期
间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
多Master多Slave集群:这种集群方式包含多个Master节点和多个Slave节点。每个Master节点都可以处理写入操作,并且有自己的一组Slave节点。当其中一个Master节点宕机时,消费者仍然可以从Slave消费。优点是数据与服务都无单点故障,Master名机情况下,消息无延迟,服务可用性与数据可用性都非常高;缺点是性能比异步复
制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为
主机。
三、工作流程
RocketMQ的工作过程大致如下:
1、启动NameServer,他会等待Broker、Producer以及Consumer的链接;
2、启动Broker,会和NameServer建立连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)
Topic信息以及Borker与Topic的映射关系;
3、启动Producer,启动时先随机和NameServer集群中的一台建立长连接,并从NameServer中获取当前发送的
Topic所在的所有Broker的地址;然后从队列列表中轮询选择一个队列,与队列所在的Broker建立长连接,进行消
息的发送;
4、Broker接收Producer发送的消息,当配置为同步复制时,master需要先将消息复制到slave节点,然后再返回
“写成功状态”响应给生产者;当配置为同步刷盘时,则还需要将消息写入磁盘中,再返回“写成功状态”:要是
配置的是异步刷盘和异步复制,则消息只要发送到master节点,就直接返回“写成功”状态;
5、启动Consumer,过程和Producer类似,先随机和一台NameServer建立连接,获取订阅信息,然后在和需要
订阅的Broker建立连接,获取消息。
四、配置文件添加NameServer
broker默认配置为:
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
添加以下配置:
namesrvAddr=175.178.247.65:9876 #配置注册路由中心地址,保证broker高可用
brokerIP1=175.178.247.65 #配置broker地址
autoCreateTopicEnable=true #主题不存在时,允许自动创建主题
根据日志提示开放10100-11000之间的端口。
=====集成&常见问题=====
一、消息发送和监听流程
1.1 消息生产者
1.创建消息生产者producer,并指定生产者组名
2.指定Nameserver地址
3.启动producer
4.创建消息对象,指定主题Topic、Tag和消息体等
5.发送消息
6.关闭生产者producer
1.2 消息消费者
1.创建消费者consumer,指定消费者组名
2.指定Nameserver地址
3.创建监听订阅主题Topic和Tag等
4.处理消息
5.启动消费者consumer
1.3 基础架构搭建
rocketmq-client版本与安装版本一致
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
生产者发送消息
@Test
public void producer() throws Exception {
//1.创建生产者并指定组名
DefaultMQProducer producer = new DefaultMQProducer("test_group");
//2.指定nameserver地址
producer.setNamesrvAddr("175.178.247.65:9876");
//3.启动生产者
producer.start();
//4.创建消息对象并指定topic,tag,消息体
Message msg = new Message("test_topic", "我是一个消息".getBytes());
//5.发送消息
SendResult send = producer.send(msg);
System.out.println(send.getSendStatus());
//6.关闭生产者
producer.shutdown();
}
消费者消费消息
@Test
public void consumer() throws Exception {
//1.创建消费者并指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_group");
//2.指定nameserver地址
consumer.setNamesrvAddr("175.178.247.65:9876");
//3.监听订阅主题topic,tag(*表示订阅该主题所有消息)等
consumer.subscribe("test_topic", "*");
//4.处理消息(异步消费)
consumer.registerMessageListener(new MessageListenerConcurrently() {
/**
* 消费消息方法(MessageListenerConcurrently是并发消费,默认20个线程一起消费)
* @param msgs 消息对象
* @param context 上下文
* @return 返回消费是否成功状态:
* CONSUME_SUCCESS成功,消息会从mq出队;
* RECONSUME_LATER失败(报错/null),消息会重新放入mq中,过一会重新出队,可给当前或其他消费者消费。
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println("我是消费者");
System.out.println("消息内容;" + new String(msgs.get(0).getBody())); //解析消息对象中的二进制数组
System.out.println("消息消费上下文:" + context);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者
consumer.start();
Thread.sleep(5000); //异步消费时,防止主线程直接结束,导致无法消费。
}
二、注意事项!!!
1.同一个生产者中可往不同topic中发送消息,而同一个消费者组内要保证订阅关系一致,topic和tag都要一样,否则会导致消息消费紊乱,甚至数据丢失!!!
2.消息分发有两种方式:
- 当只有一个消费者组时,可采用广播模式每个消费者都可消费消息,也可采用负载均衡模式按顺序每次只有一个消费者可进行消费。
- 当有多个消费者组同时订阅一个主题时,会以组为单位,每个组给一份消息,组内可进行广播或负载均衡模式进行消息分发。
3.一个topic下默认有四个队列:
- 发消息时通过轮询方式将消息存储到队列上。
- 消费消息时(负载均衡模式):
-
- 当组内只有一个消费者,会消费四个队列(一个topic内)所有消息。
- 当组内有两个消费者,每个消费者只能指定消费两个队列。
- 当组内有三个消费者,会进行reBalance重平衡,其中一个消费者消费两个队列,另外两个消费者分别消费一个队列。
- 当组内有四个消费者,每个消费者消费一个队列。
- 当组内有五个消费者,最后一个消费者永远不会收到消息。
- 所以队列数量要大于等于组内消费者数量,当队列消息堆积过多时,开多个消费者进行消费是没用的,实际上多出的消费者接受不到消息。
4.代理者点位:队列中每存一个消息,该队列的代理者点位+1;
消费者点位:消费者消费消息并返回消费成功状态后,消费者点位+1,;
差值:该队列剩余未消费消息数量。
三、消费模式
MQ的消费模式可以大致分为两种,一种是推Push,一种是拉Pull。
Push是服务端【MQ】主动推送消息给客户端,优点是及时性较好,但如果客户端没有做好流控,一旦服务端推送大量消息到客户端时,就会导致客户端消息堆积甚至崩溃。
Pull是客户端需要主动到服务端取数据,优点是客户端可以依据自己的消费能力进行消费,但拉取的频率也需要用户自己控制,拉取频繁容易造成服务端和客户端的压力,拉取间隔长又容易造成消费不及时。
Push模式也是基于pull模式的,只能客户端内部封装了api,一般场景下,上游消息生产量小或者均速的时候,选择push模式。在特殊场景下,例如电商大促,抢优惠券等场景可以选择pull模式。
四、消息发送(RocketMqClient)
4.1 同步消息 ***(重要度)
同步消息发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认状态,之后才往下继续执行,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式。
@Test
public void producer() throws Exception {
//1.创建生产者并指定组名
DefaultMQProducer producer = new DefaultMQProducer("test_group");
//2.指定nameserver地址
producer.setNamesrvAddr("175.178.247.65:9876");
//3.启动生产者
producer.start();
//4.创建消息对象并指定topic,tag,消息体
Message msg = new Message("test_topic", "我是一个消息".getBytes());
//5.发送消息
SendResult send = producer.send(msg);
System.out.println(send.getSendStatus());
//6.关闭生产者
producer.shutdown();
}
@Test
public void consumer() throws Exception {
//1.创建消费者并指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_group");
//2.指定nameserver地址
consumer.setNamesrvAddr("175.178.247.65:9876");
//3.监听订阅主题topic,tag(*表示订阅该主题所有消息)等
consumer.subscribe("test_topic", "*");
//4.处理消息(异步消费)
consumer.registerMessageListener(new MessageListenerConcurrently() {
/**
* 消费消息方法
* @param msgs 消息对象
* @param context 上下文
* @return 返回消费是否成功状态:
* CONSUME_SUCCESS成功,消息会从mq出队;
* RECONSUME_LATER失败(报错/null),消息会重新放入mq中,过一会重新出队,可给当前或其他消费者消费。
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println("我是消费者");
System.out.println("消息内容;" + new String(msgs.get(0).getBody())); //解析消息对象中的二进制数组
System.out.println("消息消费上下文:" + context);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者
consumer.start();
Thread.sleep(5000); //异步消费时,防止主线程直接结束,导致无法消费。
}
4.2 异步消息 ***
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知。
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("发送失败");
}
});
4.3 延迟消息 ***
消息放入mq后,过一段时间才会被监听到,然后消费。
比如下订单业务,提交了一个订单就可以发送一个延时消息,30min后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
RocketMq默认支持以下几个固定的延时等级,等级1就对应1s,以此类推,最高支持2h延迟;也可在配置文件中指定延迟时间。
private String messageDelayLevel =
"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
Message msg = new Message("test_topic", "我是一个消息".getBytes());
//设置延迟级别
msg.setDelayTimeLevel(3);
producer.send(msg);
4.4 单向消息 *
这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,例如日志信息的发送。
producer.sendOneway(msg);
4.5 批量消息 *
Rocketmq可以一次性发送一组消息,那么这一组消息会被当做一个消息消费。
List<Message> msgs = Arrays.asList(
new Message("TopicTest", "我是一组消息的A消息".getBytes()),
new Message("TopicTest", "我是一组消息的B消息".getBytes()),
new Message("TopicTest", "我是一组消息的C消息".getBytes())
);
producer.send(msgs);
4.6 顺序消息 *
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为:分区有序或者全局有序。
可能大家会有疑问,mq不就是FIFO吗?
rocketMq的broker的机制,导致了rocketMq会有这个问题,因为一个broker中对应了四个queue。
顺序消费的原理解析:在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
下面用订单进行分区有序的示例。一个订单的顺序流程是:下订单、发短信通知、物流、签收。订单顺序号相同的消息会被先后发送到同一个队列中,消费时,同一个顺序获取到的肯定是同一个队列。
生产者:
List<Order> orderList = Arrays.asList(
new Order(1, 111, 59D, new Date(), "下订单"),
new Order(2, 111, 59D, new Date(), "物流"),
new Order(3, 111, 59D, new Date(), "签收"),
new Order(4, 112, 89D, new Date(), "下订单"),
new Order(5, 112, 89D, new Date(), "物流"),
new Order(6, 112, 89D, new Date(), "拒收")
);
// 循环集合开始发送
orderList.forEach(order -> {
Message message = new Message("TopicTest", order.toString().getBytes());
try {
// 发送的时候 相同的订单号选择同一个队列
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 当前主题有多少个队列
int queueNumber = mqs.size();
// 这个arg就是后面传入的(send方法中第三个参数) order.getOrderNumber()
Integer i = (Integer) arg;
// 用这个值去%队列的个数得到一个队列
int index = i % queueNumber;
// 返回选择的这个队列即可 ,那么相同的订单号 就会被放在相同的队列里 实现FIFO了
return mqs.get(index);
}
}, order.getOrderNumber());
} catch (Exception e) {
System.out.println("发送异常");
}
});
消费者:将默认的多线程模式改为单线程模式,保证每一个队列内消息都是有序执行。
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
MessageExt messageExt = msgs.get(0);
System.out.println(new String(messageExt.getBody()));
return ConsumeOrderlyStatus.SUCCESS;
}
});
4.7 Tag过滤消息
Rocketmq提供消息过滤功能,通过tag或者key进行区分。
我们往一个主题里面发送消息的时候,根据业务逻辑,可能需要区分,比如带有tagA标签的被A消费,带有tagB标签的被B消费,还有在事务监听的类里面,只要是事务消息都要走同一个监听,我们也需要通过过滤才区别对待。一个消息体,除了消息本身外还包含Tag标签。
生产者:
Message msg = new Message("TopicTest","tagA", "我是一个带标记的消息".getBytes());
SendResult send = producer.send(msg);
消费者:
// 订阅一个主题来消费 表达式,默认是*,支持"tagA || tagB || tagC" 这样或者的写法 只要是符合任何一个标签都可以消费
consumer.subscribe("TopicTest", "tagA || tagB || tagC");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(msgs.get(0).getTags());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
什么时候该用Topic,什么时候该用 Tag?
总结:不同的业务应该使用不同的Topic,如果是相同的业务里面有不同表的表现形式,那么我们要使用tag进行区分。
可以从以下几个方面进行判断:
1.消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的 Topic,无法通过 Tag 进行区分。
2.业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分。
3.消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分。
4.消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个 Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的 Topic。
总的来说,针对消息分类,您可以选择创建多个Topic,或者在同一个 Topic 下创建多个 Tag。但通常情况下,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息,例如全集和子集的关系、流程先后的关系。
4.8 带Key消息
在rocketmq中的消息,默认会有一个messageId当做消息的唯一标识,我们也可以给消息携带一个key,用作唯一标识或者业务标识,包括在控制面板查询的时候也可以使用messageId或者key来进行查询。
生产者:
Message msg = new Message("test_topic1","tagA","key", "我是一个带标记和key的消息".getBytes());
SendResult send = producer.send(msg);
消费者:
// 订阅一个主题来消费 表达式,默认是*,支持"tagA || tagB || tagC" 这样或者的写法 只要是符合任何一个标签都可以消费
consumer.subscribe("test_topic1", "tagA || tagB || tagC");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(msgs.get(0).getTags());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
五、重试机制
5.1 生产者方重试
// 生产者发送消息时,失败的情况重发3次
producer.setRetryTimesWhenSendFailed(3);
// 消息在1S内没有发送成功,就会重试
producer.send(msg, 1000);
5.2 消费者方重试
在消费者方return ConsumeConcurrentlyStatus.RECONSUME_LATER;后就会执行重试
上图代码中说明了,我们再实际生产过程中,一般重试3-5次,如果还没有消费成功,则可以把消息签收了,通知人工等处理。
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
try {
// 这里执行消费的代码
System.out.println(Thread.currentThread().getName() + "----" + msgs);
// 这里制造一个错误
int i = 10 / 0;
} catch (Exception e) {
// 出现问题 判断重试的次数
MessageExt messageExt = msgs.get(0);
// 获取重试的次数 失败一次消息中的失败次数会累加一次
int reconsumeTimes = messageExt.getReconsumeTimes();
if (reconsumeTimes >= 3) {
// 则把消息确认了,可以将这条消息记录到日志或者数据库 通知人工处理
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} else {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
消费者也可通过consumer.setMaxReconsumeTimes(2); 自定义重试次数
六、死信消息
当消费重试到达阈值以后,消息不会被投递给消费者了,而是进入了死信队列,当一条消息初次消费失败,RocketMQ会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息。此时,该消息不会立刻被丢弃,而是将其发送到该消费者对应的特殊队列中,这类消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue),死信队列是死信Topic下分区数唯一的单独队列。如果产生了死信消息,那对应的ConsumerGroup的死信Topic名称为%DLQ%ConsumerGroupName,死信队列的消息将不会再被消费。可以利用RocketMQ Admin工具或者RocketMQ Dashboard上查询到对应死信消息的信息。我们也可以去监听死信队列,然后进行自己的业务上的逻辑。
生产者:
Message message = new Message("dead-topic", "我是一个死信消息".getBytes());
producer.send(message);
消费者:
consumer.setMaxReconsumeTimes(2);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(msgs);
// 测试消费失败
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
控制台查看:
死信消费者:
// 队列名称 默认是 %DLQ% + 消费者组名
consumer.subscribe("%DLQ%dead-group", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(msgs);
// 处理消息 签收了
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
七、重复消费 ***
7.1 为什么会出现重复消费问题?
1. BROADCASTING(广播)模式下,所有注册的消费者都会消费,而这些消费者通常是集群部署的一个个微服务,这样就会多台机器重复消费,当然这个是根据需要来选择。
2. CLUSTERING(负载均衡)模式下,如果一个topic被多个consumerGroup消费,也会重复消费。
3. 即使是在CLUSTERING模式下,同一个consumerGroup下,一个队列只会分配给一个消费者,看起来好像是不会重复消费。但是,有个特殊情况:一个消费者新上线后,同组的所有消费者要重新负载均衡(反之一个消费者掉线后,也一样)。一个队列所对应的新的消费者要获取之前消费的offset(偏移量,也就是消息消费的点位),此时之前的消费者可能已经消费了一条消息,但是并没有把offset提交给broker,那么新的消费者可能会重新消费一次。虽然orderly模式是前一个消费者先解锁,后一个消费者加锁再消费的模式,比起concurrently要严格了,但是加锁的线程和提交offset的线程不是同一个,所以还是会出现极端情况下的重复消费 (解释:只用当消费者消费消息成功并返回成功状态后,队列的消费者点位才会移动;但当一个消费者组下只有一个消费者,该消费者正在消费3号队列且未返回成功状态;此时消费者组新增了一个消费者,会触发reBalance机制,将12号队列分为第一个消费者,34号队列分为新消费者;此时3号队列中的消息又会被新消费者重新消费!) 。
4. 还有在发送批量消息的时候,会被当做一条消息进行处理,那么如果批量消息中有一条业务处理成功,其他失败了,还是会被重新消费一次。
那么如果在CLUSTERING(负载均衡)模式下,并且在同一个消费者组中,不希望一条消息被重复消费,改怎么办呢?我们可以想到去重操作,找到消息唯一的标识,可以是msgId也可以是你自定义的唯一的key,这样就可以去重了。
7.2 解决方案
使用去重方案解决,例如将消息的唯一标识存起来,然后每次消费之前先判断是否存在这个唯一标识,如果存在则不消费,如果不存在则消费,并且消费以后将这个标记保存。
想法很好,但是消息的体量是非常大的,可能在生产环境中会到达上千万甚至上亿条,那么我们该如何选择一个容器来保存所有消息的标识,并且又可以快速的判断是否存在呢?
我们可以选择布隆过滤器(BloomFilter)
布隆过滤器( Bloom Filter )是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
在hutool的工具中我们可以直接使用,当然你自己使用redis的bitmap类型手写一个也是可以的。
基础用法:
// 初始化
BitMapBloomFilter filter = new BitMapBloomFilter(10);
filter.add("123");
filter.add("abc");
filter.add("ddd");
// 查找
filter.contains("abc")
7.2.1 生产者
// 我们可以使用自定义key当做唯一标识
String keyId = UUID.randomUUID().toString();
Message msg = new Message("TopicTest", "tagA", keyId, "我是一个测试消息".getBytes());
SendResult send = producer.send(msg);
7.2.2 消费者
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// 拿到消息的key
MessageExt messageExt = msgs.get(0);
String keys = messageExt.getKeys();
// 判断是否存在布隆过滤器中
if (bloomFilter.contains(keys)) {
// 直接返回了 不往下处理业务
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
// 这个处理业务,然后放入过滤器中
// do sth...
bloomFilter.add(keys);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
八、SpringBoot集成
8.1 rocketmq依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.4.2</version>
</dependency>
8.2 生产者相关api&配置
8.2.1 yaml配置
spring:
application:
name: rocketmq-producer
rocketmq:
name-server: 175.178.247.65:9876 # rocketMq的nameServer地址
producer:
group: ssm-group # 生产者组别
send-message-timeout: 3000 # 消息发送的超时时间
retry-times-when-send-async-failed: 2 # 异步消息发送失败重试次数
max-message-size: 4194304 # 消息的最大长度
8.2.2 消息发送示例
SendResult sendResult = rocketMQTemplate.syncSend("test", "test");
System.out.println(sendResult.getSendStatus());
System.out.println(sendResult.getMsgId());
8.3 消费者相关api&配置
8.3.1 yaml配置
不建议在配置文件中指定消费者组,因为一个项目可以有多个消费者,指定组后,只能订阅同一个topic:tag
spring:
application:
name: rocketmq-consumer
rocketmq:
name-server: 175.178.247.65:9876 # rocketMq的nameServer地址
server:
port: 8888
8.3.2 消费者监听类搭建
@Component
@RocketMQMessageListener(topic = "test", consumerGroup = "ssmc-group",messageModel = MessageModel.CLUSTERING)
public class RocketLis implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("接收到消息:"+message);
}
}
- topic:目标主题
- consumerGroup:消费者组
- messageModel:消费模式,CLUSTERING集群(负载均衡)模式;BROADCASTING广播模式
- RocketMQListener:消息监听器,T表示消息体类型,指定具体类型后,onMessage里就是消息内容;当类型为MessageExt后,onMessage里就是消息体所有内容。
- onMessage:需要实现的方法,用于消费消息
8.4 生产者发送不同消息
8.4.1 同步消息
同步发送(有返回值) ,阻塞当前线程直到收到Broker确认或超时。
- syncSend():
-
destination:消息目标(字符串格式),通常为Topic:Tag 或 Topic组合payload:消息内容(任意对象类型),自动转换为消息体(如JSON)。timeout(可选):发送超时时间(单位:毫秒),默认值取决于配置。
- send():
-
destination:消息目标(同上)。message:消息对象(要发送的消息对象Message<?>类型)。SendCallback(可选)异步发送:异步回调接口,处理发送成功/失败的结果。
- convertAndSend():
-
destination:消息目标(同上)。payload:消息内容(同上)。headers(可选):通过Map或MessagePostProcessor设置消息头(如Tag、Key、延迟等级)。timeout(可选):同步发送的超时时间。
8.4.2 异步消息
异步消息无返回值
- asyncSend():
-
destination:消息目标(同上)。payload:消息内容(同上)。sendCallback: 异步回调接口,用于处理发送成功或失败的结果(实现onSuccess onException 方法)。
8.4.3 单向消息
- sendOneWay()无返回值:
-
destination:消息目标(同上)。payload:消息内容(同上)。
8.4.4 延迟消息
- syncSend():
-
destination:消息目标(字符串格式),通常为Topic:Tag 或 Topic组合payload:消息内容(任意对象类型),自动转换为消息体(如JSON)。timeout(可选):发送超时时间(单位:毫秒),默认值取决于配置。delayLevel:延迟等级。
// 构建消息对象
Message<String> message = MessageBuilder.withPayload("我是一个延迟消息").build();
// 发送一个延时消息,延迟等级为4级,也就是30s后被监听消费
SendResult sendResult = rocketMQTemplate.syncSend("powernode", message, 2000, 4);
8.4.5 顺序消息
- syncSendOrderly():
-
destination:消息目标(同上)。payload:消息内容(同上)。hashKey:根据hashKey的值,通过哈希算法将消息路由到 Topic 下的特定队列(MessageQueue),相同hashKey的消息会被分配到同一个队列,保证这些消息按发送顺序被消费者顺序处理。
- 消费者配置: 必须设置消费者为 顺序消费模式(
ConsumeMode.ORDERLY),且每个队列由单个线程串行处理。
@Test
public void testOrderly() throws Exception {
List<Order> orders = Arrays.asList(
new Order(UUID.randomUUID().toString().substring(0, 5), "张三的下订单", null, null, null, 1),
new Order(UUID.randomUUID().toString().substring(0, 5), "张三的发短信", null, null, null, 1),
new Order(UUID.randomUUID().toString().substring(0, 5), "张三的物流", null, null, null, 1),
new Order(UUID.randomUUID().toString().substring(0, 5), "张三的签收", null, null, null, 1),
new Order(UUID.randomUUID().toString().substring(0, 5), "李四的下订单", null, null, null, 2),
new Order(UUID.randomUUID().toString().substring(0, 5), "李四的发短信", null, null, null, 2),
new Order(UUID.randomUUID().toString().substring(0, 5), "李四的物流", null, null, null, 2),
new Order(UUID.randomUUID().toString().substring(0, 5), "李四的签收", null, null, null, 2)
);
// 我们控制流程为 下订单->发短信->物流->签收 hash的值为seq,也就是说 seq相同的会放在同一个队列里面,顺序消费
orders.forEach(order -> {
rocketMQTemplate.syncSendOrderly("powernode-obj", order, String.valueOf(order.getSeq()));
});
}
@RocketMQMessageListener(
topic = "OrderTopic",
consumerGroup = "order-group",
consumeMode = ConsumeMode.ORDERLY // 关键配置
)
public class OrderListener implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
// 顺序处理消息
}
}
8.4.6 Tag&Key过滤消息
key:想要发送带key的消息,需要在Message对象的消息头setHeader中设置。
Tag:在发送消息的destination参数中指定消息目标,通常为Topic:Tag 或 Topic组合。
在消费者端,需通过注解开启Tag标签:
- selectorType = SelectorType.TAG, 指定使用tag过滤。(也可以使用sql92 需要在配置文件broker.conf中开启enbalePropertyFilter=true)
- selectorExpression = "java" 表达式,默认是 *,支持"tag1 || tag2 || tag3"
rocketMQTemplate.syncSend("test-Topic:java", "我是一个带tag的消息");
@Component
@RocketMQMessageListener(topic = "test-topic",
consumerGroup = "test-tag-group",
selectorType = SelectorType.TAG,
selectorExpression = "java"
)
public class TagMsgListener implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println(message);
}
}
8.4.7 messageModel(消息分发模型) 与 consumeMode(消费模式)的区别?
-
- messageModel分为集群模式和广播模式,代表的是消息分发的规则。
- consumeMode分为并发消费和顺序消费,代表的是消息消费的规则。
九、消息堆积
一般认为单条队列消息差值>=10w时 算堆积问题。
什么情况下会出现堆积?
① 生产太快了
生产方可以做业务限流 或者 增加消费者数量,但是消费者数量<=队列数量,适当的设置最大的消费线程数量(根据IO(2n)/CPU(n+1))动态扩容队列数量,从而增加消费者数量。
②消费者消费出现问题: 排查消费者程序的问题。
十、消息丢失
消息丢失解决方案:
-
生产者使用同步发送模式,收到mq的返回确认以后 顺便往自己的数据库里面写 msgId,status(1), createtime,消费者消费以后修改数据这条消息的状态= 0。
-
写一个定时任务每天去查询数据 检查是否有消息发了很久没被消费 status = 1 and createTime>1,来进行补发,并结合幂等操作防止重复消费。
-
将mq的刷盘机制设置为同步刷盘
-
使用集群模式,搞主备模式,将消息持久化在不同的硬件上
-
可以开启mq的trace机制,消息跟踪机制