深入浅出RocketMQ(四):广播、集群、拉取、推送、顺序消费

前言

    上一章我们了解了RocketMQ的一些基本概念,这章我们将会通过一些实例结合实际场景对一些消费模式进行了解,并通过源码解读的形式,深入了解其原理

    本章节所有代码可以前往github下载自行查看——rocket demo

1、拉取式消费 & 推动式消费

    假如你订阅了一个微信公众号,那么你查看公众号消息的形式无外乎两种:

        1. 公众号给你推送消息

        2. 你主动点开公众号去查看消息

    对于RocketMQ的消费者而言,也同样有这两种模式。

1.1 拉取式消费(Pull)

    在RocketMQ使用拉取式消费,其实就是用户主动去获取一次该主题下队列的所有消息,然后根据不同的情况自行处理。

    拉取式消费的用法相对比较复杂,我们要先需要通过DefaultMQPullConsumer获取订阅主题下的队列,遍历队列后取出队列里的消息。

    这里只放部分代码,具体demo可以去GitHub上面查看

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicStudent");
        for (MessageQueue mq : mqs) {
            System.err.println("Consume from the queue: " + mq);
            SINGLE_MQ:
            while (true){
                try {
                    PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    System.out.println(pullResult);
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());

                    switch (pullResult.getPullStatus()) {
                        case FOUND:
                            List<MessageExt> messageExtList = pullResult.getMsgFoundList();
                            for (MessageExt m : messageExtList) {
                                System.out.println(new String(m.getBody()));
                            }
                            break;
                        case NO_NEW_MSG:
                            break SINGLE_MQ;
                        default:
                            break;
                    }

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


1.2 推动式消费(Push)

    上面的拉取式消费还只是部分代码就已经这么繁琐,相对于拉取式消费,推动式消费的代码量就简洁多了:

public class EasyConsumer {

    private DefaultMQPushConsumer consumer;

    public EasyConsumer() throws Exception {
        consumer = new DefaultMQPushConsumer(JmsConfig.GROUP);
        consumer.setNamesrvAddr(JmsConfig.NAME_SERVER);
        consumer.subscribe(JmsConfig.TOPIC_EASY, "easy-message-1");
        consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
            for (Message msg : list) {
                //消费者获取消息 这里只输出 不做后面逻辑处理
                System.out.println("---------消费消息:" + LocalTime.now());
                String body = new String(msg.getBody(), StandardCharsets.UTF_8);
                System.out.println(body);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.println("consumer启动...");
    }

}

    按照正常的逻辑去理解,推动式消费的原理应该是BrokerServer收到消息后主动通知消费者进行消费。然而我要告诉你的是,在RocketMQ中实际上是没有所谓的推动式消费。

    所谓的推动式消费其实只是RocketMQ帮你封装了一个线程去轮询,不断的去做拉取式消费要做的事情。从使用者的层面来看,就好像是BrokerServer收到消息后主动通知你有新消息一样。

    关于这一点我们在源码中也可以证实:

    首先我们进入DefaultMQPushConsumer的start()方法,然后依次进入this.defaultMQPushConsumerImpl.start(),this.mQClientFactory.start();

    然后我们观察这个类的start方法

public void start() throws MQClientException {
        synchronized(this) {
            switch(this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                if (null == this.clientConfig.getNamesrvAddr()) {
                    this.mQClientAPIImpl.fetchNameServerAddr();
                }

                this.mQClientAPIImpl.start();
                this.startScheduledTask();
                this.pullMessageService.start();
                this.rebalanceService.start();
                this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                this.log.info("the client factory [{}] start OK", this.clientId);
                this.serviceState = ServiceState.RUNNING;
            case RUNNING:
            case SHUTDOWN_ALREADY:
            default:
                return;
            case START_FAILED:
                throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", (Throwable)null);
            }
        }
    }

    事实上在这里我们就能看出端倪了,这个方法里面根本没有Push相关的代码,我们再点进去PullMessageService类就能发现,这个类实现了runnable接口,而它的start方法中做的事情,是不断从pullRequestQueue队列里获取消息

public void run() {
        this.log.info(this.getServiceName() + " service started");

        while(!this.isStopped()) {
            try {
                PullRequest pullRequest = (PullRequest)this.pullRequestQueue.take();
                this.pullMessage(pullRequest);
            } catch (InterruptedException var2) {
            } catch (Exception var3) {
                this.log.error("Pull Message Service Run Method exception", var3);
            }
        }

        this.log.info(this.getServiceName() + " service end");
    }

    整个流程大概如下图:


在这里插入图片描述

    既然有从队列中取数据,那么就一定有从队列中放数据的方法,我们可以在这个类中找到put的相关代码:

public void executePullRequestLater(final PullRequest pullRequest, long timeDelay) {
        if (!this.isStopped()) {
            this.scheduledExecutorService.schedule(new Runnable() {
                public void run() {
                    PullMessageService.this.executePullRequestImmediately(pullRequest);
                }
            }, timeDelay, TimeUnit.MILLISECONDS);
        } else {
            this.log.warn("PullMessageServiceScheduledThread has shutdown");
        }

    }

到了这里,相信各位读者已经能明白了,一开始我们进来的明明是Push的start()方法,可是最后干活的确是PullMessageService(这说明哪有什么岁月静好,只不过是PullMessageService帮你负重前行了)

2、广播消费 & 集群消费

    顾名思义

        1. 广播消费模式下,相同的Consumer Group的每个Consumer实例都接收全量的消息

        2. 集群消费模式下,相同的Consumer Group的每个Consumer实例均摊消息

2.1 广播消费(BROADCASTING)

    从代码而言,广播消费和集群消费的实现很简单,我们只需要设置consumer的message model即可:

consumer.setMessageModel(MessageModel.BROADCASTING);


2.2 集群消费(CLUSTERING)

consumer.setMessageModel(MessageModel.CLUSTERING);


3、顺序消费

    假设我们在做一个电商系统,那么对于一个用户创建订单到完成支付更新整个流程而言,整个过程在rocketMQ应该保证其消费的有序性。

    顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。

        1. 全局顺序 对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景

        2. 分区顺序 对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。 Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。

    在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列),而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。

    但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

    因此从消费者的角度而言,我们不需要改动什么,只需要在生产者生产消息时保证投递到同一队列即可:

	DefaultMQProducer mqProducer = orderedProduce.getProducer();
        //创建生产信息
        for (int i = 0; i < 5; i++) {
            Message message = new Message(JmsConfig.TOPIC_ORDER, "order_message", ("创建订单:" + i).getBytes());
            mqProducer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    int id = (int) o;
                    int index = id % list.size();
                    return list.get(index);
                }
            },i);
        }

        for (int i = 0; i < 5; i++) {
            Message message = new Message(JmsConfig.TOPIC_ORDER, "order_message", ("支付订单:" + i).getBytes());
            mqProducer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    int id = (int) o;
                    int index = id % list.size();
                    return list.get( index);
                }
            },i);
        }

        for (int i = 0; i < 5; i++) {
            Message message = new Message(JmsConfig.TOPIC_ORDER, "order_message", ("发货:" + i).getBytes());
            mqProducer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    int id = (int) o;
                    int index = id % list.size();
                    return list.get( index);
                }
            },i);
        }


4 结尾

    本章我们学习了一些简单方法的使用,但是对于电商而言,用户创建订单会有一个锁定库存超时自动释放的逻辑,针对这个场景,RocketMQ也有相应的延时消费模式可以解决,下一章,我们将学习如何使用延时消费解决锁库存问题及其实现原理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值