MQ消费模式和几种消息发送模式

156 阅读4分钟

官网

消费模式

pull模式

消费者主动从消息中间件中拉取消息。
pull模式每次接收消息都需要拉取一下队列的信息,由于是拉取的,实时性较差,不能及时有效获取最新的信息,能有效降低内存消耗。

push模式(底层也是pull模式)

消息中间件主动将消息推送给消费者。 客户端需要做好流控。
push模式接收消息是最有效的一种消息处理方式。当我们使用该模式时,我们的消费端只要启动后,就相当于使用了订阅模式,只要生产不断推送信息,消费端就会持续接收信息

如何选择

一般场景下,上游消息生产量小、或者均速、选择push。
秒杀场景下,可以选择pull模式。

消息发送模式

普通消息

发送同步消息

消息发送后会获得一个返回值,mq服务器接收到消息后返回的一个确认,非常安全,但是性能低,而且在 mq集群中,所有的从机都复制了消息以后才会返回,针对重要的消息可以选择这个模式 image.png

发送异步消息

异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式.发送异步消息需要实现异步接口 sendCallback。 对实现比较敏感 image.png

使用

image.png

public void asyncProducer() throws MQClientException, RemotingException, InterruptedException, IOException {
    DefaultMQProducer defaultMQProducer = new DefaultMQProducer("async-producer-group");
    defaultMQProducer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    defaultMQProducer.start();
    Message msg = new Message("asyncTopic", "一个异步消息的发送".getBytes(StandardCharsets.UTF_8));
    for (int i = 0; i < 100; i++) {
        defaultMQProducer.send(msg, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("发送成功");
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println("发送失败" + throwable.getMessage());
            }
        });
    }
    System.out.println("主线程执行");
    // 收到异步消息
    System.in.read();
}

发送成功的回调函数

image.png

发送单项消息

不关心发送结果的场景,吞吐量很大,但是存在消息丢失的风险。 例如 日志信息的发送。
发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

三种对比

image.png

延迟消息

在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的延时事件触发。使用 RocketMQ 的延时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。

代码 demo。

/**
 * 推迟生产
 */
@Test
public void putOffProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
    DefaultMQProducer defaultMQProducer = new DefaultMQProducer("delay-producer-group");
    defaultMQProducer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    defaultMQProducer.start();
    Message message = new Message("orderMsTopic","延迟消息".getBytes(StandardCharsets.UTF_8));
    // 10s 后收到 在配置文件中可以去指定 等级对应的时间
    message.setDelayTimeLevel(3);
    defaultMQProducer.send(message);
    System.out.println("发送时间" + new Date() );
    defaultMQProducer.shutdown();
}

/**
 * 延迟消费者
 * 第一次测试 ---> 比较慢 可能在加载一些东西
 * 发送时间 Mon Jun 12 15:27:12 CST 2023
 * 收到时间 Mon Jun 12 15:27:49 CST 2023
 * =====================================
 * 第二次就快了 
 * =====================================
 * 发送时间 Mon Jun 12 15:30:20 CST 2023
 * 收到时间 Mon Jun 12 15:30:30 CST 2023
 */
@Test
public void msConsumer() throws MQClientException, IOException {
    DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("delay-consumer-group");
    defaultMQPushConsumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    defaultMQPushConsumer.subscribe("orderMsTopic","*");
    defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            System.out.println("收到消息了" + new Date());
            byte[] body = list.get(0).getBody();
            String s = new String(body);
            System.out.println(s);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    defaultMQPushConsumer.start();
    System.in.read();
}

批量消息

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

发送到的是同一个队列,并没有通过轮询方式发送。 image.png

@Test
public void batchProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
    DefaultMQProducer defaultMQProducer = new DefaultMQProducer("batch-producer-group");
    defaultMQProducer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    defaultMQProducer.start();
    ArrayList<Message> list = new ArrayList<>();
    String topic = "batchTest" ;
    for (char a  = 'a' ; a < 'c' ; a ++) {
        list.add(new Message(topic,("我是一组消息中的" + a + "消息").getBytes(StandardCharsets.UTF_8)));
    }
    SendResult send = defaultMQProducer.send(list);
    System.out.println(send.getSendStatus());
    defaultMQProducer.shutdown();
}
@Test
public void msConsumer() throws MQClientException, IOException {
    DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("batch-consumer-group");
    defaultMQPushConsumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    defaultMQPushConsumer.subscribe("batchTest","*");
    defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            System.out.println("收到消息了" + new Date());
            System.out.println("收到消息的数组大小" + list.size());
            byte[] body = list.get(0).getBody();
            String s = new String(body);
            System.out.println(s);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    defaultMQPushConsumer.start();
    System.in.read();
}

顺序消息的发送

先看一下发送一个普通消息的情况图。在四个队列中,负载均衡的发。

image.png

并发模式的消息接收
......
* 我是第77个消息
* 我是第43个消息
* 我是第59个消息
* 我是第79个消息
* 我是第83个消息
......
消息 sharding key 分区发送
private List<Msg> list = Arrays.asList(
        new Msg(1,1,"下单"),
        new Msg(2,2,"下单"),
        new Msg(1,1,"短信"),
        new Msg(2,2,"短信"),
        new Msg(1,1,"物流"),
        new Msg(2,2,"物流")
);

@Test
public void orderProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("order-producer-group");
    producer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    producer.start();
    // 发送顺序 消息 , 发送时确保有序,并且要发到同一个队列里面去。
    list.forEach(msg -> {
        Message orderTopic = new Message("orderTopic", msg.toString().getBytes(StandardCharsets.UTF_8));

        try {
            producer.send(orderTopic, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    //int idx = o.hashCode() % list.size();
                    return list.get(idx);
                }
            },msg.getOrderId());
        } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            e.printStackTrace();
        }
    });
    producer.shutdown();
    System.out.println("发送完毕");
}

image.png

并发顺序模式接收
Msg(id=1, orderId=1, desc=下单)
Msg(id=2, orderId=2, desc=下单)
Msg(id=1, orderId=1, desc=短信)
Msg(id=2, orderId=2, desc=短信)
Msg(id=1, orderId=1, desc=物流)
Msg(id=2, orderId=2, desc=物流)
demo 代码
@Test
public void orderConsumer() throws MQClientException, IOException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-consumer-group");
    consumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
    consumer.subscribe("orderTopic","*") ;
    consumer.registerMessageListener(new MessageListenerOrderly() {
        @Override
        public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {

            System.out.println("消息上下文"  + consumeOrderlyContext);
            System.out.println("当前消息" + list.get(0));
            return ConsumeOrderlyStatus.SUCCESS;
        }
    });
    consumer.start();
    System.in.read();
}