RocketMQ消息收发

108 阅读11分钟

dc7336ee1c8e25e2c963341618112b54.png

简单消息

Snipaste_2023-11-06_08-53-11.png

星形是消息

同步消息

⽣产者发送消息后,必须等待broker返回信息后才继续之后的业务逻辑,在broker 返回信息之前,⽣产者阻塞等待。

应⽤场景:如重要通知消息、短信通知、短信营销系统等。

生产者

public class MyProducer {
    public static void main(String[] args) throws Exception {
        //1.创建生产者
        DefaultMQProducer producer =
                new DefaultMQProducer("my-producer1");
        //2.指定nameserver地址
        producer.setNamesrvAddr("192.168.150.102:9876");
        //3.启动生产者
        producer.start();
        //4.创建消息
        for (int i = 0; i < 10; i++) {
            Message message = new Message("MyTopic1", "TagA", ("hello" + i).getBytes(StandardCharsets.UTF_8));
            //5.发送消息
            SendResult sendResult = producer.send(message);
            System.out.println(sendResult);
        }
        //6.关闭生产者
        producer.shutdown();
    }
}

消费者

public class MyConsumer {
    public static void main(String[] args) throws Exception {
        //1.创建消费者对象
        DefaultMQPushConsumer consumer = new
                DefaultMQPushConsumer("my-consumer1");
        //2.指定nameserver地址
        consumer.setNamesrvAddr("192.168.150.102:9876");
        //3.订阅主题
        consumer.subscribe("MyTopic1", "*");
        //4.创建监听器,监听broker中的消息
        consumer.registerMessageListener(
                new MessageListenerConcurrently() {
                    @Override
                    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                        for (MessageExt msg:msgs){
                            System.out.println("收到的消息:"+new String(msg.getBody()));
                        }
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    }
                });
        //启动消费者
        consumer.start();
        System.out.printf("消费者已启动");
    }
}

异步消息

⽣产者发完消息后,不需要等待broker的回信,可以直接执⾏之后的业务逻辑。⽣ 产者提供⼀个回调函数供broker调⽤,体现了异步的⽅式。

异步传输⼀般⽤于响应时间敏感的业务场景。

生产者

public class AsyncProducer {
    public static void main(String[] args) throws Exception{
        //1.创建生产者
        DefaultMQProducer producer =
                new DefaultMQProducer("my-producer1");
        //2.指定nameserver地址
        producer.setNamesrvAddr("192.168.150.102:9876");
        //3.启动生产者
        producer.start();
        //设置异步消息发送失败的重试次数
        producer.setRetryTimesWhenSendAsyncFailed(0);

        int messageCount = 100;
        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();
            }
        }
        System.out.println("=============");
        //设置阻塞时间确保100条消息都能发送
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}

单向消息

⽣产者发送完消息后不需要等待任何回复,直接进⾏之后的业务逻辑,单向传输⽤ 于需要中等可靠性的情况,例如⽇志收集。

生产者

public class OnewayProducer {
    public static void main(String[] args) throws Exception{
        //1.创建生产者
        DefaultMQProducer producer =
                new DefaultMQProducer("my-producer1");
        //2.指定nameserver地址
        producer.setNamesrvAddr("192.168.150.102:9876");
        //3.启动生产者
        producer.start();
        for (int i = 0; i < 100; i++) {
            //Create a message instance, specifying topic, tag and message body.
            Message msg = new Message("TopicTest",
                    "TagA",
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            //和同步消息发送的不同点
            producer.sendOneway(msg);
        }
        //确保消息全部发出
        Thread.sleep(5000);
        producer.shutdown();
    }
}

顺序消息

原本的生产者发送消息到broker不一定存放到那个消息队列

顺序消息指的是消费者消费消息的顺序按照发送者发送消息的顺序执⾏。顺序消息 分成两种:局部顺序和全局顺序。

生产者

public class OrderProducer {
    public static void main(String[] args) throws Exception{
        //Instantiate with a producer group name.
        MQProducer producer = new DefaultMQProducer("example_group_name");
        //名字服务器的地址已经在环境变量中配置好了:NAMESRV_ADDR=172.16.253.101:9876
        //Launch the instance.
        producer.start();
        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));
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    /**
                     * select()方法从给定的消息队列列表中根据特定规则选择一个MessageQueue进行消息发送。
                     * 具体而言,该select()方法首先将参数id转换为Integer类型,然后通过对mqs列表的大小取余获						得一个索引值,
                     * 最后返回mqs列表中该索引对应的MessageQueue。
                     * @param mqs 消息队列
                     * @param msg 消息主体
                     * @param arg 对应orderId
                     * @return
                     */
                    @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);
            }
        }
        //server shutdown
        producer.shutdown();
    }
}

局部顺序

局部消息指的是消费者消费某个topic的某个队列中的消息是顺序的。消费者使⽤ MessageListenerOrderly类做消息监听,实现局部顺序。

消费者

public class OrderConsumer {
    public static void main(String[] args) throws Exception {

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
        //设置消费者从第一个开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("OrderTopicTest", "*");
                                        //核心
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
                                                       ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for(MessageExt msg:msgs){
                    System.out.println("消息内容:"+new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;

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

全局顺序 消费者消费全部消息都是顺序的,只能通过⼀个某个topic只有⼀个队列才能实现, 这种应⽤场景较少,且性能较差。

乱序消费

消费者消费消息不需要关注消息的顺序。消费者使⽤MessageListenerConcurrently 类做消息监听。

和局部顺序相比不同的是new的内部类不同,MessageListenerConcurrently

⼴播消息

⼴播是向主题(topic)的所有订阅者发送消息。订阅同⼀个topic的多个消费者,能 全量收到⽣产者发送的所有消息。

消费者:在乱序消费的基础上

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
//设置消费者从第一个开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//设置消费者的广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("OrderTopicTest", "*");

生产者还是发送同步消息

延迟消息

延迟消息与普通消息的不同之处在于,它们要等到指定的时间之后才会被传递。

生产者

for (int i = 0; i < 10; i++) {
    Message message = new Message("MyTopic1", "TagA", ("hello" +i).getBytes(StandardCharsets.UTF_8));
    //5.发送消息前设置延迟等级
    message.setDelayTimeLevel(3);
    SendResult sendResult = producer.send(message);
    System.out.println(sendResult);
}

消费者采用乱序消费

consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
                for (MessageExt message : messages) {
                    // Print approximate delay time period
                    System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
                            + (System.currentTimeMillis() - message.getStoreTimestamp()) +"mslater");
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

延迟等级 RocketMQ设计了18个延迟等级,分别是:

  • 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

系统为这18个等级配置了18个topic,⽤于实现延迟队列的效 果。

在商业版RocketMQ中,不仅可以设置延迟等级,还可以设置具体的延迟时间,但是 在社区版RocketMQ中,只能设置延迟等级。

批量消息

批量发送消息提⾼了传递⼩消息的性能。

常规的批量消息发送

public class BatchProducer {
    public static void main(String[] args) throws Exception{
        DefaultMQProducer producer =
                new DefaultMQProducer("MyGroup1");
        //也可以在配置中编辑环境变量:NAMESRV_ADDR=192.168.150.102:9876
        producer.setNamesrvAddr("IP地址:端口号");
        producer.start();
        String topic="BatchTest";
        //不同点:一般生产者是通过for循环发送,批量则是通过发送集合类实现
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(topic,"TagA","OrderD001","Hello0".getBytes()));
        messages.add(new Message(topic,"TagA","OrderD002","Hello1".getBytes()));
        messages.add(new Message(topic,"TagA","OrderD003","Hello2".getBytes()));
        producer.send(messages);
        producer.shutdown();
    }
}

超出限制的批量消息

官⽅建议批量消息的总⼤⼩不应超过1m,实际不应超过4m。如果超过4m的批量消 息需要进⾏分批处理,同时设置broker的配置参数为4m(在broker的配置⽂件中修 改: maxMessageSize=4194304 )

public class MaxBatchProducer {
    public static void main(String[] args) throws Exception {

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

        String topic = "BatchTest";
        List<Message> messages = new ArrayList<>(100*1000);
        for (int i = 0; i < 100*1000; i++) {
            messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes()));
        }
        //把大批量分成多个小批量
        ListSplitter splitter = new ListSplitter(messages);
        while (splitter.hasNext()) {
            List<Message> listItem = splitter.next();
            producer.send(listItem);
        }
        producer.shutdown();
    }
}

官方提供的类ListSplitter用于分割消息

public class ListSplitter implements Iterator<List<Message>> {
    private int sizeLimit = 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();
            }
            //计算出单条消息的长度
            tmpSize = tmpSize + 20;
            if (tmpSize > sizeLimit) {
                //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 > sizeLimit) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }
}

使⽤限制 同⼀批次的消息应该具有:相同的主题、相同的 waitStoreMsgOK 并且不⽀持延迟 消息和事务消息。

过滤消息

在⼤多数情况下,标签是⼀种简单⽽有⽤的设计,可以⽤来选择您想要的消息。

生产者

public class TagProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        producer.start();

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

        for (int i = 0; i < 15; i++) {
            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();
    }
}

消费者

public class TagConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
        //通过订阅不同的标签来进行消息过滤
        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");
    }
}

消费者将收到包含 TAGA 或 TAGB 或 TAGC 的消息。但是限制是⼀条消息只能有⼀个 标签,这可能不适⽤于复杂的场景。在这种情况下,您可以使⽤ SQL 表达式来过滤 掉消息。

使⽤SQL过滤

使⽤注意:只有推模式的消费者可以使⽤SQL过滤。拉模式是⽤不了的。

生产者:在标签过滤生产者的基础上

for (int i = 0; i < 15; i++) {
    Message msg = new Message("SqlFilterTest",
            tags[i % tags.length],
            ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
    );
    //不同点  Set some properties.
    msg.putUserProperty("a", String.valueOf(i));

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

消费者:设置订阅规则

public class SQLConsumer {
    public static void main(String[] args) throws MQClientException {

        DefaultMQPushConsumer consumer = newDefaultMQPushConsumer("please_rename_unique_group_name");

        //核心 Don't forget to set enablePropertyFilter=true in broker
        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)"));

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

事务消息

  • 事务消息的定义
    • 它可以被认为是⼀个两阶段的提交消息实现,以确保分布式系统的最终⼀致性。事务性消息确保本地事务的执⾏和消息的发送可以原⼦地执⾏。
  • 事务消息有三种状态:
    • a.TransactionStatus.CommitTransaction:提交事务,表示允许消费者消费该消息。
    • b.TransactionStatus.RollbackTransaction:回滚事务,表示该消息将被删除,不允许 消费。
    • c.TransactionStatus.Unknown:中间状态,表示需要MQ回查才能确定状态。
  • 事务消息的实现流程 Snipaste_2023-11-06_18-38-35.png 重点在事务的生产者,消费者和基础的没区别

自定义事件监听器

public class TransactionListenerImpl implements TransactionListener {
    /**
     * 当事务消息发送成功后执行本地事务
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String tags = msg.getTags();
        if(StringUtils.contains(tags,"TagA")){
            return LocalTransactionState.COMMIT_MESSAGE;
        }else if(StringUtils.contains(tags,"TagB")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else{
            //标记为未知消息,隔段时间进行回查
            return LocalTransactionState.UNKNOW;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        String tags = msg.getTags();
        if(StringUtils.contains(tags,"TagC")){
            return LocalTransactionState.COMMIT_MESSAGE;
        }else if(StringUtils.contains(tags,"TagD")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
}

事务生产者

public class TransactionProducer {
    public static void main(String[] args) throws Exception {
        //自定义事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
        producer.setNamesrvAddr("172.16.253.101: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));
                                                //发送事务消息
                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();
    }
}

事务消费者

public class TransactionConsumer {
    public static void main(String[] args) throws MQClientException {
        //1.创建消费者对象
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer-group1");
        //2.指明nameserver的地址
        consumer.setNamesrvAddr("172.16.253.101:9876");
        //3.订阅主题:topic 和过滤消息用的tag表达式
        consumer.subscribe("TopicTest","*");
        //4.创建一个监听器,当broker把消息推过来时调用
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {

                for (MessageExt msg : msgs) {
//                    System.out.println("收到的消息:"+new String(msg.getBody()));
                    System.out.println("收到的消息:"+msg);
                }

                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //5.启动消费者
        consumer.start();
        System.out.println("消费者已启动");
    }
}

使⽤限制

  • 事务性消息没有调度和批处理⽀持。

  • 为避免单条消息被检查次数过多,导致半队列消息堆积,我们默认将单条 消息的检查次数限制为15次,但⽤户可以通过更改“transactionCheckMax”来 更改此限制”参数在broker的配置中,如果⼀条消息的检查次数超过 “transactionCheckMax”次,broker默认会丢弃这条消息,同时打印错误⽇ 志。⽤户可以通过重写“AbstractTransactionCheckListener”类来改变这种⾏ 为。

  • 事务消息将在⼀定时间后检查,该时间由代理配置中的参数“transactionTimeout”确定。并且⽤户也可以在发送事务消

    息时通过设置⽤ 户属性“CHECK_IMMUNITY_TIME_IN_SECONDS”来改变这个限制,这个参数 优先于“transactionMsgTimeout”参数。

  • ⼀个事务性消息可能会被检查或消费不⽌⼀次。

  • 提交给⽤户⽬标主题的消息reput可能会失败。⽬前,它取决于⽇志记录。 ⾼可⽤是由 RocketMQ 本身的⾼可⽤机制来保证的。如果要保证事务消息不 丢失,保证事务完整性,推荐使⽤同步双写机制。

  • 事务性消息的⽣产者 ID 不能与其他类型消息的⽣产者 ID 共享。与其他类型 的消息不同,事务性消息允许向后查询。MQ 服务器通过其⽣产者 ID 查询客 户端。