RocketMQ

q

RocketMQ

1. RocketMQ简介

官网: http://rocketmq.apache.org/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P0dF6Qg0-1671795329101)(images\1.png)]

RocketMQ是阿里巴巴2016年MQ中间件,使用Java语言开发,RocketMQ 是一款开源的分布式消息系统
,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。同时,广泛应用于多个领域,包括异步通信解耦、企业解决方案、金融支付、电信、电子商务、快递物流、广告营销、社交、即时通信、移动应用、手游、视频、物联网、车联网等。

具有以下特点:

  1. 能够保证严格的消息顺序

  2. 提供丰富的消息拉取模式

  3. 高效的订阅者水平扩展能力

  4. 实时的消息订阅机制

  5. 亿级消息堆积能力

2.为什么要使用MQ

1,要做到系统解耦,当新的模块进来时,可以做到代码改动最小; 能够解耦

2,设置流量缓冲池,可以让后端系统按自身吞吐能力进行消费,不被冲垮; 能够削峰,限流

3,强弱依赖梳理能把非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步

MQ的核心作用:流量削峰,解耦,异步

2.1 定义

中间件(缓存中间件 redis memcache 数据库中间件 mycat canal 消息中间件mq )

面向消息的中间件(message-oriented middleware0) MOM能够很好的解决以上的问题。

是指利用高效可靠的消息传递机制进行与平台无关(跨平台)的数据交流,并基于数据通信来进行分布式系统的集成。

通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储,流量削峰,异步通信,数据同步等

2.2基本流程

发送者把消息发给消息服务器,消息服务器把消息存放在若干队列/主题中,在合适的时候,消息服务器会把消息转发给接受者。在这个过程中,发送和接受是异步的,也就是发送无需等待,发送者和接受者的生命周期也没有必然关系在发布pub/订阅sub模式下,也可以完成一对多的通信,可以让一个消息有多个接受者。例如微信订阅号就是这样的。

在这里插入图片描述

2.3消息特点

2.3.1 异步处理模式

消息发送者可以发送一个消息而无需等待响应。消息发送者把消息发送到一条虚拟的通道(主题或队列)上;

消息接收者则订阅或监听该通道。一条信息可能最终转发给一个或多个消息接收者,这些接收者都无需对消息发送者做出回应。整个过程都是异步的。

案例:
在这里插入图片描述

也就是说,一个系统和另一个系统间进行通信的时候,假如系统A希望发送一个消息给系统B,让它去处理,但是系统A不关注系统B到底怎么处理或者有没有处理好,所以系统A把消息发送给MQ,然后就不管这条消息的“死活”
了,接着系统B从MQ里面消费出来处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事,与系统A无关。

2.3.2 应用系统的解耦

(1) 发送者和接收者不必了解对方,只需要确认消息

(2) 发送者和接收者不必同时在线

2.3.3实际的业务

在这里插入图片描述

3.MQ队列产品的比较

在这里插入图片描述

4.Rocket核心概念

在这里插入图片描述

Producer 消息生产者,负责产生消息,一般由业务系统负责产生消息。

Producer Group 一类 Producer 的集合名称,这类 Producer 通常发送一类消息,且发送逻辑一致。 Consumer 消息费者,负责消费消息,一般是后台系统负责异步消费。

Push Consumer 服务端向消费者端推送消息

Pull Consumer 消费者端从服务定时拉取消息

Consumer Group 一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致。 NameServer 集群架构中的组织协调员,收集broker的工作情况 不负责消息的处理

Broker 是RocketMQ的核心负责消息的发送、接收、高可用等(真正干活的) 需要定时发送自身情况到NameServer,默认10秒发送一次,超时2分钟会认为该broker失效。

Topic 主题,不同类型的消息以不同的Topic名称进行区分,如User、Order等 是逻辑概念 。

Message Queue 消息队列,用于存储消息

5.安装Rocketmq

了解了mq的基本概念和角色以后,我们开始安装rocketmq,建议在linux上

5.1 下载RocketMQ

官网下载地址:https://rocketmq.apache.org/dowloading/releases/

注意选择版本,这里我们选择4.9.2的版本,后面使用alibaba时对应

下载地址:https://archive.apache.org/dist/rocketmq/4.9.2/rocketmq-all-4.9.2-bin-release.zip

5.2上传文件到usr/local目录并解压

unzip rocketmq-all-4.9.2-bin-release.zip

如果你的服务器没有unzip命令,则下载安装一个 yum install unzip

5.3目录结构

Benchmark:包含一些性能测试的脚本;
Bin:可执行文件目录;
Conf:配置文件目录;
Lib:第三方依赖;
LICENSE:授权信息;
NOTICE:版本公告;

5.4启动RocketMQ

直接启动会出现错误,是因为内存不够,导致启动失败,原因:RocketMQ的配置默认是生产环境的配置,设置的jvm的内存 大小值比较大,而现在的测试环境的内存往往都不是很大,所以需要调整默认值。

5.4.1修改配置

#调整默认的内存大小参数
#修改runserver.sh文件,将71行和76行的Xms和Xmx等改小一点
cd bin/
vim runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx128m -Xmn128m -XX:MetaspaceSize=128m -
XX:MaxMetaspaceSize=128m"
#进入bin目录下,修改runbroker.sh文件,修改67行
cd bin/
vim runbroker.sh
JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx128m"
#进入conf目录,修改broker.cnf配置文件,添加以下内容
namesrvAddr=localhost:9876   #指定命名服务器,如果broker和nameser不在一个服务器,需要设ip地址
autoCreateTopicEnable=true  #自动创建topic,否则需要手动创建

5.4.2正常启动rocketmq

#启动nameserver
bin/mqnamesrv
# The Name Server boot success. serializeType=JSON 看到这个表示已经提供成功
#启动broker
bin/mqbroker -n 192.168.179.128:9876 #-n 指定nameserver地址和端口,如果不指定就本机:默认端口
#指定待配置文件启动
bin/mqbroker -c /usr/local/rocketmq/conf/broker.conf

5.4.3指定输出日志

#启动nameSrv
nohup sh bin/mqnamesrv > ./logs/namesrv.log &
#启动broker 这里的-c是指定使用的配置文件
nohup sh bin/mqbroker -c conf/broker.conf > ./logs/broker.log &
#查看服务是否启动
JPS

5.4.4停止服务

#停止namesrv
bin/mqshutdown namesrv
#停止broker
bin/mqshutdown broker

5.5安装RocketMQ控制台

Rocketmq 控制台可以可视化MQ的消息发送!

旧版本源码是在rocketmq-external里的rocketmq-console,新版本已经单独拆分成dashboard

网址: https://github.com/apache/rocketmq-dashboard

下载地址:

https://github.com/apache/rocketmq-dashboard/archive/refs/tags/rocketmq-dashboard-1.0.0.zip

5.5.1解压

mvn clean package -Dmaven.test.skip=true

5.5.2运行

#上传rocketmq-dashboard-1.0.0.jar到usr/local/rocketmq目录
#1.直接启动
java -jar rocketmq-dashboard-1.0.0.jar &
#2.指定日志输出路径启动
nohup java -jar ./rocketmq-dashboard-1.0.0.jar > ./rocketmq-4.9.2/logs/dashboard.log &
#3.默认端口是8080,可以通过参数设置启动端口
nohup java -jar rocketmq-dashboard-1.0.0.jar 
--server.port=8081 --rocketmq.config.namesrvAddr192.168.175.128:9876 > rocketmq-dashboard.log &
#4.关闭防火墙
systemctl stop firewalld

5.5.3运行测试

http://192.168.179.128:8001/#/

6.消息发送和接受

RocketMQ提供了发送多种发送消息的模式,例如同步消息,异步消息,顺序消息,延迟消息,事务消息等。

6.1Rocket发送同步消息

发送同步消息,发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式 .

6.1.1创建消息生产者项目Prducer

<dependencies>
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.9.2</version>
        </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.22</version>
    </dependency>
</dependencies>

6.1.2发送同步消息

  public static void main(String[] args) throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        producer.setSendMsgTimeout(3000);
        producer.setRetryTimesWhenSendFailed(3);
        // 启动实例
        producer.start();
       for (int i = 0; i < 10; i++)
        {
            // 创建消息
            // 第一个参数:主题的名字
            // 第二个参数:消息内容
            Message msg = new Message("Topic1", ("Hello RocketMQ " +i).getBytes());
            SendResult send = producer.send(msg);
            System.out.println(send);
        }
        // 关闭实例
        producer.shutdown();

    }

6.1.3创建消费者项目Consumer

项目导入的包和生产者相同

6.1.4接收消息

 public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("Topic1", "*");
        consumer.setMaxReconsumeTimes(3);
        // 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认20个线程,可以参看consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.println(new Date());
                for (MessageExt msg : msgs) {
                    try {
                        System.out.println(msg.getMsgId()+","+msg.getBrokerName()+","+"queue-"+msg.getQueueId()+","+new String(msg.getBody(), "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();


    }

6.1.5消息Message结构

字段默认值说明
Topicnull必填,线下环境不需要申请,线上环境需要申请后才能使用
Bodynull必填,二进制形式,序列化由应用决定,Producer 与 Consumer 要协商好 序列化形式。
Tagsnull选填,类似于 Gmail 为每封邮件设置的标签,方便服务器过滤使用。目前只 支持每个消息设置一个 tag,所以也可以类比为 Notify 的 MessageType 概 念
Keysnull选填,代表这条消息的业务关键词,服务器会根据 keys 创建哈希索引,设置 后,可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引, 请尽可能保证 key 唯一,例如订单号,商品 Id 等。
Flag0选填,完全由应用来设置,RocketMQ 不做干预
DelayTimeLevel0选填,消息延时级别,0 表示不延时,大于 0 会延时特定的时间才会被消费
WaitStoreMsgOKtrue选填,表示消息是否在服务器落盘后才返回应答。

6.2RocketMQ发送异步消息

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知

6.2.1异步消息生产者

public class AscProducer {
    public static void main(String[] args) throws Exception{
// 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("test-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        Message msg = new Message("TopicTest", ("异步消息").getBytes());
        producer.send(msg, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("发送成功");
            }
            @Override
            public void onException(Throwable e) {
                System.out.println("发送失败"+e.getMessage());
            }
        });
        System.out.println("看看谁先执行");
        // 挂起jvm 因为回调是异步的不然测试不出来
        System.in.read();
        // 关闭实例
        producer.shutdown();

    }
}

6.2.2异步消息消费者

public class AscConsumer {
    public static void main(String[] args) throws Exception {
// 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("TopicTest", "*");
        // 注册一个消费监听 MessageListenerConcurrently是并发消费
        // 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
        System.out.println("监听消息....");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                // 这里执行消费的代码 默认是多线程消费
                System.out.println(Thread.currentThread().getName() + "----" );
                for(MessageExt ex:msgs){
                    System.out.println(new String(ex.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();

    }
}

6.3RocketMQ发送单向消息

这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,例如日志信息的发送

6.3.1单向消息生产者

public class OnewayProducer {
    public static void main(String[] args) throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("oneway-procedure-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        for(int i=1;i<=10;i++) {
            Message msg = new Message("oneway-topic", ("单向消息...."+i).getBytes());
            // 发送单向消息
            producer.sendOneway(msg);
        }
        System.out.println("消息发送完成:"+new Date());
        // 关闭实例
        producer.shutdown();

    }
}

6.3.2单向消息消费者

public class OneWayConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("oneway-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("oneway-topic", "*");
        consumer.setMaxReconsumeTimes(3);
        System.out.println("我开始监听消息...."+new Date());
        // 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认20个线程,可以参看consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.println("开始消费消息:"+new Date());
                for (MessageExt msg : msgs) {
                    try {
                        System.out.println(msg.getMsgId()+","+msg.getBrokerName()+","+"queue-"+msg.getQueueId()+","+new String(msg.getBody(), "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

6.4RocketMQ发送延迟消息

消息放入mq后,过一段时间,才会被监听到,然后消费

比如下订单业务,提交了一个订单就可以发送一个延时消息,30min后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

6.4.1延迟消息生产者

public class DelayProducer {
    public static void main(String[] args) throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("delay-producer-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        Message msg = new Message("delay-topic", ("我是延迟消息").getBytes());
        // 给这个消息设定一个延迟等级
        // messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

        msg.setDelayTimeLevel(3);
        // 发送延迟消息
        producer.send(msg);
        // 打印时间
        System.out.println("发送时间:"+new Date());
        // 关闭实例
        producer.shutdown();

    }
}

6.4.2延迟消息消费者

延迟等级设置为了3,也就是在发送者发送完成之后10秒开始接受消息。

public class DelayConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("delay-topic", "*");
        consumer.setMaxReconsumeTimes(3);
        // 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认20个线程,可以参看consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {

                for (MessageExt msg : msgs) {
                    System.out.println("消费时间:"+new Date());
                    System.out.println(msg.getMsgId()+","+msg.getBrokerName()+","+"queue-"+msg.getQueueId()+","+new String(msg.getBody()));

                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

注意

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";

6.5RocketMQ发送顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为:分区有序或者全局有序。可能大家会有疑问,mq不就是FIFO吗?

rocketMq的broker的机制,导致了rocketMq会有这个问题,因为一个broker中对应了四个queue

查看dashbord中的主题-状态,可以看到如下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yL941G30-1671795329104)(images\8.png)]

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列)
;而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

在这里插入图片描述

下面用订单进行分区有序的示例。一个订单的顺序流程是:下订单、发短信通知、物流、签收。订单顺序号相同的消息会被先后发送到同一个队列中,消费时,同一个顺序获取到的肯定是同一个队列。

6.5.1模拟订单业务

模拟一个订单的发送流程,创建两个订单,发送的消息分别是

订单号2001 消息流程 下订单->物流->签收

订单号3001 消息流程 下订单->物流->拒收

6.5.2创建订单实体Order

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    /**
     * 订单id
     */
    private Integer orderId;

    /**
     * 订单编号
     */
    private Integer orderNumber;

    /**
     * 订单价格
     */
    private Double price;

    /**
     * 订单号创建时间
     */
    private Date createTime;

    /**
     * 订单描述
     */
    private String desc;

}

6.5.3创建顺序消息生产者

public class OrderlyProdcure {
    public static void main(String[] args) throws  Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("order-producer-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        List<Order> orderList = Arrays.asList(
                new Order(1, 2001, 59D, new Date(), "下订单"),
                new Order(2, 2001, 59D, new Date(), "物流"),
                new Order(3, 2001, 59D, new Date(), "签收"),
                new Order(4, 3001, 89D, new Date(), "下订单"),
                new Order(5, 3001, 89D, new Date(), "物流"),
                new Order(6, 3001, 89D, new Date(), "拒收")
        );
        // 循环集合开始发送
        orderList.forEach(order -> {
            Message message = new Message("order-topic", 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就是后面传入的 order.getOrderNumber()
                        Integer i = (Integer) arg;
                        // 用这个值去%队列的个数得到一个队列
                        int index = i % queueNumber;
                        System.out.println("index="+i);
                        // 返回选择的这个队列即可 ,那么相同的订单号 就会被放在相同的队列里 实现FIFO了
                        return mqs.get(index);
                    }
                }, order.getOrderNumber());
                System.out.println("订单--"+order.getOrderId()+"发送完成..."+message);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        });
        // 关闭实例
        producer.shutdown();
    }
}

6.5.4顺序消息的消费者

public class OrderlyConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("order-topic", "*");
        // 注册一个消费监听 MessageListenerOrderly 是顺序消费 单线程消费
        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;
            }
        });
        consumer.start();
        System.in.read();
    }
}

6.6RocketMQ发送批量消息

Rocketmq可以一次性发送一组消息,那么这一组消息会被当做一个消息消费 

6.6.1批量消息生产者

public class BatchProducer {
    public static void main(String[] args) throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("batch-producer-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        List<Message> msgs = Arrays.asList(
                new Message("batch-topic", "我是一组消息的A消息".getBytes()),
                new Message("batch-topic", "我是一组消息的B消息".getBytes()),
                new Message("batch-topic", "我是一组消息的C消息".getBytes())
        );
        SendResult send = producer.send(msgs);
        System.out.println(send);
        // 关闭实例
        producer.shutdown();
    }
}

6.6.2批量消息消费者

public class BatchConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("batch-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("batch-topic", "*");
        // 注册一个消费监听 MessageListenerOrderly 是顺序消费 单线程消费
        // 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            int count=0;
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                // 这里执行消费的代码 默认是多线程消费
                MessageExt msg=msgs.get(0);
               // System.out.println(1/0);消息被消费了,没有输出
                System.out.println(msg.getMsgId() + "," + msg.getBrokerName() + "," + "queue-" + msg.getQueueId() + "," + new String(msg.getBody()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();
    }
}

6.7RocketMQ发送事务消息

6.7.1分布式事务消息

随着项目越来越复杂,越来越服务化,就会导致系统间的事务问题,这个就是分布式事务问题。 分布式事务分类有这几种: 基于单个JVM,数据库分库分表了(跨多个数据库)。 基于多JVM,服务拆分了(不跨数据库)。 基于多JVM,服务拆分了
并且数据库分库分表了。 解决分布式事务问题的方案有很多,使用消息实现只是其中的一种。
在这里插入图片描述

半消息(Prepare) Message 指的是暂不能投递的消息,发送方已经将消息成功发送到了 MQ 服务端,但是服务端未收到生产者对该消息的二次 确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半消息。 Message
Status Check 由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,MQ 服务端通过扫描发现某条消息长 期处于“半消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是
Rollback),该过程即消息回 查。

6.7.2事务消息执行流程

1. 发送方向 MQ 服务端发送消息。
2. MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
3. 发送方开始执行本地事务逻辑。
4. 发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback),MQ Server 收到
Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 Rollback 状态则删除半
消息,订阅方将不会接受该消息。
5. 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后
MQ Server 将对该消息发起消息回查。
6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
7. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤4对半消息进行操作。

6.7.3事务消息生产者

public class TranProducer {
    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new
                TransactionMQProducer("transaction_producer");
        producer.setNamesrvAddr("192.168.179.128:9876");
// 设置事务监听器
        producer.setTransactionListener(new TransactionListenerImpl());
        producer.start();
// 发送消息
        Message message = new Message("tans_topic", "用户A给用户B转账500元".getBytes("UTF-8"));
         producer.sendMessageInTransaction(message, null);
        Thread.sleep(999999);
        producer.shutdown();
    }
}

6.7.4本地事务处理业务逻辑

实现接口TransactionListener,提供本地业务逻辑的处理和回查的逻辑

public class TransactionListenerImpl implements TransactionListener {
 
    /**
     * 执行具体的业务逻辑
     *
     * @param msg 发送的消息对象
     * @param arg
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            System.out.println("用户A账户减500元.");
            Thread.sleep(500); //模拟调用服务
            System.out.println(1/0);  //模拟异常
            System.out.println("用户B账户加500元.");
            Thread.sleep(800);
           
            // 二次提交确认
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            e.printStackTrace();
        }
       
       // 回滚
        System.out.println("回滚事务了");
        return LocalTransactionState.UNKNOW; //如果返回ROLLBACK不执行回查
    }
    /**
     * 消息回查
     *
     * @param msg
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.err.println(new Date());
        System.err.println(new String(msg.getBody()));
        System.out.println("回查方法....");
        return LocalTransactionState.UNKNOW;
    }
}

6.7.5事务消息消费者

public class TransConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("trans-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("tans_topic", "*");
        // 注册一个消费监听 MessageListenerConcurrently是并发消费
        // 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                // 这里执行消费的代码 默认是多线程消费
                System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.in.read();

    }
}

6.7.6执行结果

如果本地事务正常,则消费者可以接收到正确的消息,如果本地事务出现异常,则会将消息删除。每个1分钟执行回查方法。

//回查不是再次执行业务操作,而是确认上面的操作是否有结果
// 默认是1min回查 默认回查15次 超过次数则丢弃打印日志 可以通过参数设置
// transactionTimeOut 超时时间
// transactionCheckMax 最大回查次数
// transactionCheckInterval 回查间隔时间单位毫秒
// 触发条件
// 1.当上面执行本地事务返回结果UNKNOW时,或者下面的回查方法也返回UNKNOW时 会触发回查
// 2.当上面操作超过20s没有做出一个结果,也就是超时或者卡主了,也会进行回查

在这里插入图片描述

7.消息重试

rocketMq具有消息重试的机制,重试也分为两种重试:producer重试consumer重试

7.1生产者Prducer重试

如果由于网络抖动等原因,Producer程序向Broker发送消息时没有成功,即发送端没有收到Broker的ACK,导致最终Consumer无法消费消息,此时RocketMQ会自动进行重试。

public class RetryProducer {
    public static void main(String[] args) throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("retry-producer-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        Message msg = new Message("retry-topic", ("我是重试消息").getBytes());
        //设置重试次数为3,默认为16
        producer.setRetryTimesWhenSendFailed(3);
        // 在1秒内没有发送成功,则重试
        producer.send(msg,1000);
        // 打印时间
        System.out.println("发送时间:"+new Date());
        // 关闭实例
        producer.shutdown();

    }
}

7.2消费者Consumer重试

在消费者放return ConsumeConcurrentlyStatus.RECONSUME_LATER;后就会执行重试

在实际生产过程中,一般重试5-7次,如果还没有消费成功,则可以把消息签收了,通知人工等处理

messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

public class RetryConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("retry-topic", "*");
        consumer.setMaxReconsumeTimes(2);
        // 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认20个线程,可以参看consumer.setConsumeThreadMax()
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                MessageExt messageExt = msgs.get(0);
                try {
                    // 这里执行消费的代码
                    System.out.println(new Date() + "----" + new String(messageExt.getBody()));
                    // 这里制造一个错误
                    int i = 10 / 0;
                } catch (Exception e) {
                    // 出现问题 判断重试的次数
                    // 获取重试的次数 失败一次消息中的失败次数会累加一次
                    int reconsumeTimes = messageExt.getReconsumeTimes();
                    if (reconsumeTimes >= 2) {
                        // 则把消息确认了,可以将这条消息记录到日志或者数据库 通知人工处理
                        System.err.println(new Date()+"消息重发2次都失败");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    } else {
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

8.RocketMQ死信消息

当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ
不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。我们需要关注死信队列,并对死信队列中的消息做人工的业务补偿操作。

在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue).

8.1死信生产者

public class DeadProducer {
    public static void main(String[] args) throws Exception {
        // 创建默认的生产者
        DefaultMQProducer producer = new DefaultMQProducer("dead-producer-group");
        // 设置nameServer地址
        producer.setNamesrvAddr("192.168.179.128:9876");
        // 启动实例
        producer.start();
        Message msg = new Message("dead-topic", ("我是死信消息").getBytes());
        // 在1秒内没有发送成功,则重试
        producer.send(msg);
        // 打印时间
        System.out.println("发送时间:"+new Date());
        // 关闭实例
        producer.shutdown();

    }
}

8.2死信消费者

public class DeadConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dead-consumer-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("dead-topic", "*");
        // 设置最大消费重试次数 2 次
        consumer.setMaxReconsumeTimes(2);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println(msgs);
                // 测试消费失败
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });
        consumer.start();
    }
}

重试2次后,仍然不能正确消费消息,则进入死信队列。队列名字为:%DLQ%dead-consumer-group

8.3处理死信消息

public class DeadMsgConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dead-process-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("%DLQ%dead-consumer-group", "*");
        // 消费重试到达阈值以后,消息不会被投递给消费者了,而是进入了死信队列
        // 队列名称 默认是 %DLQ% + 消费者组名
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println(new String(msgs.get(0).getBody()));
                // 处理消息,签收,比如人工处理
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

重试2次后,仍然不能正确消费消息,则进入死信队列。队列名字为:%DLQ%dead-consumer-group

## 8.3处理死信消息

```java
public class DeadMsgConsumer {
    public static void main(String[] args) throws Exception{
        // 创建默认消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dead-process-group");
        // 设置nameServer地址
        consumer.setNamesrvAddr("192.168.179.128:9876");
        // 订阅一个主题来消费   *表示没有过滤参数 表示这个主题的任何消息
        consumer.subscribe("%DLQ%dead-consumer-group", "*");
        // 消费重试到达阈值以后,消息不会被投递给消费者了,而是进入了死信队列
        // 队列名称 默认是 %DLQ% + 消费者组名
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println(new String(msgs.get(0).getBody()));
                // 处理消息,签收,比如人工处理
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huangshaohui00

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值