RocketMq使用(5.0)

RocketMq使用(5.0)

基础概念

这一部分我们可以结合一下管理控制台,先来理解下RocketMQ的一些重要的基础概念:

官方文档-消息发送领域模型:https://rocketmq.apache.org/zh/docs/domainModel/01main

整个消息流程(大致,错了勿怪)

1、部署时Broker会根据配置的nameserver地址,将自身的名称,地址等信息注册到nameserver上,每个nameserver上都具备了全量的broker信息。一般只要一台nameserver和一台broker master 节点正常,整个集群就可以正常提供服务。

2、producer发送消息时会向NameServer获取全量的broker数据,缓存在本地,后续每次发送消息,会发送请求给nameserver,判断缓存的broker数据是否有变动,nameserver返回ture or false,从而确定是否需要更新数据。

3、发送消息是基于topic,topic是逻辑概念,每个topic对应多个messagequeue,按照默认配置,messagequeue被存放在多个broker上,每个messagequeue又包含多个消息。

4、consumer从nameServer获取到数据后,会定位到对应的broker进行消费。

1 消息模型(Message Model)

RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup由多个Consumer 实例构成。

2 消息生产者(Producer)

负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。生产者中,会把同一类Producer组成一个集合,叫做生产者组。同一组的Producer被认为是发送同一类消息且发送逻辑一致。

3 消息消费者(Consumer)

负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。拉取式消费的应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。推动式消费模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。消费者同样会把同一类Consumer组成一个集合,叫做消费者组,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。

集群消费模式下, 相同Consumer Group的每个Consumer实例平均分摊消息。

广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

4 主题(Topic)

表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
Topic只是一个逻辑概念,并不实际保存消息。同一个Topic下的消息,会分片保存到不同的Broker上,而每一个分片单位,就叫做MessageQueue。
MessageQueue是一个具有FIFO特性的队列结构,生产者发送消息与消费者消费消息的最小单位。

5 代理服务器(Broker Server)

消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
Broker Server是RocketMQ真正的业务核心,包含了多个重要的子模块:

  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

RocketMQ 5.0 引入了全新的弹性无状态代理模式,将当前的Broker职责进行拆分,对于客户端协议适配、权限管理、消费管理等计算逻辑进行抽离,独立无状态的代理角色提供服务,Broker则继续专注于存储能力的持续优化。

而Broker Server要保证高可用需要搭建主从集群架构。RocketMQ中有两种Broker架构模式:
普通集群:
这种集群模式下会给每个节点分配一个固定的角色,master负责响应客户端的请求,并存储消息。slave则只负责对master的消息进行同步保存,并响应部分客户端的读请求。消息同步方式分为同步同步和异步同步。这种集群模式下各个节点的角色无法进行切换,也就是说,master节点挂了,这一组Broker就不可用了,剩余的slave只能保证存量消息被正常消费。
Dledger高可用集群:
Dledger是RocketMQ自4.5版本引入的实现高可用集群的一项技术。这个模式下的集群会随机选出一个节点作为master,而当master节点挂了后,会从slave中自动选出一个节点升级成为master。
Dledger技术做的事情:1、从集群中选举出master节点 2、完成master节点往slave节点的消息同步。

6 名字服务(Name Server)

名称服务充当路由消息的提供者。Broker Server会在启动时向所有的NameServer注册自己的服务信息,并且后续通过心跳请求的方式保证这个服务信息的实时性。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个
Namesrv实例组成集群,但相互独立,没有信息交换。这种特性也就意味着NameServer中任意的节点挂了,只要有一台服务节点正常,整个路由服务就不会有影响。当然,这里不考虑节点的负载情况。

7 消息(Message)

消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题Topic。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。并且Message上有一个为消息设置的标志,Tag标签。用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

关于Message的更详细字段,在源码的docs/cn/best_practice.md中有详细介绍。

Admin Tool

官方文档:https://rocketmq.apache.org/zh/docs/deploymentOperations/16admintool

执⾏命令⽅法:./mqadmin {command} {args}
用于通过命令直接操作rocketMq集群

rocketmq dashboard 使用

大致上没啥好讲的,

  • 消息:用于查询莫一条消息详情,提供多种查询方式
  • 生产者:
  • 消费者:
  • 运维:运维部分显示了nameserver集群地址
  • 集群: 集群部分显示了broker集群信息

驾驶舱

驾驶舱消息当前数量和最近的趋势,如图左侧为当前消息总数量,右侧为消息数量趋势。

我们分别解释如下四张图。

1)按broker实例为类目(比如说集群中有3个broker实例)展示当前的消息数

2)按topic为类目(比如说当前所有broker中存在10个topic)展示当前的消息数

3)指定某天和时间段,查询不同broker实例的消息数的趋势

4)指定某天和时间段,查询某topic下消息数的趋势

主题

界面如下

状态

其中状态栏中点位可以理解成队列索引,每个点位存放一条消息,队列中的消息数量由最大点位减去最小点位,当broker按照配置清理已经被消费的消息后,最小点位可能会发生变化。

topic配置
  • perm 有三个值, 2 : 不可读不可写, 4 :只可读 6 : 可读可写

对于单个topic,可以配置在每个broker上messagequeue的数量,又分为读写队列,读写队列说明如下,

读写队列说明

参考:https://blog.csdn.net/weixin_35973945/article/details/123525722

如下,已经实际验证

RocketMQ创建Topic时,可以配置writeQueueNums和readQueueNums,读写队列,是在做路由信息时使用。在消息发送时,使用写队列个数返回路由信息,而消息消费时按照读队列个数返回路由信息。在物理文件层面,只有写队列才会创建文件。

写队列个数是8,设置的读队列个数是4,这个时候,会创建8个文件夹,代表0 1 2 3 4 5 6 7,但在消息消费时,路由信息只返回4,在具体拉取消息时,就只会消费0 1 2 3这4个队列中的消息,4 5 6 7中的信息压根就不会被消费。反过来,如果写队列个数是4,读队列个数是8,在生产消息时只会往0 1 2 3中生产消息,消费消息时则会从0 1 2 3 4 5 6 7所有的队列中消费,当然 4 5 6 7中压根就没有消息 。

readQueueNums>=writeQueueNums,程序才能正常进行,最佳实践是readQueueNums=writeQueueNums。那rocketmq为什么要区分读写队列呢?直接强制r,不就没有问题了吗?rocketmq设置读写队列数的目的在于方便队列的缩容和扩容。

假如一个topic在每个broker上创建了128个队列,现在需要将队列缩容到64个,怎么做才能100%不会丢失消息,并且无需重启应用程序?最佳实践:先缩容写队列128->64,写队列由0 1 2 …127缩至 0 1 2 …63。等到64 65 66…127中的消息全部消费完后,再缩容读队列128->64(同时缩容写队列和读队列可能会导致部分消息未被消费)。

消费者组的消费者数量和读队列有对应关系。一个读队列只对应消费者组的一个消费者。

多个读队列可以对应一个消费者。最多消费者个数为读队列的数量

发送消息

用于模拟消息发送

跳过堆积

指定一个消费分组,这个分组未消费的消息的状态全部置为已消费,执行后,可以在consumer管理页面看到差值为0

重置点位

官方文档 :https://rocketmq.apache.org/zh/docs/featureBehavior/09consumerprogress#%E9%87%8D%E7%BD%AE%E6%B6%88%E8%B4%B9%E4%BD%8D%E7%82%B9

consumer管理
  • 消费者位点 : 表示当前消费者已经消费到了当前messagequeue的第几条消息,
  • 差值 :表示还未消费的消息,
  • 消费者终端,在消费者在线的情况下,会显示对应的ip信息,150.133.36.64@17672#1315792578757901

通过样例熟悉rocketmq

在rocketmq 的源码包中有一个ex

RocketMQ的编程模型

然后RocketMQ的生产者和消费者的编程模型都是有个比较固定的步骤的,掌握这个固定的步骤,对于我们学习源码以及以后使用都是很有帮助的。
消息发送者的固定步骤

  • 1.创建消息生产者producer,并制定生产者组名
  • 2.指定Nameserver地址
  • 3.启动producer
  • 4.创建消息对象,指定主题Topic、Tag和消息体
  • 5.发送消息
  • 6.关闭生产者producer

消息消费者的固定步骤

  • 1.创建消费者Consumer,制定消费者组名
  • 2.指定Nameserver地址
  • 3.订阅主题Topic和Tag
  • 4.设置回调函数,处理消息
  • 5.启动消费者consumer

RocketMq消息样例

官方文档位置:https://rocketmq.apache.org/zh/docs/featureBehavior/01normalmessage

详细说明请查看官方文档。

rocketmq源码包中有一个example项目,包含了诸多示例,这里介绍部分消息相关的示例,无语,示例中部分依赖下载不下来,这里只依赖以下两个jar包,报错的代码全部注释即可

		
		<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>5.0.0</version>
        </dependency>
        # 权限控制部分,不需要可以不引入
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-acl</artifactId>
            <version>5.0.0</version>
        </dependency>

但是在调试这些代码的时候要注意一个问题:这些测试代码中的生产者和消费者都需要依赖NameServer才能运行,只需要将NameServer指向我们自己搭建的RocketMQ集群,而不需要管Broker在哪里,就可以连接我们自己的自己的
RocketMQ集群。而RocketMQ提供的生产者和消费者寻找NameServer的方式有两种:
1、在代码中指定namesrvAddr属性。例如:

consumer.setNamesrvAddr(“k8s-master:9876;k8s-node1:9876;k8s-node2:9876”);
2、通过NAMESRV_ADDR环境变量来指定。多个NameServer之间用分号连
接。

部分代码消息发送超时,设置消息发送超时时间

 producer.setSendMsgTimeout(6000); //默认是3s,这里有点疑问,就是明明没有三秒,还是报错

消息内容说明

SendResult [sendStatus=SEND_OK, msgId=7F000001158C18B4AAC28585F520007E, offsetMsgId=2B8F88CB00002A9F000000000008D8C5, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-a, queueId=0], queueOffset=31]
  • sendStatus : 消息是否发送成功
  • msgId : 消息唯一id
  • offsetMsgId : Broker 服务器的 IP 与端口号、消息的物理偏移量等信息
  • messageQueue : 包含了这条消息所在的位置

RocketMQ msgId与offsetMsgId释疑:https://blog.csdn.net/prestigeding/article/details/104739950

基本样例

部分我们使用消息生产者分别通过三种方式发送消息,同步发送、异步发送以及单向发送。
然后使用消费者来消费这些消息。

1、同步发送消息的样例

见:org.apache.rocketmq.example.simple.Producer
等待消息返回后再继续进行下面的操作。代码如下,似乎不用注释也可以看明白

# 声明生产者组
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
producer.setSendMsgTimeout(6000);
producer.start();
for (int i = 0; i < 128; i++)
try {
Message msg = new Message(TOPIC, TAG, "OrderID188", "Helloworld".getBytes(RemotingHelper.DEFAULT_CHARSET));

SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
}
producer.shutdown();
2、异步发送消息的样例

见:org.apache.rocketmq.example.simple.AsyncProducer
这个示例有个比较有趣的地方就是引入了一个countDownLatch来保证所有消息回调方法都执行完了再关闭Producer。 所以从这里可以看出,RocketMQ的Producer也是一个服务端,在往Broker发送消息的时候也要作为服务端提供服务。

可以看到示例中包含了异常和正常的回调

DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");
producer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
producer.start();

//指示当异步发送流量过大时是否阻塞消息。建议在大流量时启用enableBackpressureForAsyncMode,默认为false
producer.setEnableBackpressureForAsyncMode(true);
//在异步模式下声称发送失败之前在内部执行的最大重试次数,默认为2。这可能会潜在地导致消息复制,这将由应用程序开发人员解决
producer.setRetryTimesWhenSendAsyncFailed(0);

int messageCount = 100;
//由于是异步发送,这里引入一个countDownLatch,保证所有Producer发送消息的回调方法都执行完了再停止Producer服务。
final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
for (int i = 0; i < messageCount; i++) {
	try {
		final int index = i;
		Message msg = new Message("Jodie_topic_1023",
			"TagA","OrderID188","Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
		
		// 注册一个回调
		producer.send(msg, new SendCallback() {
			@Override
			public void onSuccess(SendResult sendResult) {
				countDownLatch.countDown();
				System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
			}

			@Override
			public void onException(Throwable e) {
				countDownLatch.countDown();
				System.out.printf("%-10d Exception %s %n", index, e);
				e.printStackTrace();
			}
		});
	} catch (Exception e) {
		e.printStackTrace();
	}
}
countDownLatch.await(5, TimeUnit.SECONDS);
producer.shutdown();
3、单向发送消息的样例:

org.apache.rocketmq.example.simple.OnewayProducer

和上面两种发送消息的方式最明显的区别就是producer.sendOneway(msg)不需要回执。

DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
producer.start();
for (int i = 0; i < 100; i++) {
	Message msg = new Message("TopicTest" /* Topic */,
			"TagA" /* Tag */,
			("Hello RocketMQ " +
					i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
	);
	producer.sendOneway(msg);
}
//不太理解
Thread.sleep(5000);
producer.shutdown();
4、消费者推模式

官方文档:https://rocketmq.apache.org/zh/docs/4.x/consumer/12push

推模式和拉模式的区别 :https://blog.csdn.net/zjj2006/article/details/124978905

就代码层面,推模式会有一个监听器和consumer绑定,consumer收到消息后会回调监听器的方法,我们可以在方法内部获取消息具体内容

拉模式有类似主动从broker获取消息的代码,并且消费端可以随时终止这个获取消息的过程

推拉模式优缺点 :https://blog.csdn.net/u014532775/article/details/106742978

broker主动向消费者推送消息

这里需要注意的是生产者发送消息的对象是Message,消费者拿到的对象是MessageExt

org.apache.rocketmq.example.simple.PushConsumer

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1");
consumer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
consumer.subscribe("TopicTest", "*");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//wrong time format 2017_0422_221800
//        consumer.setConsumeTimestamp("20181109221800");
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);
		return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
	}
});
consumer.start();
System.out.printf("Consumer Started.%n");
5、消费者拉模式

官方文档:https://rocketmq.apache.org/zh/docs/4.x/consumer/13pull

消费者主动从broker上获取消息

org.apache.rocketmq.example.simple.PullConsumer(示例中DefaultMQPullConsumer效率低下,已经不建议使用)

org.apache.rocketmq.example.simple.LitePullConsumerAssign(消费后需要确认)

public static volatile boolean running = true;

public static void main(String[] args) throws Exception {
	DefaultLitePullConsumer litePullConsumer = new       DefaultLitePullConsumer("please_rename_unique_group_name");
	litePullConsumer.setAutoCommit(false);
	litePullConsumer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
	litePullConsumer.start();
	Collection<MessageQueue> mqSet = litePullConsumer.fetchMessageQueues("TopicTest");
	List<MessageQueue> list = new ArrayList<>(mqSet);
	List<MessageQueue> assignList = new ArrayList<>();
	for (int i = 0; i < list.size() / 2; i++) {
		assignList.add(list.get(i));
	}
	litePullConsumer.assign(assignList);
	litePullConsumer.seek(assignList.get(0), 10);
	try {
		while (running) {
			List<MessageExt> messageExts = litePullConsumer.poll();
			System.out.printf("%s %n", messageExts);
			litePullConsumer.commitSync();
		}
	} finally {
		litePullConsumer.shutdown();
	}

}

org.apache.rocketmq.example.simple.LitePullConsumerSubscribe(消费后自动确认)

public static volatile boolean running = true;

public static void main(String[] args) throws Exception {
	DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("lite_pull_consumer_test");
	litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
	litePullConsumer.subscribe("TopicTest",  "*");
	litePullConsumer.start();
	try {
		while (running) {
			List<MessageExt> messageExts = litePullConsumer.poll();
			System.out.printf("%s%n", messageExts);
		}
	} finally {
		litePullConsumer.shutdown();
	}
}

通常情况下,用推模式比较简单。
实际上RocketMQ的推模式也是由拉模式封装出来的。
DefaultMQPullConsumerImpl这个消费者类已标记为过期,但是还是可以使用的。替换的类是DefaultLitePullConsumerImpl

顺序消息

官网:https://rocketmq.apache.org/zh/docs/featureBehavior/03fifomessage

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xi2NZVLn-1670329636618)(C:\Users\jeol\AppData\Roaming\Typora\typora-user-images\image-20221027161815458.png)]

消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ可以严格的保证消息有序。如上图,在一个messagequeue可能存在多个订单的消息,但是总体而言,同一个订单在队列中是有序的。

生产者示例:org.apache.rocketmq.example.ordermessage.Producer(代码修改了,方便理解)

try {
	DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
	producer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
	producer.start();
	producer.setSendMsgTimeout(60000);

	for (int i = 0; i < 10; i++) {
		int orderId = i;
		// 这里模拟一个订单的多个步骤,每个步骤发送一条消息
		for(int j = 0 ; j <= 5 ; j ++){
			Message msg =
					new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
							("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET)); // 消息内容包含订单id和步骤变编号
			SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
				@Override
				public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
					Integer id = (Integer) arg;
					int index = id % mqs.size();
					return mqs.get(index);
				}
			}, orderId);

			System.out.printf("%s%n", sendResult);
		}
	}

	producer.shutdown();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
	e.printStackTrace();
}
}

消费者示例:org.apache.rocketmq.example.ordermessage.Consumer(代码修改了,方便理解)

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
consumer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");
// 设置开始消费的位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("OrderTopicTest", "*");

consumer.registerMessageListener(new MessageListenerOrderly() {
	AtomicLong consumeTimes = new AtomicLong(0);

	@Override
	public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
		context.setAutoCommit(true);
		for(MessageExt msg:msgs){
			System.out.println("收到消息内容 "+new String(msg.getBody()));
		}
		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;
	}
});

consumer.start();
System.out.printf("Consumer Started.%n");
}

顺序消费实际上有两个核心点,一个是生产者有序存储,另一个是消费者有序消费。

代码层面的逻辑:https://blog.csdn.net/weixin_43767015/article/details/121028059

生产者方面,就示例而言,消费者根据订单id取模,保证了同一个订单的消息都位于broker的同一个messagequeue上,而queue本省就保证了消息的先进先出

消费者层面,rocketmq提供了并发消费的监听器MessageListenerConcurrently,和顺序消费的监听器MessageListenerOrderly,这里我们需要选择顺序消费的监听器,顺序消费会获取messageQueue的锁,**因为顺序消费也是通过线程池消费的,所以这个synchronized锁用来保证同一时刻对于同一个队列只有一个线程去消费它。**如果使用MessageListenerConcurrently进行消费,由于其不会获取锁,那么这个messagequeue上的消息会被多个消费者消费,在多线程的环境下无法保证订单不同步骤的消息被处理的先后顺序,单个消费者的情况下需另外分析。

启动两个消费者的消费结果,不粘贴了,有点长,总之不会出现一个订单被两个消费者同时消费的情况

广播消息

样例:org.apache.rocketmq.example.broadcast.PushConsumer

#冗余代码上面已经介绍过了,不粘贴了,只展示关键代码

 //   BROADCASTING("BROADCASTING"),
//    CLUSTERING("CLUSTERING");
consumer.setMessageModel(MessageModel.BROADCASTING);

我们只需要在消费者端设置消息模式为广播模式即可,

广播消息并没有特定的消息消费者样例,这是因为这涉及到消费者的集群消费模式。在集群状态(MessageModel.CLUSTERING)下,每一条消息只会被同一个消费者组中的一个实例消费到(这跟kafka和rabbitMQ的
集群模式是一样的)。而广播模式则是把消息发给了所有订阅了对应主题的消费者,而不管消费者是不是同一个消费者组。

延时消息

4.X版本文档: https://rocketmq.apache.org/zh/docs/4.x/producer/07message3

5.0版本文档:https://rocketmq.apache.org/zh/docs/featureBehavior/02delaymessage

延迟级别示例

org.apache.rocketmq.example.schedule.ScheduledMessageProducer

就是到特定时间消息才可以非消费者消费

在4.x版本中,只支持设置延迟级别,一共18个级别(5.0是兼容4.x版本)

Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
// This message will be delivered to consumer 10 seconds later.
# 关键代码
message.setDelayTimeLevel(3);
// Send the message
producer.send(message);
# 消费者端获取延时时间
System.currentTimeMillis() - message.getStoreTimestamp()

而是只支持18个固定的延迟级别,1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m
9m 10m 20m 30m 1h 2h

这个延迟级别可以在broker配置文件中进行配置

5.0版本延时级别示例

已经可以设置到

org.apache.rocketmq.example.schedule.TimerMessageProducer

# 以下是关键代码,三行代码实现的效果是一致的,都是延时10s
// This message will be delivered to consumer 10 seconds later.
message.setDelayTimeSec(10);
// The effect is the same as the above
message.setDelayTimeMs(10_000L);
// Set the specific delivery time, and the effect is the same as the above
message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L);
# 消费者端获取时间间隔
System.currentTimeMillis() - message.getBornTimestamp()

5.0版本已经可以自行设置延时时长,但是有以下限制

定时时间必须设置在定时时长范围内,超过范围则定时不生效,服务端会立即投递消息。
定时时长最大值默认为24小时,不支持自定义修改,更多信息,请参见参数限制。
定时时间必须设置为当前时间之后,若设置到当前时间之前,则定时不生效,服务端会立即投递消息。

延时消息有一个特定的消息状态

  • 定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息单独存储在定时存储系统中,等待定时时刻到达。

注意事项

需要避免大量消息的延时时间设置在同一时刻,导致系统压力过大

过滤消息

tag

在大多数情况下,可以使用Message的Tag属性来简单快速的过滤信息。

生产者端 org.apache.rocketmq.example.filter.TagFilterProducer

首先消息内容需要设置对应的tag

DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();

String[] tags = new String[] {"TagA", "TagB", "TagC"};

for (int i = 0; i < 60; i++) {
 // 需要说明的是一个消息中只能设置一个tag
	Message msg = new Message("TagFilterTest",
		tags[i % tags.length],
		"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

	SendResult sendResult = producer.send(msg);
	System.out.printf("%s%n", sendResult);
}

producer.shutdown();

消费者端 org.apache.rocketmq.example.filter.TagFilterConsumer

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
# 关键代码 这句只订阅TagA和TagC的消息,
consumer.subscribe("TagFilterTest", "TagA || TagC");

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);
		return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
	}
});

consumer.start();

System.out.printf("Consumer Started.%n");

TAG是RocketMQ中特有的一个消息属性。RocketMQ的最佳实践中就
建议,使用RocketMQ时,一个应用可以就用一个Topic,而应用中的不
同业务就用TAG来区分。

Apache RocketMQ 的消息过滤功能通过生产者和消费者对消息的属性、标签进行定义,并在 Apache RocketMQ 服务端根据过滤条件进行筛选匹配,将符合条件的消息投递给消费者进行消费。(说明过滤的条件最终会上传到服务端,客户端不会接收到过滤条件之外的数据,避免了带宽占用)

sql

tag过滤在一些比较复杂的场景就有点不足了。 这时候,可以使用SQL表达式来对消息进行过滤。

生产者样例 :org.apache.rocketmq.example.filter.SqlFilterProducer

        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC"};

        for (int i = 0; i < 10; i++) {
            Message msg = new Message("SqlFilterTest",
                tags[i % tags.length],
                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            # 可以为消息设置单独的属性
            msg.putUserProperty("a", String.valueOf(i));

            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }

        producer.shutdown();

消费者样例 :org.apache.rocketmq.example.filter.SqlFilterConsumer

// 关键代码
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)"));

具体的sql语法查看官网 :https://rocketmq.apache.org/zh/docs/featureBehavior/07messagefilter

数值比较,比如:>,>=,<,<=,BETWEEN,=;
字符比较,比如:=,<>,IN;
IS NULL 或者 IS NOT NULL;
逻辑符号 AND,OR,NOT;
常量支持类型为:
数值,比如:123,3.1415;
字符,比如:‘abc’,必须用单引号包裹起来;

NULL,特殊的常量
布尔值,TRUE 或 FALSE

需要在broker配置中开启相关配置(sql也是在服务端完成的过滤)

enablePropertyFilter=true

事务消息

官方地址:https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage

事务是多个业务操作同时成功或者同时失败,分部式事务是指有多个子系统,每个子系统都有对应的事务,需要保证多个子系统之间事务同时成功或同时失败。分布式事务实现方案保证最终一致性的两阶段提交。

示例:

用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统的变更,这些变更要么都同时成功,要么都同时失败

rocketmq的事务消息是保证生产者本地事务和发送消息这两个操作要么同时成功,要么同时失败,至于业务关联的其他子系统的业务最终是否执行成功,可能就由消息的重发机制来保证

事务消息处理流程

我们先了解事务消息的流程,再来看代码

代码样例

:org.apache.rocketmq.example.transaction.TransactionProducer

        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
        producer.setNamesrvAddr("k8s-master:9876;k8s-node1:9876;k8s-node2:9876");

        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        // 半消息发送成功后,用于回查本地事务执行状态的线程,可以不设置,有默认的
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg =
                    new Message("TopicTest", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                // 设置broker首次回查本地事务执行状态的间隔时长
                msg.putUserProperty("CHECK_IMMUNITY_TIME_IN_SECONDS","10000");
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }

​ org.apache.rocketmq.example.transaction.TransactionListenerImpl

public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    //在提交完事务消息后执行。
    // 此方法由主线程执行
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String tags = null;
        System.out.println(Thread.currentThread().getName());
        try {
            // 执行本地事务相关业务代码
            int status = 1;
            // 存放事务的执行结果
            localTrans.put(msg.getTransactionId(), status);
            //返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            e.printStackTrace();
            //返回ROLLBACK_MESSAGE状态的消息会被丢弃。
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        //返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
        // 使用场景大致是数据库宕机本地业务执行失败,先返回unknow,等待数据库恢复后再执行对应业务,然后回查的时候再确认本地事务执行成功
//        return LocalTransactionState.UNKNOW;
    }

    // broker一段时间后未接受到事务状态的响应,会调用这个方法进行回查
    // 此方法由线程池的线程执行
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {

        //根据消息内容查询业务逻辑是否执行成功
        Integer status = localTrans.get(msg.getTransactionId());
        System.out.println(Thread.currentThread().getName());

        if(status ==1 ){
            return LocalTransactionState.COMMIT_MESSAGE;
        }else if(status == 2){
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
}
事务消息使用限制

事务消息的使用限制:

  • 1、事务消息不支持延迟消息和批量消息。
  • 2、为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话
  • ( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  • 3、事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于transactionMsgTimeout 参数。
  • 4、事务性消息可能不止一次被检查或消费。
  • 5、提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
批量消息

这里调用非常简单,将消息打包成 Collection<Message> msgs 传入方法中即可,需要注意的是批量消息的大小不能超过 1MiB(否则需要自行分割),其次同一批 batch 中 topic 必须相同。

简单批量org.apache.rocketmq.example.batch.SimpleBatchProducer

DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
// Uncomment the following line while debugging, namesrvAddr should be set to your local address
//        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
producer.start();

//If you just send messages of no more than 1MiB at a time, it is easy to use batch
//Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support
List<Message> messages = new ArrayList<>();
messages.add(new Message(TOPIC, TAG, "OrderID001", "Hello world 0".getBytes(StandardCharsets.UTF_8)));
messages.add(new Message(TOPIC, TAG, "OrderID002", "Hello world 1".getBytes(StandardCharsets.UTF_8)));
messages.add(new Message(TOPIC, TAG, "OrderID003", "Hello world 2".getBytes(StandardCharsets.UTF_8)));

SendResult sendResult = producer.send(messages);
System.out.printf("%s", sendResult);

拆分后再批量发送

class ListSplitter implements Iterator<List<Message>> {
    private static final int SIZE_LIMIT = 1000 * 1000;
    private final List<Message> messages;
    private int currIndex;

    public ListSplitter(List<Message> messages) {
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
        return currIndex < messages.size();
    }

    @Override
    public List<Message> next() {
        int nextIndex = currIndex;
        int totalSize = 0;
        for (; nextIndex < messages.size(); nextIndex++) {
            Message message = messages.get(nextIndex);
            int tmpSize = message.getTopic().length() + message.getBody().length;
            Map<String, String> properties = message.getProperties();
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }
            //for log overhead
            tmpSize = tmpSize + 20;
            if (tmpSize > SIZE_LIMIT) {
                //it is unexpected that single message exceeds the sizeLimit
                //here just let it go, otherwise it will block the splitting process
                if (nextIndex - currIndex == 0) {
                    //if the next sublist has no element, add this one and then break, otherwise just break
                    nextIndex++;
                }
                break;
            }
            if (tmpSize + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }

        }
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Not allowed to remove");
    }
}
        //large batch
        List<Message> messages = new ArrayList<>(MESSAGE_COUNT);
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            messages.add(new Message(TOPIC, TAG, "OrderID" + i, ("Hello world " + i).getBytes(StandardCharsets.UTF_8)));
        }

        //split the large batch into small ones:
        ListSplitter splitter = new ListSplitter(messages);
        while (splitter.hasNext()) {
            List<Message> listItem = splitter.next();
            SendResult sendResult = producer.send(listItem);
            System.out.printf("%s", sendResult);
        }

ACL权限控制

权限控制(ACL)主要为RocketMQ提供Topic资源级别的用户访问控制。用户在使用RocketMQ权限控制时,可以在Client客户端通过 RPCHook注入AccessKey和SecretKey签名;同时,将对应的权限控制属性(包括Topic访问权限、IP白名单和
AccessKey和SecretKey签名等)设置在$ROCKETMQ_HOME/conf/plain_acl.yml的配置文件中。Broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常;需要引入依赖rocketmq-acl;

样例:org.apache.rocketmq.example.simple.AclClient

// 客户端关键代码
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook());
// 消费端关键代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5", getAclRPCHook(), new AllocateMessageQueueAveragely());

// 设置权限对象
    static RPCHook getAclRPCHook() {
        return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY,ACL_SECRET_KEY));
    }

broker需要开启权限控制

而Broker端具体的配置信息可以参见源码包下docs/cn/acl/user_guide.md。主要是在broker.conf中打开acl的标志:aclEnable=true。然后就可以用plain_acl.yml来进行权限配置了。并且这个配置文件是热加载的,也就是说要修改
配置时,只要修改配置文件就可以了,不用重启Broker服务。我们来简单分析下源码中的plan_acl.yml的配置:

#全局白名单,不受ACL控制
#通常需要将主从架构中的所有节点加进来
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*
accounts:
#第一个账户
- accessKey: RocketMQ
secretKey: 12345678
whiteRemoteAddress:
admin: false
defaultTopicPerm: DENY #默认Topic访问策略是拒绝
defaultGroupPerm: SUB #默认Group访问策略是只允许订阅
topicPerms:
- topicA=DENY #topicA拒绝
- topicB=PUB|SUB #topicB允许发布和订阅消息
- topicC=SUB #topicC只允许订阅
groupPerms:
# the group should convert to retry topic
- groupA=DENY
- groupB=PUB|SUB
- groupC=SUB
#第二个账户,只要是来自192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2
secretKey: 12345678
whiteRemoteAddress: 192.168.1.*

# if it is admin, it could access all resources
admin: true

rocketMq使用中常见问题

使用RocketMQ如何保证消息不丢失?

哪些环节会有丢消息的可能?
我们考虑一个通用的MQ场景:

其中,1,2,4三个场景都是跨网络的,而跨网络就肯定会有丢消息的可能。

1: 生产者消息发送到broker

事务消息机制,保证本地事务和发送消息两个操作的原子性。

2:主从之间消息同步

多节点(集群)多副本模式-同步双写

如果主节点宕机,那么消费者会从从节点消费消息,这时,如果主从之间数据不一致,可能导致消息丢失,这里选择

# SYNC_MASTER/ASYNC_MASTER/SLAVE 前两个对应两种集群模式,其中SLAVE对应从节点
brokerRole=SYNC_MASTER

这样接收到了消息,就会等主备都写成功,才向应用返回成功

Dledger的文件同步

Dledger也是一种broker集群的部署模式

在使用Dledger技术搭建的RocketMQ集群中,Dledger会通过两阶段提交的方式保证文件在主从之间成功同步。

简单来说,数据同步会通过两个阶段,一个是uncommitted阶段,一个是commited阶段。

Leader Broker上的Dledger收到一条数据后,会标记为uncommitted状态,然后他通过自己的DledgerServer组件把这个uncommitted数据发给Follower Broker的DledgerServer组件。接着Follower Broker的DledgerServer收到uncommitted消息之后,必须返回一个ack给Leader Broker的Dledger。然后如果LeaderBroker收到超过半数的Follower Broker返回的ack之后,就会把消息标记为committed状态。
 再接下来, Leader Broker上的DledgerServer就会发送committed消息给Follower Broker上DledgerServer,让他们把消息也标记为committed状态。这样,就基于Raft协议完成了两阶段的数据同步
3、消息写入磁盘

通常MQ存盘时都会先写入操作系统的缓存page cache中,然后再由操作系统异步的将消息写入硬盘。这个中间有个时间差,就可能会造成消息丢失。如果服务挂了,缓存中还没有来得及写入硬盘的消息就会丢失。
这个是MQ场景都会面对的通用的丢消息问题。那我们看看用RocketMQ时要如何
解决这个问题

# flushDiskType表示刷盘策略,分为SYNC_FLUSH和ASYNC_FLUSH两种,分别代表同步刷盘和异步刷盘。同步刷盘情况下,消息真正写入磁盘后再返回成功状态;异步刷盘情况下,消息写入page_cache后就返回成功状态。
flushDiskType=SYNC_FLUSH
4、消费者端不要使用异步消费机制

正常情况下,消费者端都是需要先处理本地事务,然后再给MQ一个ACK响应,这时MQ就会修改Offset,将消息标记为已消费,从而不再往其他消费者推送消息。所以在Broker的这种重新推送机制下,消息是不会在传输过程中丢失的。但是也会有下面这种情况会造成服务端消息丢失:这种异步消费的方式,就有可能造成消息状态返回后消费者本地业务逻辑处理失败造成消息丢失的可能

DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("please_rename_unique_group_name_4");
consumer.registerMessageListener(new MessageListenerConcurrently()
{
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
new Thread(){
public void run(){
//处理业务逻辑
System.out.printf("%s Receive New Messages: %s %n",
Thread.currentThread().getName(), msgs);
}
};
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
5、NameServer挂了如何保证消息不丢失?

NameServer在RocketMQ中,是扮演的一个路由中心的角色,提供到Broker的路由功能。但是其实路由中心这样的功能,在所有的MQ中都是需要的。kafka是用zookeeper和一个作为Controller的Broker一起来提供路由服务,整个功能是相当
复杂纠结的。而RabbitMQ是由每一个Broker来提供路由服务。而只有RocketMQ把这个路由中心单独抽取了出来,并独立部署。

这个NameServer之前都了解过,集群中任意多的节点挂掉,都不会影响他提供的路由功能。那如果集群中所有的NameServer节点都挂了呢?有很多人就会认为在生产者和消费者中都会有全部路由信息的缓存副本,那整个服
务可以正常工作一段时间。其实这个问题大家可以做一下实验,当NameServer全部挂了后,生产者和消费者是立即就无法工作了的。至于为什么,可以去源码中找找答案。
那再回到我们的消息不丢失的问题,在这种情况下,RocketMQ相当于整个服务都不可用了,那他本身肯定无法给我们保证消息不丢失了。我们只能自己设计一个降级方案来处理这个问题了。例如在订单系统中,如果多次尝试发送RocketMQ不成功,那就只能另外找给地方(Redis、文件或者内存等)把订单消息缓存下来,然后起一个线程定时的扫描这些失败的订单消息,尝试往RocketMQ发送。这样等RocketMQ的服务恢复过来后,就能第一时间把这些消息重新发送出去。整个这套
降级的机制,在大型互联网项目中,都是必须要有的。

6、RocketMQ消息零丢失方案总结
完整分析过后,整个RocketMQ消息零丢失的方案其实挺简单

生产者使用事务消息机制。
Broker配置同步刷盘+Dledger主从架构
消费者不要使用异步消费。
整个MQ挂了之后准备降级方案

那这套方案是不是就很完美呢?其实很明显,这整套的消息零丢失方案,在各个环节都大量的降低了系统的处理性能以及吞吐量。在很多场景下,这套方案带来的性能损失的代价可能远远大于部分消息丢失的代价。所以,我们在设计RocketMQ使用方案时,要根据实际的业务情况来考虑。例如,如果针对所有服务器都在同一个机房的场景,完全可以把Broker配置成异步刷盘来提升吞吐量。而在有些对消息可靠性要求没有那么高的场景,在生产者端就可以采用其他一些更简单的方案来提升吞吐,而采用定时对账、补偿的机制来提高消息的可靠性。而如果消费者不需要进行消息存盘,那使用异步消费的机制带来的性能提升也是非常显著的。总之,这套消息零丢失方案的总结是为了在设计RocketMQ使用方案时的一个很好的参考

事务消息相关问题

1、为什么要发送个half消息?有什么用?
这个half消息是在订单系统进行下单操作前发送,并且对下游服务的消费者是不可见的。那这个消息的作用更多的体现在确认RocketMQ的服务是否正常。相当于嗅探下RocketMQ服务是否正常,并且通知RocketMQ,我马上就要发一个很重要的消息了,你做好准备。
2.half消息如果写入失败了怎么办?
如果没有half消息这个流程,那我们通常是会在订单系统中先完成下单,再发送消息给MQ。这时候写入消息到MQ如果失败就会非常尴尬了。而half消息如果写入失败,我们就可以认为MQ的服务是有问题的,这时,就不能通知下游服务了。我们可以在下单时给订单一个状态标记,然后等待MQ服务正常后再进行补偿操作,等MQ服务正常后重新下单通知下游服务。

3.订单系统写数据库失败了怎么办?
这个问题我们同样比较下没有使用事务消息机制时会怎么办?如果没有使用事务消息,我们只能判断下单失败,抛出了异常,那就不往MQ发消息了,这样至少保证不会对下游服务进行错误的通知。但是这样的话,如果过一段时间数据库恢复过来了,这个消息就无法再次发送了。当然,也可以设计另外的补偿机制,例如将订单数据缓存起来,再启动一个线程定时尝试往数据库写。而如果使用事务消息机制,就可以有一种更优雅的方案。如果下单时,写数据库失败(可能是数据库崩了,需要等一段时间才能恢复)。那我们可以另外找个地方把订单消息先缓存起来(Redis、文本或者其他方式),然后给
RocketMQ返回一个UNKNOWN状态。这样RocketMQ就会过一段时间来回查事务状态。我们就可以在回查事务状态时再尝试把订单数据写入数据库,如果数据库这时候已经恢复了,那就能完整正常的下单,再继续后面的业务。这样这个订单的消息就不会因为数据库临时崩了而丢失。

4.half消息写入成功后RocketMQ挂了怎么办?
我们需要注意下,在事务消息的处理机制中,未知状态的事务状态回查是由RocketMQ的Broker主动发起的。也就是说如果出现了这种情况,那RocketMQ就不会回调到事务消息中回查事务状态的服务。这时,我们就可以将订单一直标记为"新下单"的状态。而等RocketMQ恢复后,只要存储的消息没有丢失,RocketMQ就会再次继续状态回查的流程。

5.下单成功后如何优雅的等待支付成功?
在订单场景下,通常会要求下单完成后,客户在一定时间内,例如10分钟,内完成订单支付,支付完成后才会通知下游服务进行进一步的营销补偿。
如果不用事务消息,那通常会怎么办?
最简单的方式是启动一个定时任务,每隔一段时间扫描订单表,比对未支付的订单的下单时间,将超过时间的订单回收。这种方式显然是有很大问题的,需要定时扫描很庞大的一个订单信息,这对系统是个不小的压力。
那更进一步的方案是什么呢?是不是就可以使用RocketMQ提供的延迟消息机制。
往MQ发一个延迟1分钟的消息,消费到这个消息后去检查订单的支付状态,如果订单已经支付,就往下游发送下单的通知。而如果没有支付,就再发一个延迟1分钟的消息。最终在第十个消息时把订单回收。这个方案就不用对全部的订单表进行扫描,而只需要每次处理一个单独的订单消息。
那如果使用上了事务消息呢?我们就可以用事务消息的状态回查机制来替代定时的任务。在下单时,给Broker返回一个UNKNOWN的未知状态。而在状态回查的方法中去查询订单的支付状态。这样整个业务逻辑就会简单很多。我们只需要配置RocketMQ中的事务消息回查次数(默认15次)和事务回查间隔时间(messageDelayLevel),就可以更优雅的完成这个支付状态检查的需求。

使用RocketMQ如何保证消息顺序

MQ的顺序问题分为全局有序和局部有序。
全局有序:整个MQ系统的所有消息严格按照队列先入先出顺序进行消费。
局部有序:只保证一部分关键消息的消费顺序。

首先 我们需要分析下这个问题,在通常的业务场景中,全局有序和局部有序哪个更重要?其实在大部分的MQ业务场景,我们只需要能够保证局部有序就可以了。例如我们用QQ聊天,只需要保证一个聊天窗口里的消息有序就可以了。而对于电商订单场景,也只要保证一个订单的所有消息是有序的就可以了。至于全局消息的顺序,并不会太关心。而通常意义下,全局有序都可以压缩成局部有序的问题。例如以前我们常用的聊天室,就是个典型的需要保证消息全局有序的场景。但是这种场景,通常可以压缩成只有一个聊天窗口的QQ来理解。即整个系统只有一个聊天通道,这样就可以用QQ那种保证一个聊天窗口消息有序的方式来保证整个系统的全局消息有序。然后 落地到RocketMQ。通常情况下,发送者发送消息时,会通过MessageQueue轮询的方式保证消息尽量均匀的分布到所有的MessageQueue上,而消费者也就同样需要从多个MessageQueue上消费消息。而MessageQueue是RocketMQ存储消息的最小单元,他们之间的消息都是互相隔离的,在这种情况下,是无法保证消息全局有序的。

而对于局部有序的要求,只需要将有序的一组消息都存入同一个MessageQueue里,这样MessageQueue的FIFO设计天生就可以保证这一组消息的有序。RocketMQ中,可以在发送者发送消息时指定一个MessageSelector对象,让这个
对象来决定消息发入哪一个MessageQueue。这样就可以保证一组有序的消息能够发到同一个MessageQueue里。
另外,通常所谓的保证Topic全局消息有序的方式,就是将Topic配置成只有一个MessageQueue队列(默认是4个)。这样天生就能保证消息全局有序了。这个说法其实就是我们将聊天室场景压缩成只有一个聊天窗口的QQ一样的理解方式。而这种方式对整个Topic的消息吞吐影响是非常大的,如果这样用,基本上就没有用MQ的必要了。

使用RocketMQ如何快速处理积压消息?

1、如何确定RocketMQ有大量的消息积压?

对于RocketMQ来说,有个最简单的方式来确定消息是否有积压。那就是使用web控制台,就能直接看到消息的积压情况。

另外,也可以通过mqadmin指令在后台检查各个Topic的消息延迟情况。

如何处理

如果Topic下的MessageQueue配置得是足够多的,那每个Consumer实际上会分配多个MessageQueue来进行消费。这个时候,就可以简单的通过增加Consumer的服务节点数量来加快消息的消费,等积压消息消费完了,再恢复成正常情况。最
极限的情况是把Consumer的节点个数设置成跟MessageQueue的个数相同。但是如果此时再继续增加Consumer的服务节点就没有用了。

而如果Topic下的MessageQueue配置得不够多的话,那就不能用上面这种增加Consumer节点个数的方法了。这时怎么办呢? 这时如果要快速处理积压的消息,可以创建一个新的Topic,配置足够多的MessageQueue。然后把所有消费者节点
的目标Topic转向新的Topic,并紧急上线一组新的消费者,只负责消费旧Topic中的消息,并转储到新的Topic中,这个速度是可以很快的。然后在新的Topic上,就可以通过增加消费者个数来提高消费速度了。之后再根据情况恢复成正常情况

RocketMQ的消息轨迹

1、RocketMQ消息轨迹数据的关键属性:

Producer端Consumer端Broker端
生产实例信息消费实例信息消息的Topic
发送消息时间投递时间,投递轮次消息存储位置
消息是否发送成功消息是否消费成功消息的Key值
发送耗时消费耗时消息的Tag值

2、消息轨迹配置
打开消息轨迹功能,需要在broker.conf中打开一个关键配置:
这个配置的默认值是false。也就是说默认是关闭的。

traceTopicEnable=true 

3、消息轨迹数据存储
默认情况下,消息轨迹数据是存于一个系统级别的Topic
,RMQ_SYS_TRACE_TOPIC。这个Topic在Broker节点启动时,会自动创建出来。

另外,也支持客户端自定义轨迹数据存储的Topic。在客户端的两个核心对象 DefaultMQProducer和DefaultMQPushConsumer,他们的构造函数中,都有两个可选的参数来打开消息轨迹存储

  • enableMsgTrace:是否打开消息轨迹。默认是false。
  • customizedTraceTopic:配置将消息轨迹数据存储到用户指定的Topic 。

日志配置

官方文档:https://rocketmq.apache.org/zh/docs/4.x/bestPractice/20log

记录一下,springBoot中日志示例配置

<    <!-- rocketmq日志 参考rocketmq-dashboard配置-->

    <appender name="RocketmqClientAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logHome}/mq/rocketmq-console.log</file>
        <append>true</append>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logHome}/mq/history/rocketmq-console-%d{yyyy-MM-dd}.%i.log
            </fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--100M-->
                <maxFileSize>104857600</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <MaxHistory>10</MaxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %p %t - %m%n</pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
    </appender>
    
            <!-- additivity="false" 只在当前记录器记录,不会在根记录器再次记录 -->

        <logger name="RocketmqClient" additivity="false">
            <level value="warn" />
            <appender-ref ref="RocketmqClientAppender"/>
        </logger>

        <logger name="RocketmqCommon" additivity="false">
            <level value="warn" />
            <appender-ref ref="RocketmqClientAppender"/>
        </logger>

        <logger name="RocketmqRemoting" additivity="false">
            <level value="warn" />
            <appender-ref ref="RocketmqClientAppender"/>
        </logger>

暂时只能通过启动参数或者在springbootapplication初始化前设置系统参数,才能改变日志文件位置

@SpringBootApplication
@Slf4j
public class Demo2Application {

    /**
     * 用于初始化rocketmq 日志打印的参数, 参考 ClientLogger的静态变量定义,和默认配置
     * 也可以通过-Drocketmq.client.logRoot=/home/admin/logs -Drocketmq.client.logLevel=WARN -Drocketmq.client.logFileMaxIndex=20  -Drocketmq.client.logFileMaxSize=67108864
     * */
    static {
        // 会将rocketmq日志,似乎大部分是建立连接,连接信息,连接异常信息等,这个会将日志桥接到slf4j,混杂着看也影响先有的日志,注释掉
        System.setProperty(ClientLogger.CLIENT_LOG_USESLF4J,"true");
        // 定义mq日志的根目录,默认是在用户目录 logs文件夹下
//        System.setProperty(ClientLogger.CLIENT_LOG_ROOT,"./MYLOG/mq/");

    }
    
    .......

非spring中的项目,同样可以初始化系统参数

org.apache.rocketmq.client.log.ClientLogger

vip通道

Broker监听客户端请求的端口,而其他两个端口是根据listenPort的值,动态计算出来的。这三个端口由Broker内部不同的组件使用,作用分别如下:

**remotingServer:**监听listenPort配置项指定的监听端口,默认10911

**fastRemotingServer:**监听端口值listenPort-2,即默认为10909

**HAService:**监听端口为值为listenPort+1,即10912,该端口用于Broker的主从同步

vipChannelEnabled 可以控制是否走broker的fastRemotingServer端口发送消息

关联信息

  • 关联的主题:
  • 上一篇:
  • 下一篇:
  • image: 20221026/1
  • 转载自:
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值