RocketMQ学习笔记

e尽量使用jdk1.8 , 若要使用其他版本参考这篇文档

基本概念

Client端
Producer Group
一类Producer的集合名称,这类Producer通常发送一类消息,且发送逻辑一致
Consumer Group
一类Consumer的集合名称,这类Consumer通常消费一类消息,且消费逻辑一致
Server端

Broker
消息中转角色,负责存储消息,转发消息,这里就是RocketMQ Server
Topic
消息的主题,用于定义并在服务端配置,消费者可以按照主题进行订阅,也就是消息分类,通常一个系统一个Topic
Message
在生产者、消费者、服务器之间传递的消息,一个message必须属于一个Topic
消息是要传递的信息。邮件中必须包含一个主题,该主题可以解释为要发送给您的信的地址。消息还可能具有可选标签和额外的键值对。例如,您可以为消息设置业务密钥,然后在代理服务器上查找消息以在开发过程中诊断问题。

Namesrver
一个无状态的名称服务,可以集群部署,每一个broker启动的时候都会向名称服务器注册,主要是接收broker的注册,接收客户端的路由请求并返回路由信息

Offset
偏移量,消费者拉取消息时需要知道上一次消费到了什么位置, 这一次从哪里开始

Partition
分区,Topic物理上的分组,一个Topic可以分为多个分区,每个分区是一一个有序的队列。
分区中的每条消息都会给分配一个有序的ID,也就是偏移量,保证了顺序,消费的正确性

Tag
用于对消息进行过滤,理解为message的标记,同一业务不同目的的message可以用相同的topic但是
可以用不同的tag来区分

key
消息的KEY字段是为了唯- -表示消息的,方便查问题,不是说必须设置,只是说设置为了方便开发和运维定位问题。
比如:这个KEY可以是订单ID等

配置完,环境变量之后
使用在其 bin 目录使用 以启动 命名服务

start mqnamesrv.cmd
start mqbroker.cmd -n (命名服务器的地址) [autoCreateTopicEnable=true // 开启自动创建话题]
start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true

broker配置文件

默认在 conf/broker.conf 目录下

#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#设置启动的ip
brokerIP1=127.0.0.1
#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

启动命令

sh mqbroker -c (配置文件路径)

nameserv配置文件

stenPort = 6541
# serverWorkerThreads = 8
# serverCallbackExecutorThreads = 0
# serverSelectorThreads = 3
# serverOnewaySemaphoreValue = 256
# serverAsyncSemaphoreValue = 64
# serverChannelMaxIdleTimeSeconds = 120

# serverSocketSndBufSize = NettySystemConfig.socketSndbufSize
# serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize
# writeBufferHighWaterMark = NettySystemConfig.writeBufferHighWaterMark
# writeBufferLowWaterMark = NettySystemConfig.writeBufferLowWaterMark
# serverSocketBacklog = NettySystemConfig.socketBacklog
# private boolean serverPooledByteBufAllocatorEnable = true

启动方式

sh mqnamesrv -c (配置文件所在的路径名)

rocketmq 命令行测试工具

首先配置环境变量

变量名 NAMESRV_ADDR
变量值 127.0.0.1:9876
tools.cmd org.apache.rocketmq.example.quickstart.Producer
tools.cmd org.apache.rocketmq.example.quickstart.Consumer

图形化界面

项目地址 https://github.com/apache/rocketmq-externals
已经更新rocketMq console 到 https://github.com/apache/rocketmq-dashboard
新项目好像有问题 用老版 https://github.com/apache/rocketmq-externals/tree/rocketmq-console-1.0.0/rocketmq-console

编写生产者

引入依赖

<dependency>
  <groupId>org.apache.rocketmq</groupId>
  <artifactId>rocketmq-client</artifactId>
  <version>4.9.4</version>
</dependency>
        // 谁来发
        DefaultMQProducer defaultMQProducer =
                new DefaultMQProducer("group1");
        // 发给谁
        defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");

        try {
            // 怎么发
            defaultMQProducer.start();
            // 发什么
            SendResult send = defaultMQProducer.send(new Message("topic", "tag1", "hello".getBytes(StandardCharsets.UTF_8)));
            // 发的结果是什么
            System.out.println(send);
            // 打扫战场
            defaultMQProducer.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }

编写消费者

// 谁来收
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer");
// 从那收
consumer.setNamesrvAddr("127.0.0.1:9876");
try {
    // 监听那个消息队列
    consumer.subscribe("topic" , "*");
    //处理业务流程 注册监听器
    consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
        // 业务逻辑
        for (MessageExt x : msgs){
            System.out.println(new String(x.getBody()));
            
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    });
    consumer.start();
    
} catch (MQClientException e) {
    e.printStackTrace();
}

消费者模式

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer");

同话题,同组

会均摊这个话题的消息
image.png

同话题,不同组

则都会收到这个话题的消息
image.png

消费模式

若想在同组同的多个消费者都能接受到完整的消息,可以设置其消费模式 为广播模式

// 设置消费模式 默认使用的是 CLUSTERING[集群模式]
consumer.setMessageModel(MessageModel.BROADCASTING);

完整代码

// 谁来收
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer");
        // 从那收
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 设置消费模式 默认使用的是 CLUSTERING[集群模式]
        consumer.setMessageModel(MessageModel.BROADCASTING);
        try {
            // 监听那个消息队列
            consumer.subscribe("topic2" , "*");
            //处理业务流程 注册监听器
            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                // 业务逻辑
                for (MessageExt x : msgs){
                    System.out.println(new String(x.getBody()));
                    System.out.println("======================");
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            consumer.start();

        } catch (MQClientException e) {
            e.printStackTrace();
        }

同步消息

及时性较强,重要的消息,且必须有回执的消息
image.png

SendResult send = defaultMQProducer.send(new Message("topic2" , "tag1", ("hello" + i).getBytes(StandardCharsets.UTF_8)));

异步消息

及时性较弱,但是需要有回执的消息(回执消息不会立即返回)
image.png

@Override
    public void send(Message msg,
        SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {
        msg.setTopic(withNamespace(msg.getTopic()));
        this.defaultMQProducerImpl.send(msg, sendCallback);
    }
// 发什么,发送的是同步的消息
defaultMQProducer.send(message, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
         System.out.println((new Date()).toString() + sendResult);
    }

   @Override
   public void onException(Throwable e) {
          e.printStackTrace();
   }
});
// 若要等待回执 不要使用 defaultMQProducer.shutdown() 的方法 关闭生产者

单向消息

不需要回执的消息
image.png

defaultMQProducer.sendOneway(mes);

延迟消息

消息发送时并不是直接发送到消息服务器,而是根据设定的时间等待送达,起到延迟到达的缓冲作用
image.png
延时消息设置在message上 , 可以设置其延时等级

message.setDelayTimeLevel(3);

image.png
依次对应 等级 1、2、3 ……

批量消息

注意点

  • 这些批量消息应该有相同的topic
  • 相同的 waitStoreMsgOK (消息类型)
  • 不能是延时消息
  • 消息内容总长度不超过4M

消息内容总长度包含如下:

  • topic(字符串字节数)
  • body(字节数组长度)
  • 消息追加的属性(key与value对应字符串字节数)
  • 日志(固定20字节)

生产者的 send方法里存在

public SendResult send(
        Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException, InterruptedException 

他支持传入一个集合,用于发送批量消息

消费者 默认一次监听 只支持消费一个消息 若要消费多个消息可以设置

consumer.setConsumeMessageBatchMaxSize(5);

tag过滤

消费者可以设置过滤表达式来接受所需要的消息

consumer.subscribe("topic8" , "vip || normal");

其中 * 标识接受所有的 TAG

SQL过滤

前置 RocketMq 需要开启配置 以支持SQL 过滤
进入到安装目录下的 /conf/broker.conf 文件

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# start  sql   Filter
enablePropertyFilter=true

加入 enablePropertyFilter=true 用以支持SQL 过滤
重启broker

start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true

或者直接cmd中输入 (windows 平台下可能不能 立即生效,使用下面命令可以立即生效)

mqadmin.cmd updateBrokerConfig -blocalhost:10911 -kenablePropertyFilter -vtrue

在 console 界面 可以查看到开启与否
image.png
生产者可以 putUserProperty方法 设置消息的属性
x.putUserProperty("age" , String.valueOf(RandomUtil.randomInt(10,100)));
消费者 监听对应的表达式即可
consumer.subscribe("topic9" , MessageSelector.bySql("age > 16"));

spring boot 整合生产者

引入start

<dependency>
  <groupId>org.apache.rocketmq</groupId>
  <artifactId>rocketmq-spring-boot-starter</artifactId>
  <version>2.2.2</version>
</dependency>

添加配置

rocketmq:
  name-server: 127.0.0.1:9876 #nameserver 地址
  producer:
    group: group1 # 生产者的组别

注入template

@Resource
RocketMQTemplate rocketMQTemplate;

通过rocketMQTemplate 发送消息

1.可以通过 send 方法 发送消息,不过需要 springframework 的message消息,可以通过 messagebuild构造

rocketMQTemplate.send(MessageBuilder.withPayload("hh").build());

2.可以通过 convertAndSend 方法发送消息,第一个参数填写 主题,第二个参数填写需要发送的消息体

public void convertAndSend(D destination, Object payload) 
rocketMQTemplate.convertAndSend("topic8" , new User("gf" , 15));

3.通过 rocketMQTemplate 的 getProducer() 方法 可以拿到 DefaultMQProducer 然后通过原始的方法 发送消息

DefaultMQProducer producer = rocketMQTemplate.getProducer();

整合消费者

继承 RocketMQListener 接口实现方法 (T 为接收消息的类型)

public class CustomerService implements RocketMQListener<User> {
    @Override
    public void onMessage(User user) {
        System.out.println(user);
    }
}

使用 RocketMQMessageListener 注解来设置消费者的细节

关注问题 当使用 该注解 第一次运行监听 topic8 ,停下来,改为监听topic15 再运行 ,还是能监听到 topic8的消息,未改组别

  • RocketMQListener接口:消费者都需实现该接口的消费方法onMessage(msg)。
  • RocketMQPushConsumerLifecycleListener接口:当@RocketMQMessageListener中的配置不足以满足我们的需求时,可以实现该接口直接更改消费者类DefaultMQPushConsumer配置
  • @RocketMQMessageListener:被该注解标注并实现了接口RocketMQListener的bean为一个消费者并监听指定topic队列中的消息,该注解中包含消费者的一些常用配置(大部分按默认即可),一般只需更改consumerGroup(消费组)与topic。RocketMQMessageListener中的属性配置是可以从配置文件或配置中心获取的
  • RocketMQLocalTransactionListener接口:可以发送事务型消息,它里面有两个方法executeLocalTransaction和checkLocalTransaction,用于实现执行本地事务和事务回查的两个方法。前者就是我们需要在事务型消息可以被消费之前需要在本地执行的事物操作,只有本地事务提交后发送到MQ中的事物消息才对Consumer可见,否则如果本地事务执行失败,那么消息队列中的消息也会回滚;如果超过一定时间还本地事务还没有提交,就会调用checkLocalTransaction执行本地事务回查。
  • @RocketMQTransactionListener:配合RocketMQLocalTransactionListener接口一起使用,一般只需要修改txProducerGroup。

其余配置

这里重点说明一下发送 带 tag 的消息 和 userproperty (用于消费端的sql 过滤)的消息

发送带 tag 的消息

rocketTemplate 发送带tag 的消息只需要在填写 topic 的地方后面追加 ':tag',推送消息就会把tag带上,
例如

rocketMQTemplate.syncSend("topic10:admin", new User("tag fliter" , 15))

其中 admin 即使该消息的 tag

原理

rocketmqTemplate 在 发送消息中 处理过程中 通过处理 destination 字段截取出 tag 的信息

String[] tempArr = destination.split(":", 2);
String topic = tempArr[0];
String tags = "";
if (tempArr.length > 1) {
   tags = tempArr[1];
}

接受 带tag 的消息

只需要在_ @RocketMQMessageListener 的注解上添加 _selectorExpression 属性 就能过滤出所需要的消息

@RocketMQMessageListener(topic = "topic10" ,consumerGroup = "cu1",selectorExpression = "admin")

发送 带 userproperty (用于消费端的sql 过滤)

通过 org.springframework.messaging 包下的 Message 在 hander 中添加 键值对 就可以添加userproperty

Message<User> build = MessageBuilder.withPayload(new User("sql filter", 15))
                .setHeader("iq", 15)
                .setHeader("eq", "80")
                .build();

rocketMQTemplate.syncSend("topic10" , build).toString()

注意:添加的key 不要和 以下 一样

public static final String PREFIX = "rocketmq_";
public static final String KEYS = "KEYS";
public static final String TAGS = "TAGS";
public static final String TOPIC = "TOPIC";
public static final String MESSAGE_ID = "MESSAGE_ID";
public static final String BORN_TIMESTAMP = "BORN_TIMESTAMP";
public static final String BORN_HOST = "BORN_HOST";
public static final String FLAG = "FLAG";
public static final String QUEUE_ID = "QUEUE_ID";
public static final String SYS_FLAG = "SYS_FLAG";
public static final String TRANSACTION_ID = "TRANSACTION_ID";
public static final String DELAY = "DELAY";
public static final String WAIT = "WAIT";
"WAIT_STORE_MSG_OK"

原理

在处理其发送的消息时 会 遍历 header 的每个属性 并排除键值带有 FLAG 和 WAIT_STORE_MSG_OK

headers.entrySet().stream()
                .filter(entry -> !Objects.equals(entry.getKey(), "FLAG")
                    && !Objects.equals(entry.getKey(), "WAIT_STORE_MSG_OK")) // exclude "FLAG", "WAIT_STORE_MSG_OK"
                .forEach(entry -> {
                    if (!MessageConst.STRING_HASH_SET.contains(entry.getKey())) {
                        rocketMsg.putUserProperty(entry.getKey(), String.valueOf(entry.getValue()));
                    }
                });

接受带有 user property的消息

rocketmq 要开启 sql 过滤 切记切记
只需要在_ @RocketMQMessageListener 的注解上添加 _selectorExpression 属性 并设置 selectorType = SelectorType.SQL92 就能过滤出所需要的消息

@RocketMQMessageListener(consumerGroup = "cu3" , topic = "topic10",selectorType = SelectorType.SQL92 , selectorExpression = "iq > 14")

同一个消费者组,不要出现不同的 topic 和 tag

首先根据报错信息The consumer group[user_group] has been created before, specify another name please.
可以得出一个结论,那就是消费者组的名称需要唯一,至少在一个项目中要唯一。所以先修改UpdateUserMessageListener
消费者的消费者组名称为user_update_group
。这样的话如果订阅相同Topic
不同的Tag
就需要指定不同的消费者组名称,我之前的理解是,一个Topic
一个消费者组,看来这种理解是错误的,而应该是一个Topic:Tag
一个消费者组。因为消息发送的地址是Topic:Tag
,那么显即使Topic
相同但Tag
不同的话,其实消费者也是不同的,自然消费者的组也就不同。也就是说消费者的分组归根到底是按照Topic:Tag
来区分的,我之所以认为是按照Topic
,是因为之前 消费者中Tag
定义的都是*
(默认值),所以才有这样的误解。

发送同步消息

消息错乱产生原因

同一个topic 下 有四个队列(可以通过配置文件修改默认的队列数),当生产者发送消息时候,会向这几个队列发送消息并不会发送至同一个队列,消费者取的时候也不会按顺序去取消息者就造成了消息错乱

生产者发送顺序消息

只在集群模式有效广播模式无效

常规方法

生产者发送消息有一个含有 MessageQueueSelector 参数的方法

public SendResult send(Message msg, MessageQueueSelector selector, Object arg)

MessageQueueSelector 这是个接口定于如下

public interface MessageQueueSelector {
    MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

send 方法 第一个参数发送消息的实体,不用讲解,看第二个 MessageQueueSelector ,这是个消息队列选择器,用来告诉mq这个消息要放到那个队列, MessageQueueSelector 第一个参数 传入消息队列的一个列表,第二个参数 message 消息的实体 ,第三个参数 先按下不表,返回的是一个消息队列,用处:就是告诉mq这个消息所选择的队列,send 方法 的第三个参数 arg 是不是 和 MessageQueueSelector 的第三个参数一样,他们执行后也确实一样。具体看源码

String userTopic = NamespaceUtil.withoutNamespace(userMessage.getTopic(), mQClientFactory.getClientConfig().getNamespace());
                userMessage.setTopic(userTopic);

                mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg));

在send 方法的执行过程最终会 回传给 MessageQueueSelector

使用 rocketMQTemplate 发送

使用 rocketMQTemplate 发送顺序消息需要 使用 syncSendOrderly 方法

public SendResult syncSendOrderly(String destination, Message<?> message, String hashKey)

其他不做过多解释,这里主要解释最后一个参数
在 syncSendOrderly 方法的执行过程中会 执行到这样的一个指令

SendResult sendResult = producer.send(rocketMsg, messageQueueSelector, hashKey, timeout);

其中这个 messageQueueSelector 的实现是这样的

public class SelectMessageQueueByHash implements MessageQueueSelector {

    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode() % mqs.size();
        if (value < 0) {
            value = Math.abs(value);
        }
        return mqs.get(value);
    }
}

这个 arg 和 最开始的 hashkey 是一样的也就是说他会根据这个hashkey 的 hash值取模来得到所选择的队列,
如果内置的这个 MessageQueueSelector 没法实现需求 rocketMQTemplate 支持设定MessageQueueSelector() 来实现个性化的需求
需要注意的是spring 默认注入的是单例模式这样修改会影响到其他的组件

rocketMQTemplate.setMessageQueueSelector()

消费者接收顺序消息

同样也是注册监听,不过需要实现 MessageListenerOrderly 接口其余和之前的正常监听没有区别

consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> null);

RocketMQListener 接收消息

只需要在 _@RocketMQMessageListener _添加 consumeMode = ConsumeMode.ORDERLY 属性即可

事务消息

事务消息的流程

  1. 生产者发送一个半消息给MQServer(半消息是指消费者暂时不能消费的消息)
  2. 服务端响应消息写入结果,半消息发送成功
  3. 开始执行本地事务
  4. 根据本地事务的执行状态执行Commit或者Rollback操作

image.png
事务补偿流程

  1. 如果MQServer长时间没收到本地事务的执行状态会向生产者发起一个确认回查的操作请求
  2. 生产者收到确认回查请求后,检查本地事务的执行状态
  3. 根据检查后的结果执行Commit或者Rollback操作
    补偿阶段主要是用于解决生产者在发送Commit或者Rollback操作时发生超时或失败的情况。

image.png

RocketMQ事务流程关键

  1. 事务消息在一阶段对用户不可见
    事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的,也就是说消费者不能直接消费。这里RocketMQ的实现方法是原消息的主题与消息消费队列,然后把主题改成 RMQ_SYS_TRANS_HALF_TOPIC ,这样由于消费者没有订阅这个主题,所以不会被消费。
  2. 如何处理第二阶段的失败消息?在本地事务执行完成后会向MQServer发送Commit或Rollback操作,此时如果在发送消息的时候生产者出故障了,那么要保证这条消息最终被消费,MQServer会像服务端发送回查请求,确认本地事务的执行状态。当然了rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,RocketMQ默认回滚该消息。
  3. 消息状态
    事务消息有三种状态:
  • TransactionStatus.CommitTransaction:提交事务消息,消费者可以消费此消息
  • TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
  • TransactionStatus.Unknown :中间状态,它代表需要检查消息队列来确定状态。

生产者发送事务消息

不在使用 DefaultMQProducer 来创立生产者 而是使用 TransactionMQProducer 来创立生产者

TransactionMQProducer trans1 = new TransactionMQProducer("trans1");

完整代码

TransactionMQProducer trans1 = new TransactionMQProducer("trans1");
        trans1.setNamesrvAddr("127.0.0.1:9876");
        trans1.setTransactionListener(new TransactionListener() {
            @Override
            // 执行本地事务
            public LocalTransactionState executeLocalTransaction(org.apache.rocketmq.common.message.Message msg, Object arg) {

                System.out.println("producer--->" + "执行本地事务" + "args===>" + arg.toString());

                return LocalTransactionState.UNKNOW;
            }

            @Override
            // 检测本地事务
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                System.out.println("事务补偿->>>><<<<<-");
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });
        try {
            trans1.start();
            trans1.sendMessageInTransaction(new org.apache.rocketmq.common.message.Message("trans1","事务消息".getBytes(StandardCharsets.UTF_8)) , "args");
        } catch (MQClientException e) {
            e.printStackTrace();
        }


这里主要说明一下 事务的监听
本地事务的执行有个 arg 参数,这个与下面 发送事务消息的 arg是同一个,执行时会传过来,
不论是 生产者 还是 消费者在发送/接收 消息前记得调用start方法、

使用rocketmqtemplat 发送消息

这里需要使用 rocketmqtemplate 的 start 改为 2.0.1 新版本的 2.2.2 有问题
这个篇文章 看代码 用的是新版 不知道有没有出现这个问题
利用rocketMQTemplate类的sendMessageInTransaction实现半消息发送
第一个参数为txProducerGroup:就是group名称,根据业务自定义
第二次信息为destination:topic名称
第三个信息为message:消息体,利用MessageBuilder.withPayload构建
第四个信息为arg:业务对象,用于处理本地业务
代码如下:

// 发送半消息
rocketMQTemplate.sendMessageInTransaction(
"test-transactional",
"test-topic",
MessageBuilder.withPayload(
Demo.builder().demoId(1).remark("哈哈哈").build()
).setHeader(RocketMQHeaders.TRANSACTION_ID, UUID.randomUUID().toString()).build(),
forObject
);


新建demoTransactionalListener类,继承RocketMQLocalTransactionListener接口,实现了两个方法
executeLocalTransaction:处理本地业务
checkLocalTransaction:MQ Server发送检查信息相应
添加@RocketMQTransactionListener注解,txProducerGroup属性值与半消息的txProducerGroup参数值相同
代码如下:

import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;

@RocketMQTransactionListener(txProducerGroup   = "test-transactional") 
public class demoTransactionalListener implements RocketMQLocalTransactionListener {
    /**
    
    ● 处理本地事务
    */
    @Override  
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
        // 消息头
        MessageHeaders headers = message.getHeaders();
        String transactionalId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
// arg:sendMessageInTransaction方法的第四个参数,用于处于本地业务

使用 start 2.2.2 注意的地方

从 2.1.0 开始 rocketmq 事务消息的监听器加入实在所有 单例bean初始化完成之后执行的,所以发送事务消息的时机需要在容器启动之后

@Configuration
public class RocketMQTransactionConfiguration implements ApplicationContextAware, SmartInitializingSingleton {

    private final static Logger log = LoggerFactory.getLogger(RocketMQTransactionConfiguration.class);

    private ConfigurableApplicationContext applicationContext;

    @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = (ConfigurableApplicationContext) applicationContext;
    }

    @Override public void afterSingletonsInstantiated() {
        Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(RocketMQTransactionListener.class)
            .entrySet().stream().filter(entry -> !ScopedProxyUtils.isScopedTarget(entry.getKey()))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        beans.forEach(this::registerTransactionListener);
    }

    private void registerTransactionListener(String beanName, Object bean) {
        Class<?> clazz = AopProxyUtils.ultimateTargetClass(bean);

        if (!RocketMQLocalTransactionListener.class.isAssignableFrom(bean.getClass())) {
            throw new IllegalStateException(clazz + " is not instance of " + RocketMQLocalTransactionListener.class.getName());
        }
        RocketMQTransactionListener annotation = clazz.getAnnotation(RocketMQTransactionListener.class);
        RocketMQTemplate rocketMQTemplate = (RocketMQTemplate) applicationContext.getBean(annotation.rocketMQTemplateBeanName());
        if (((TransactionMQProducer) rocketMQTemplate.getProducer()).getTransactionListener() != null) {
            throw new IllegalStateException(annotation.rocketMQTemplateBeanName() + " already exists RocketMQLocalTransactionListener");
        }
        ((TransactionMQProducer) rocketMQTemplate.getProducer()).setExecutorService(new ThreadPoolExecutor(annotation.corePoolSize(), annotation.maximumPoolSize(),
            annotation.keepAliveTime(), annotation.keepAliveTimeUnit(), new LinkedBlockingDeque<>(annotation.blockingQueueSize())));
        ((TransactionMQProducer) rocketMQTemplate.getProducer()).setTransactionListener(RocketMQUtil.convert((RocketMQLocalTransactionListener) bean));
        log.debug("RocketMQLocalTransactionListener {} register to {} success", clazz.getName(), annotation.rocketMQTemplateBeanName());
    }
}

可以看到它实现了 SmartInitializingSingleton 接口 , 而添加事务监听器的方法在 afterSingletonsInstantiated 里面,所以当发送事务消息时候要在spring 启动完成之后执行
可以继承 _ _CommandLineRunner 接口
SpringBoot构建的服务在启动完成时执行功能的三种方法

@Component
public class OrderMessage implements CommandLineRunner {
    @Resource
    private RocketMQTemplate rocketMQTemplate;

    //@Bean
    public void rockettemplateSendTrans() {
        System.out.println(SpringUtil.getBean(RocketMQLocalTransactionListener.class));


        rocketMQTemplate.sendMessageInTransaction("trans1" ,
                MessageBuilder.withPayload(new Order(45L , "事务消息")).build() ,
                null);

    }


    @Override
    public void run(String... args) throws Exception {
        rockettemplateSendTrans();
    }
}

这样就不会启动报错了

定义非标的RocketMQTemplate

第一步: 定义非标的RocketMQTemplate使用你需要的属性,可以定义与标准的RocketMQTemplate不同的nameserver、groupname等。如果不定义,它们取全局的配置属性值或默认值。

// 这个RocketMQTemplate的Spring Bean名是'extRocketMQTemplate', 与所定义的类名相同(但首字母小写)
@ExtRocketMQTemplateConfiguration(nameServer="127.0.0.1:9876"
   , ... // 定义其他属性,如果有必要。
)
public class ExtRocketMQTemplate extends RocketMQTemplate {
  //类里面不需要做任何修改
}

第二步: 使用这个非标RocketMQTemplate

@Resource(name = "extRocketMQTemplate") // 这里必须定义name属性来指向上述具体的Spring Bean.
private RocketMQTemplate extRocketMQTemplate; 

接下来就可以正常使用这个extRocketMQTemplate了。

集群

image.png
image.png
拥有相同的brokerName 的 broker 为统一集群,特别的brokerID = 0 的机器为主机 其余的为从机,不论组从机多要向命名服务器里注册( 有多少台命名服务器,就要向多少台命名服务器中注册 )
RocketMQ集群工作流程
步骤1:NameServer,启动,开启监听,等待broker、producer与consumer连接
步骤2:broker,启动,根据配置信息,连接所有的NameServer,并保持长连接
步骤2补充:如果broker中有现存数据,NameServer将保存topic-与broker关系
步骤3:producer发信息,连接某个NameServer,并建立长连接
步骤4:producer发消息
步骤4.1若果topic存在,由NameServer.直接分配
步骤4.2如果topic不存在,由NameServert创建topic.与broker关系,并分配
步骤5:producer在broker的topic选择一个消息队列(从列表中选择)
步骤6:producer.与broker建立长连接,用于发送消息
步骤7:producer发送消息
comsumer工作流程同producer
image.png

broker配置文件

默认在 conf/broker.conf 目录下

#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#设置启动的ip
brokerIP1=127.0.0.1
#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

启动命令

sh mqbroker -c (配置文件路径)

nameserv配置文件

stenPort = 6541
# serverWorkerThreads = 8
# serverCallbackExecutorThreads = 0
# serverSelectorThreads = 3
# serverOnewaySemaphoreValue = 256
# serverAsyncSemaphoreValue = 64
# serverChannelMaxIdleTimeSeconds = 120

# serverSocketSndBufSize = NettySystemConfig.socketSndbufSize
# serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize
# writeBufferHighWaterMark = NettySystemConfig.writeBufferHighWaterMark
# writeBufferLowWaterMark = NettySystemConfig.writeBufferLowWaterMark
# serverSocketBacklog = NettySystemConfig.socketBacklog
# private boolean serverPooledByteBufAllocatorEnable = true

启动方式

sh mqnamesrv -c (配置文件所在的路径名)

高级特性

文件存储

①消息生成者发送消息到MQ
②MQ收到消息,将消息进行持久化,存储该消息
③MQ返回ACK给生产者
④ MQ push 消息给对应的消费者
⑤ 消息消费者返回ACK给MQ
⑥ MQ删除消息

注意:
①第⑤步MQ在指定时间内接到消息消费者返回ACK, MQ认定消息消费成功,执行⑥
②第⑤步MQ在指定时间内未接到消息消费者返回ACK, MQ认定消息消费失败,重新执行④⑤⑥
image.png
image.png

文件系统存储流程

先向 OS 中申请空间(具体申请多少可在配置文件中配置) 用于完成顺序写

broker 向消费者端 发送消息

传统意义上要经过一下几个步骤
image.png
而 rocketMQ 干掉了 内核态到用户态过程,直接由内核态到 网络驱动内核
image.png
这就是 0拷贝技术
• Linux系统发送数据的方式
・ “零拷贝”技术
•数据传输由传统的4次复制简化成3次复制,减少1次复制过程, Java语言中使用MappedByteBuffer类实现了该技术
•要求:预留存储空间,用于保存数据(1G存储空间起步)

文件结构

MQ数据存储区域包含如下内容

  1. 消息数据存储区域(commitlog)
  • topic
  • queueld
  • message
  1. 消费逻辑队列(consumequeue)
  • minOffset
  • maxOffset
  • consumerOffset
  1. 索引(index)
  • key索引
  • 创建时间索引

image.png

刷盘机制

同步刷盘

  1. 生产者发送消息到MQ, MQ接到消息数据
  2. MQ挂起生产者发送消息的线程
  3. MQ将消息数据写入内存
  4. 内存数据写入硬盘
  5. 磁盘存储后返回SUCCESS
  6. MQ恢复挂起的生产者线程
  7. 发送ACK到生产者

image.png

异步刷盘

  1. 生产者发送消息到MQ, MQ接到消息数据
  2. broker接收到消息存放到内存
  3. 发送ACK到生产者

优缺点

同步刷盘:安全性高,效率低,速度慢(适用于对数据安全要求较高的业务)
异步刷盘:安全性低,效率高,速度快(适用于对数据处理速度要求较高的业务)
image.png

高可用性

  • nameserver
    • 无状态+全服务器注册
  • 消息服务器
    • 主从架构(2M—2S)
  • 消息生产
    • 生产者将相同的topic绑定到多个group组,保障master挂掉后,其他master仍可正常进行消息接收
  • 消息消费
    • RocketMQ自身会根据master的压力确认是否由master承担消息读取的功能,当master繁忙时候,自动切换由slave承担数据读取的工作

消息重试

顺序消息

当消费者消费失败后,Rocketmq 会自动进行消费重试(每次间隔时间为1秒)
注意:应用会出现消息消费被阻塞的情况,因此,要对顺序消息的消费情况进行监控,避免阻塞现象的发生

image.png

无序消息

  • 无序消息包括普通消息、定时消息、延时消息、事务消息
  • 无序消息重试仅适用于负载均衡(集群)模型下的消息消费,不适用于广播模式下的消息消费
  • 为保障无序消息的消费,MQ设定了合理的消息重试间隔时长

image.png
当消费者接收到消息想进行重试时

传统

只需要在消息监听的返回

  • null
  • ConsumeConcurrentlyStatus.RECONSUME_LATER
  • 抛出异常
consumer.subscribe("topic10" , MessageSelector.bySql("iq > 14"));
            consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
                list.forEach((e)->{
                    System.out.println("sqlFilter ---- " + e.toString());
                });
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            });

就可以进行重试

使用spring

使用 _RocketMQMessageListener _就只能使用抛出异常的方式来进行重试

@Repository

@RocketMQMessageListener(consumerGroup = "TestOrderMessageListerTrans"
            ,topic = "trans1" )
            public class TestTransactionMessageLister implements RocketMQListener<MessageExt>,RocketMQPushConsumerLifecycleListener {

    
    @Override
    public void onMessage(MessageExt message) {
        throw  new RuntimeException();
        //System.out.println("trans1---" + message.toString());
    }

    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        System.out.println("---- consumer -----");
        consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragelyByCircle());
    }
}

当重试次数用完时候那么这个消息就会进入死信队列

死信息队列

当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。这个队列就是死信队列(Dead—Letter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)。


死信队列的特征

死信队列具有如下特征:

  • 死信队列中的消息不会再被消费者正常消费
  • 死信存储有效期与正常消息相同,均为3天,3天后会被自动删除
  • 死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup
  • 如果一个消费者组未产生死信消息,则不会为其创建相应的死信队列

负载均衡

进阶操作

消费者操作

有时我们想对消费者进行一些特殊的设置,比如设置消费者消费消息的负载均衡模式
在继承 RocketMQListener接口 的类上在 继承一个 RocketMQPushConsumerLifecycleListener 接口并实现 prepareStart方法,就可以进行这些设置

@Repository

@RocketMQMessageListener(consumerGroup = "TestOrderMessageListerTrans"
            ,topic = "trans1" )
            public class TestTransactionMessageLister implements RocketMQListener<MessageExt>,RocketMQPushConsumerLifecycleListener {
    @Override
    public void onMessage(MessageExt message) {
        System.out.println("trans1---" + message.toString());
    }

    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        System.out.println("---- consumer -----");
        consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragelyByCircle());
    }
}

生产者操作

有时需要对生产者推送消息的方式进行设置,比如设置消费队列选择器,就可以使用 非标的RocketMQTemplate

posted @ 2023-01-17 16:05  Epiphanyi  阅读(167)  评论(0)    收藏  举报