RocketMQ学习三-java代码应用

1 消息发送分类

1.1 普通消息

1.1.1 同步发送

  1. 原理
    同步发送是指消息发送方发出一条消息后,会在收到服务端返回响应之后才发下一条消息的通讯方式。
    在这里插入图片描述
  2. 测试代码
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // procedureGroup全局唯一
        String producerGroup = "demo-procedure";

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.setNamesrvAddr(namesrv);
        // 设置消息发送超时时间
        producer.setSendMsgTimeout(1000);
        // 设置发送异步消息重试次数
        producer.setRetryTimesWhenSendAsyncFailed(3);
        producer.start();
        for (int i = 0; i < 10; i ++) {
            SendResult result = producer.send(new Message("demo-topic", "demo-tag", ("msg-" + i).getBytes()));
            System.out.println("send msg end:" + result);
        }
        producer.shutdown();
    }

在这里插入图片描述

1.1.2 异步发送

  1. 原理
    异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。RocketMQ异步发送,需要实现异步发送回调接口(SendCallback)。消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息。发送方通过回调接口接收服务端响应,并处理响应结果。
    在这里插入图片描述
  2. 测试代码
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // procedureGroup全局唯一
        String producerGroup = "demo-procedure";

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.setNamesrvAddr(namesrv);
        // 设置消息发送超时时间
        producer.setSendMsgTimeout(1000);
        // 设置发送异步消息重试次数
        producer.setRetryTimesWhenSendAsyncFailed(3);
        producer.start();
        for (int i = 0; i < 10; i ++) {
            producer.send(new Message("demo-topic", "demo-tag", String.valueOf("msg-" + i).getBytes()), new SendCallback() {

                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.println(sendResult);
                }

                @Override
                public void onException(Throwable e) {
                    e.printStackTrace();
                }
            });
        }
        producer.shutdown();
    }

在这里插入图片描述

1.1.3 单向发送消息

  1. 原理
    单向发送消息是指Procedure仅负责发送消息,不等待,不处理MQ的ACK,该发送方式时MQ也不会返回ACK。该方式发送效率最高,但是消息可靠性没有保障
    在这里插入图片描述

  2. 测试代码

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // procedureGroup全局唯一
        String producerGroup = "demo-procedure";

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.setNamesrvAddr(namesrv);
        // 设置消息发送超时时间
        producer.setSendMsgTimeout(1000);
        // 设置发送异步消息重试次数
        producer.setRetryTimesWhenSendAsyncFailed(3);
        producer.start();
        for (int i = 0; i < 10; i ++) {
            SendResult result = producer.sendOneway(new Message("demo-topic", "demo-tag", ("msg-" + i).getBytes()));
            System.out.println("send msg end:" + result);
        }
        producer.shutdown();
    }

1.1.4 消费消息

public class DemoConsumer implements MessageListenerConcurrently {

    public static void main(String[] args) throws MQClientException {
        // consumerGroup
        String consumerGroup = "demo-consumer";
        String subscribeTopic = "demo-topic";

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
        consumer.setNamesrvAddr(namesrv);
        // 从第一个消息开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe(subscribeTopic, "*");
        // 设置每次最大拉取消息量。仅在mq发生消息堆积时候有效
        consumer.setConsumeMessageBatchMaxSize(1);
        consumer.registerMessageListener(new DemoConsumer());
        consumer.start();
    }

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        MessageExt message = msgs.get(0);
        String msg;
        try {
            msg = new String(message.getBody(), StandardCharsets.UTF_8);
            System.out.println("receive msg:" + msg);
        } catch (Exception e) {
            e.printStackTrace();
            int retryTimes = message.getReconsumeTimes();
            // 超过3次不再重试
            return retryTimes >= 3? ConsumeConcurrentlyStatus.CONSUME_SUCCESS: ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

在这里插入图片描述

1.2 顺序消息

1.2.1 全局顺序消息

在这里插入图片描述
7. 概念
对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序来发布和消费。这个时候一个Topic中只有一个Queue。可以通过三种方式指定Queue数量
1 通过生产者指定:producer.setDefaultTopicQueueNums(4);
2 RocketMQ控制台创建Topic时候指定
3 使用mqadmin命令创建Topic时候指定
8. 适用场景
适用于性能要求不高,所有的消息严格按照FIFO原则来发布和消费的场景。
9. 示例
在证券处理中,以人民币兑换美元为Topic,在价格相同的情况下,先出价者优先处理,则可以按照FIFO的方式发布和消费全局顺序消息。

1.2.2 分区顺序消息

在这里插入图片描述
10. 概念
对于指定的一个Topic,仅保证在该Queue分区队列上的消息顺序,称为分区有序。
Queue选择一般通过创建自定义选择器,利用key或key的hashCode或者发送消息时候的自定义参数对该Topic包含的队列数取模
11. 适用场景
适用于性能要求高,以Sharding Key作为分区字段,在同一个区块中严格地按照FIFO原则进行消息发布和消费的场景。
12. 示例
用户注册需要发送发验证码,以用户ID作为Sharding Key,那么同一个用户发送的消息都会按照发布的先后顺序来消费。
电商的订单创建,以订单ID作为Sharding Key,那么同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息都会按照发布的先后顺序来消费。

1.2.3 测试代码

  1. 生产者
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // procedureGroup全局唯一
        String producerGroup = "orderly-procedure";

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.setNamesrvAddr(namesrv);
        // 设置消息发送超时时间
        producer.setSendMsgTimeout(1000);
        // 设置发送异步消息重试次数
        producer.setRetryTimesWhenSendAsyncFailed(3);
        producer.start();
        // 模拟10个订单
        for (int orderId = 0; orderId < 10; orderId++) {
            // 每一份订单包含的3个步骤都是顺序消费的
            for (int j = 0; j < 3; j++) {
                Message message = new Message("orderly-topic", "orderly-tag", ("msg-" + orderId + "-" + j).getBytes());
                // send的第三个参数arg会传给messageSelector的第三个参数arg
                producer.send(message,
                        (mqs, msg, arg) -> {
                            int len = mqs.size();
                            int id = (int) arg;
                            // 若希望全局有序,则设置queue数量只有一个或者这里仅选择第一个queue
                            MessageQueue queue = mqs.get(id % len);
                            System.out.println(new String(msg.getBody(), StandardCharsets.UTF_8) + "->" + queue.getQueueId());
                            return queue;
                        }, orderId);
            }
        }
        producer.shutdown();
    }

在这里插入图片描述
14. 消费者

public class OrderlyConsumer implements MessageListenerOrderly {

    public static void main(String[] args) throws MQClientException {
        // consumerGroup
        String consumerGroup = "orderly-consumer";
        String subscribeTopic = "orderly-topic";

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
        consumer.setNamesrvAddr(namesrv);
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.subscribe(subscribeTopic, "*");
        // 设置每次最大拉取消息量。仅在mq发生消息堆积时候有效
        consumer.setConsumeMessageBatchMaxSize(1);
        // 设置消费线程数最小值和最大值
        consumer.setConsumeThreadMin(5);
        consumer.setConsumeThreadMax(10);
        consumer.registerMessageListener(new OrderlyConsumer());
        consumer.start();
    }

    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        MessageExt message = msgs.get(0);
        String msg;
        try {
            msg = new String(message.getBody(), StandardCharsets.UTF_8);
            System.out.println("receive msg:" + msg + "->" + message.getQueueId());
        } catch (Exception e) {
            e.printStackTrace();
            int retryTimes = message.getReconsumeTimes();
            // 超过3次不再重试
            return retryTimes >= 3? ConsumeOrderlyStatus.SUCCESS: ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
}

在这里插入图片描述
可以看到5、8、9号订单都是有序处理的

1.3 延时消息

1.3.1 概述

  1. 概念
    Producer将消息发送到消息队列RocketMQ服务端,但并不期望立马投递这条消息,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息。
  2. 适用场景
    消息生产和消费有时间窗口要求,例如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。如支付未完成,则关闭订单。如已完成支付则忽略。

1.3.2 延时等级

延时消息延长时间不支持随意时常的延时,通过特定的延时等级来指定的。延时等级在RocketMQ服务端的MessageStoreClient中。如果指定等级为3则延时是时长为10s

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

1.3.3 实现原理

Procedure将消息发送给Broker后,Broker首先会将消息写入commitlog文件,然后需要将其分发到相应的consumequeue。在分发前,先判断消息是否带有延时等级。如果没有则正常分发,如果有延时等级则会有如下过程

  1. 修改消息的Topic为SCHEDULE_TOPIC_XXX
  2. 根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXX主题下创建对应的queueId目录与consumequeue文件(queueId=延时等级-1)
  3. 修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本放的是消息Tag的Hash值。现修改为消息的投递时间(消息存储时间+延时时间)
  4. 将消息索引写入到SCHEDULE_TOPIC_XXX主题下对应的consumequeue,按照消息存储时间排序
  5. Broker内部有一个延时消息服务类ScheuleMessageService,会消费SCHEDULE_TOPIC_XXX主题下的消息,即按照每条消息的投递时间,将延时消息投到目标topic中。在投递之前会从commitlog中将原来写入的消息再次取出,并将延时等级设置为0,即变成一条不延时的消息。
  6. Broker启动时,会创建并启动一个定时器Timer,用于执行相应的定时任务。系统会根据延时等级个数创建TimerTask,每个TimerTask会负责一个延时等级队列消息的消费与投递。每个TimerTask会检查队列中第一条消息是否到达投递时间,若到达,则开始消费队列里的消息
  7. ScheuleMessageService将延迟发送消息再一次发送给commitlog,并再次形成新的消息索引条目,分发到对应的Queue

1.3.4 代码实现

只需要在发送消息的时候设置延时时间即可

msg.setDelayTimeLevel(3);

1.4 事务消息

2 批量消息

2.1 批量发送

2.1.1 发送限制

生产者发送消息时可以一次发送多条消息,提高消息发送效率。不过需要注意一下几点

  • 批量发送消息必须具有相同Topic
  • 批量发送消息必须具有相同的刷盘策略
  • 批量发送消息只能是普通消息,不能是延时消息或者事务消息

2.1.2 批量发送大小

默认情况下一批消息大小不能超过4M,如果超过4M有两种解决方案

  • 消息拆分,将大消息拆分成多个4M以内的消息
  • 修改Procedure和Broker的maxMessageSize属性

2.1.3 消息大小计算

在这里插入图片描述
Procedure调用send()方法发送的Message,并不只是将Message序列化后发送到网络上, 而是通过这个Message生成一个字符串发送出去。这个字符串由四部分构成:Topic,Body,Log,Properties(k-v形式,包括生产者地址,生产时间,要发送的queueId等)构成,最终写道Broker中消息单元中的数据都来自于这些属性

2.2 批量消费

2.2.1 批量消费数量

MessageListenerConcurrently监听接口默认每次只能消费一条消息,若要一次消费多条消息可以修改setConsumeMessageBatchMaxSize属性来指定,不能超过32,因为一次拉取消息最多32条。若要修改拉取大小可以设置setPullBatchSize

2.2.2 消费数量控制

setConsumeMessageBatchMaxSize和setPullBatchSize并不是越大越好。

  • setPullBatchSize值越大,Consumer每次拉取消息时间越长,网络传输出现的问题越高。若拉取出现问题那么本批消息都得重新拉取
  • setConsumeMessageBatchMaxSize值越大,Consumer并发消费能力越低,且这批消息具有相同的处理结果。因为每批消息只会用一个线程来处理,只要一个消息处理异常那么整批消息都需要重新再次消费

2.3 代码示例

2.3.1 消息分割器

public class MessageSplitter implements Iterator<List<Message>> {

    // 指定批次大小4M
    private final Integer SIZE_LIMIT = 4 * 1024 * 1024;
    // 要发送的消息
    private List<Message> messageList;
    // 消息批次索引
    private Integer currIndex = 0;

    public MessageSplitter(List<Message> messageList) {
        this.messageList = messageList;
    }

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

    @Override
    public List<Message> next() {
        int nextIndex = currIndex;
        int totalSize = 0;
        while (nextIndex < messageList.size()) {
            Message message = messageList.get(nextIndex);

            // 计算消息大小
            // 统计topic & body
            final int[] msgSize = {message.getTopic().length() + message.getBody().length};
            // 统计properties大小
            if (!CollectionUtils.isEmpty(message.getProperties())) {
                message.getProperties().forEach((k, v) -> msgSize[0] += k.length() + v.length());
            }
            // 统计日志
            msgSize[0] += 20;

            // 阈值判断
            if (totalSize + msgSize[0] > SIZE_LIMIT) {
                break;
            } else {
                totalSize += msgSize[0];
                nextIndex ++;
            }
        }
        List<Message> targetList = messageList.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return targetList;
    }
}

2.3.2 消息生产者

public class BatchProcedure {

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // procedureGroup全局唯一
        String producerGroup = "batch-procedure";

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.setNamesrvAddr(namesrv);
        // 设置消息发送超时时间
        producer.setSendMsgTimeout(1000);
        // 设置发送异步消息重试次数
        producer.setRetryTimesWhenSendAsyncFailed(3);
        producer.start();
        List<Message> messageList = new ArrayList<>();
        for (int i = 0; i < 100; i ++) {
            messageList.add(new Message("demo-topic", "demo-tag", ("msg-" + i).getBytes()));
        }
        MessageSplitter messageSplitter = new MessageSplitter(messageList);
        while (messageSplitter.hasNext()) {
            List<Message> list = messageSplitter.next();
            System.out.println("send msg list size:" + list.size());
            producer.send(list);
        }
        producer.shutdown();
    }

}

2.3.3 消息消费者

public class BatchConsumer implements MessageListenerConcurrently {

    public static void main(String[] args) throws MQClientException {
        // consumerGroup
        String consumerGroup = "batch-consumer";
        String subscribeTopic = "demo-topic";

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
        consumer.setNamesrvAddr(namesrv);
        // 从第一个消息开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe(subscribeTopic, "*");
        // 设置每次最大拉取消息量。仅在mq发生消息堆积时候有效
        consumer.setConsumeMessageBatchMaxSize(16);
        consumer.registerMessageListener(new BatchConsumer());
        consumer.start();
    }

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.println("receive msg size:" + msgs.size());
        for (MessageExt message: msgs) {
            String msg = new String(message.getBody(), StandardCharsets.UTF_8);
            System.out.println("receive msg:" + msg);
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

3 消息过滤

3.1 Tag过滤

consumer.subscribe("topic", "tagA || tagB");

3.2 SQL过滤

SQL过滤是通过特定的表达式来对消息的properties属性进行筛选过滤。只有Push消费模式才能使用SQL过滤
在这里插入图片描述
需要在broker配置文件中开启SQL过滤功能

enablePropertityFilter = true
  • 代码示例
consumer.subscribe(subscribeTopic, MessageSelector.bySql("age between 0 and 6"));

4 消息重试

4.1 生产者重试

4.1.1 重试机制

Procedure对发送失败的消息进行重试,需要注意以下几点

  • 生产者发送消息时,若采用同步或者异步发送会重试,oneway方式不会重试
  • 顺序消息没有重试机制,因为消息重试的时候会选择其他的broker尝试,而顺序消息只会发在特定Queue上
  • 消息重试尽量保证消息发送成功,但是当网络抖动,消息量大时可能发生消息重复,消费端需要做幂等处理
  • 消息发送重试有三种机制,同步发送失败、异步发送失败、消息刷盘失败

4.1.2 同步重试机制

对于普通消息,消息发送默认采用round-robin策略来选择所发送到的队列。如果发送失败,默认重试两次。但在重试的时候不会选择上次发送失败的broker,而是选择其他的broker。如果只有一个broker,则尝试发送到别的Queue

producer.setRetryTimesWhenSendFailed(3);

4.1.3 异步重试机制

异步发送失败重试时,重试机制不会选择其他broker,仅在同一个broker上重试,无法保证消息不丢。

producer.setRetryTimesWhenSendAsyncFailed(3);

4.1.4 消息刷盘失败重试

消息刷盘超时或slave不可用(slave返回master状态不是SEND_OK)时,默认是不会尝试将该消息发送给其他broker的,不过对于重要消息可以在broker的配置文件中设置retryAnotherBrokerWhenNotStoreOK=true开启

4.2 消息消费重试机制

4.2.1 顺序消息消费重试

顺序消息没有发送重试机制,但是有消费重试。对于顺序消息,Consumer消费失败后,为了保证消息顺序性 ,其会自动不断地进行消费重试,直到消费成功。消费重试默认时间间隔1000ms,重试期间会出现消息消费被阻塞的情况。
设置重试间隔时间

consumer.setSuspendCurrentQueueTimeMillis(100);

4.2.2 无序消息消费重试

对于无序消息(普通、延时、事务),当Consumer消费失败后可以通过返回重试状态(ConsumeConcurrentlyStatus.RECONSUME_LATER)来达到重试效果。不过消息重试仅对集群消费模式有效,广播消费模式不生效、

4.2.3 无序消息消费间隔

对于无序消息集群消费模式下的重试消费,每条消息默认最多重试16次,重试间隔逐步变长
在这里插入图片描述
修改消息重试次数

consumer.setMaxReconsumeTimes(10);

对于修改后的重试间隔,如果<16,则按照指定间隔进行重试
如果>16,则超过16次的重试时间间隔均为两小时
对于consumerGroup,如果仅修改了一个Consumer的消费重试次数,则会应用到该Group中所有Consumer实例。如果多个Consumer均作了修改则采用覆盖方式生效,即最后被修改的值会覆盖前面设置的值

4.2.4 重试队列

对于需要重试消费的消息,并不是Consumer等待了指定时长后再去拉取消息消费,而是将这些重试消息放到特殊的Topic中,然后再次消费,这些队列就是重试队列。这个队列是针对消费者组设置的,当出现需要重试的消息时才会为该消费者组创建重试队列
Broker对重试消息的处理是通过延时消息实现的,先将消息保存到SCHEDULE_TOPIC_XXXX延时队列中,达到延时时间后会投递到重试队列。

5 死信队列

消息重试达到最大次数后若依旧消费失败则会进入到死信队列中,死信队列具有如下特征

  • 死信队列的消息不会被消费,即死信队列对于消费者来说是不可见的
  • 死信队列存储有效期与正常消息相同,都是3天,即commitlog文件的过期时间,3天后会被自动删除
  • 每个消费者组都有一个死信队列,在发生死信消息的时候创建
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果你使用的是 RocketMQJava 客户端,可以通过使用 `MessageListenerConcurrently` 接口来开发消费者应用程序。接口中的方法 `consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context)` 可以在消息到达时自动调用。当应用程序在处理消息时出现异常,可以使用 `context.setDelayLevelWhenNextConsume(delayLevel)` 方法来设置下次消费的延迟级别,从而实现自动重启消费线程。 以下是示例代码: ```java public class MyMessageListener implements MessageListenerConcurrently { private DefaultMQPushConsumer consumer; public MyMessageListener(DefaultMQPushConsumer consumer) { this.consumer = consumer; } @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { try { // 处理消息 for (MessageExt message : msgs) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(message.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } catch (Exception e) { // 异常处理,设置延迟级别 context.setDelayLevelWhenNextConsume(5); e.printStackTrace(); return ConsumeConcurrentlyStatus.RECONSUME_LATER; } } } ``` 在应用程序中,可以使用以下代码来启动消费者: ```java public class MyConsumer { public static void main(String[] args) throws MQClientException { // 创建消费者 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my_consumer_group"); consumer.setNamesrvAddr("localhost:9876"); // 订阅主题和标签 consumer.subscribe("my_topic", "my_tag"); // 注册消息监听器 MyMessageListener messageListener = new MyMessageListener(consumer); consumer.registerMessageListener(messageListener); // 启动消费者 consumer.start(); } } ``` 在处理消息时发生异常时,会调用 `consumeMessage` 方法中的异常处理代码,并设置下次消费的延迟级别为 5 秒。这个级别越高,下次消费的时间就越晚。当级别为 0 时,表示立即重新消费。如果重试多次后仍然失败,可以将消息发送到死信队列,进行手动处理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值