Spring Cloud Alibaba 教程 | RocketMQ(二):生产者和消费者

核心概念

在这里插入图片描述
上图是RocketMQ一些核心概念组件之间的关系图,在深入讲解RocketMQ生产者和消费者之前我们先来熟悉一下RocketMQ的核心概念。

Producer生产者

生产者将应用系统生成消息发送给Brokers。RocketMQ提供了多种消息的发送方式:同步、异步和单向。

Producer Group生产者组

具有相同角色的生产者被分配到一组。如果组里面的一个生产者在事务之后崩溃,那么同一个组内的另外一个不同的生产者可能会连接Broker去执行commit或者roll back事务操作。
警告:考虑到生产者在发送消息方面足够强大,因此每个生产者组只允许有一个实例,以避免不必要的其他生产者实例初始化。

Consumer消费者

消费者从Broker拉取消息后交给应用程序处理,从应用程序的角度来看,提供了两种消费者类型:PullConsumer和PushConsumer。

PullConsumer拉取方式消费者

PullConsumer会积极地从Brokers拉取消息,一旦拉取到批量的消息,用户程序将开始执行消费处理。

PushConsumer推送方式消费者

PushConsumer封装了消息拉取、消费进度,并在内部维护其他工作。将回调接口留给用户去最终实现,该接口将在消息推送过来时回调执行。

Consumer Group消费者组

与前面提到的生产者组相似,角色完全相同的消费者被分组在一起,并命名为消费者组。
消费者组是一个很棒的概念,通过它可以很轻松地实现消息在消费方面的负载均衡(CLUSTERING)和容错(BROADCASTING)目标。
警告:消费者组里的消费者实例必须具有完全相同的主题(Topic)订阅。

Topic主题

Topic代表了消息(Message)的类别,消息就是指生产者传递的消息和消费者拉取的消息。Topic与生产者和消费者之间的关系非常松散。具体来说,一个Topic可能有零个,一个或多个向其发送消息的生产者。相反,生产者可以发送不同Topic的消息。从消费者的角度来看,一个Topic可以由零个,一个或多个消费者组订阅。与此类似,消费者组可以订阅一个或多个Topic,只要该组的实例保持其订阅一致即可。

Message消息

消息是要被传递的信息。 一个消息必须包含一个主题(Topic),该主题可以理解为你发送邮件的地址。消息还可能具有可选标签(Tag)和额外的键值对。 例如,在开发过程中你可以为消息设置业务key,然后在Broker Server上查找消息来诊断问题。

Message Queue消息队列

主题(Topic)代表了消息的类别,一个主题包含了多个消息队列(Message Queue)。

Tag标签

标签(也被称为子主题)为用户提供了额外的灵活性。 使用标签,来自同一业务模块而目的不同的消息可以具有相同的主题和不同的标签。所以RocketMQ使用了Topic和Tag来区分一个具体消息,并且标签将有助于保持代码整洁和一致,还可以简化RocketMQ提供的查询系统。

Broker

Broker是RocketMQ系统的主要组成部分。它接收从生产者发送的消息,进行存储,并准备处理来自消费者的拉取请求。它还存储与消息相关的元数据,包括使用者组,使用者进度偏移量(offsets)和主题/队列信息。

Name Server

Name Server充当了一个协调者的角色,负责管理Broker,生产者和消费者可以通过Name Server查找主题(Topic)对应的Broker列表。在集群环境有多个Broker的时候,主题包含的消息队列(Message Queue)将分布在多个Broker上。

Message Model消息模型

Clustering:集群模式,消费者组内的消费者平均消费Topic的消息。
Broadcasting:广播模式,消费者组内的消费者消费到Topic的所有消息。

Message Order有序消息

使用PushConsumer时,您可以决定按顺序(Orderly)或同步(Concurrently)两种方式来消费消息。

顺序消费消息意味着消息的消费顺序与生产者消息发送顺序要相同。通常有两种做法:1、确保该消息对应的主题只有一个消息队列。2、或者将要顺序的消息发送到主题下的某个指定的消息队列。
警告:如果使用有序消费消息,那么消息消费的最大并发数就是消费者组订阅主题的消息队列数。因为此时对于消费者来说主题下的每一个消息队列都将只使用单线程来处理。

使用同步消费消息时,消息消费最大并发数取决于消费者客户端指定的线程池限制。
警告:由于使用了多线程去消费消息,所以在此模式下不再保证消息顺序。

Producer生产者

生产者根据不同的业务场景需求可以采取不同的发送策略。例如同步发送、异步发送、延迟发送、发送单向消息、发送事务消息等等,下面具体介绍。

同步发送消息

public class SyncProducer {

    public static void main(String[] args) {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName-1");//@1
            producer.setInstanceName("producer1");
            producer.setNamesrvAddr("192.168.0.149:9876");//@2
            producer.start();
            for(int i = 0; i < 10; i++){
                Message message = new Message("TopicTest","TagA",
                        ("Hello RocketMQ "+i).getBytes(RemotingHelper.DEFAULT_CHARSET));//@3
                SendResult sendResult = producer.send(message);//@4
                SendStatus sendStatus = sendResult.getSendStatus();
                System.out.println("sendResult:"+sendResult+",sendStatus"+sendStatus.name());
            }
            producer.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

代码@1:设置Producer的GroupName和InstanceName。通过InstanceName可以标识一个生产者实例,通过GroupName可以标识一组生产者实例。

代码@2:设置NameServer地址,无论是发送者还是生产者都是通过连接NameServer去发现和获取Broker、Topic和MessageQueue等信息。

代码@3:组装消息,通过设置Topic和Tag用来区分消息类别。

代码@4:同步发送消息,发送之后返回SendResult,通过SendResult可以获取到消息发送状态SendStatus,SendStatus有四个值:

public enum SendStatus {
    SEND_OK,
    FLUSH_DISK_TIMEOUT,
    FLUSH_SLAVE_TIMEOUT,
    SLAVE_NOT_AVAILABLE,
}
  • FLUSH_DISK_TIMEOUT:表示刷盘超时(需要Broker的刷盘策略设置成SYNC_FLUSH,表示Broker接收到消息需要同步存储到磁盘之后才返回发送成功)
  • FLUSH_SLAVE_TIMEOUT:Broker在主备模式下(Broker设置SYNC_MASTER),规定时间内没有完成主备同步。
  • SLAVE_NOT_AVAILABLE:Broker在主备模式下(Broker设置SYNC_MASTER),没有找到Slave的Broker。
  • SEND_OK:表示消息发送成功。当Broker设置不同的刷盘策略、主从策略时,获取到该结果也表示完成了相应的刷盘策略和主从策略。

异步发送消息

public class AsyncProducer {

    private static final String GROUP_NAME = "GROUP_NAME_ONE";

    private static final String NAME_SERVER_ADDRESS = "192.168.0.149:9876";

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer(GROUP_NAME);
        producer.setNamesrvAddr(NAME_SERVER_ADDRESS);
        producer.start();

        for (int i = 0; i < 100; i++) {
            final String keys = "keys_"+i;
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message("TopicTest",
                    "TagA",
                    keys,
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            //发送消息异步等待Broker结果
            producer.send(msg, new SendCallback() { //@1
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.println("MsgId: "+sendResult.getMsgId());
                }
                @Override
                public void onException(Throwable e) {
                    System.out.println("msgId :"+keys+"exception:"+e);
                }
            });

        }
        producer.shutdown();
    }
}

代码@1:异步发送总体流程和同步发送相同,区别在于异步发送需要生产者自己实现SendCallback回调接口,处理异步发送结果。

发送延迟消息

Message message = new Message("TopicTest","TagA",
                        ("Hello RocketMQ "+i).getBytes(RemotingHelper.DEFAULT_CHARSET));
message.setDelayTimeLevel(3); //延迟消息
SendResult sendResult = producer.send(message);

延迟消息只需要通过调用消息对象Message的setDelayTimeLevel()方法即可。 目前延迟消息仅支持预设的时间长度值:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
从1s到2h分别对应着等级1到18,比如setDelayTimeLevel(3)表示延迟10秒。
延迟消息指的是当Broker接收到这种消息时,延迟一段时间之后再处理。

发送单向消息

 // 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("TopicTest",
        "TagA",keys,
        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

//单向发送,不需要等待结果
producer.sendOneway(msg);

通过调用生产者Producer对象的sendOneway(msg)方法即可发送单向消息。发送单向消息之后没有结果返回,也不知道发送结果是否成功,通常适用于对发送结果要求不高的业务场景,例如发送日志消息。

消息发送到指定的MessageQueue

//将要同顺序的消息发送到同一个消息队列MessageQueue(一个Topic下面有多个MessageQueue)
SendResult sendResult = producer.send(message, (List<MessageQueue> mqs, Message msg, Object arg) -> {
    Long orderId = (Long) arg;
    long index = orderId % mqs.size();
    return mqs.get((int) index);
}, orderBean.getOrderId());

我们知道发送消息到Broker,实际上就是将消息发送到Broker上指定Topic里的某个随机的MessageQueue。一个Topic包含了多个MessageQueue,生产者可以显示指定将消息发送到指定的MessageQueue,只需要调用生产者对象的send(Message msg, MessageQueueSelector selector, Object arg)方法,自己实现MessageQueueSelector接口即可。
这种消息发送方式通常运用在需要保证消息顺序的业务场景中。例如电商系统需要保证订单流程的消息的顺序性,可以将同一个订单的相关消息发送到同一个MessageQueue,从而保证对于某个订单相关业务消息是顺序存储到Broker上的。
这里只是保证了消息发送的顺序性,而消费的顺序性是由消费者来保证的,这块内容会在下面介绍。

发送批量消息

List<Message> messages = new ArrayList<>();
messages.add(new Message("TopicTest","TagA","rocketmq1".getBytes()));
messages.add(new Message("TopicTest","TagA","rocketmq2".getBytes()));
messages.add(new Message("TopicTest","TagA","rocketmq3".getBytes()));
SendResult sendResult = producer.send(messages);

生成者可以一次性发送多个消息,从而达到批量发送消息的效果。

生产者超时消息处理

DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName-1");
producer.setNamesrvAddr("192.168.0.149:9876");
producer.setInstanceName("producer1");
producer.setSendMsgTimeout(10*1000);//@1
producer.setRetryTimesWhenSendFailed(2);//@2

代码@1:指定消息发送的超时时间。

代码@2:指定消息发送失败之后的重试次数。使用该方法可能会因为网络等原因产生重复消息的情况。

Consumer消费者

RocketMQ将消费者分为两种类型,一种是DefaultMQPushConsumer,由系统控制消息读取操作,将消息推送给消费者,这种方式使用简单,而且不需要关心消息队列的偏移量。另外一种是DefaultMQPullConsumer,由消费者自己控制消息读取操作,这种方式使用灵活,但是需要消费者自己去处理消息队列偏移量存储等问题。

DefaultMQPushConsumer

public class PushConsumer {

    public static void main(String[] args) {
        try {
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ProducerGroupName-B");//@1
            consumer.setNamesrvAddr("192.168.0.149:9876");//@2
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);//@3
            //消息模式
            consumer.setMessageModel(MessageModel.BROADCASTING);//@4
            consumer.subscribe("TopicTest","*");//@5

            consumer.registerMessageListener((List<MessageExt> msgList,ConsumeConcurrentlyContext consumeConcurrentlyContext) -> {
                msgList.forEach((messageExt)->{
                   System.out.println("msgBody: "+new String(messageExt.getBody())+",MsgId:"+messageExt.getMsgId());
                });//@6
                //由服务端维护队列的偏移
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            consumer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

代码@1:设置消费者组GroupName。

代码@2:设置NameServer地址。

代码@3:设置消费的起点,ConsumeFromWhere表示消费者从哪个位置开始消费消费:

CONSUME_FROM_LAST_OFFSET:第一次启动从队列最后位置消费,后续再启动接着上次消费的进度开始消费 。
CONSUME_FROM_FIRST_OFFSET:第一次启动从队列初始位置消费,后续再启动接着上次消费的进度开始消费 。
CONSUME_FROM_TIMESTAMP:第一次启动从指定时间点位置消费,后续再启动接着上次消费的进度开始消费 。

以上所说的第一次启动是指从来没有消费过的消费者,如果该消费者消费过,那么会在broker端记录该消费者的消费位置,如果该消费者挂了再启动,那么自动从上次消费的进度开始

代码@4:设置消息模式MessageModel。消息模式需要配合消费者组一起使用。

BROADCASTING:同一个消费者组内的每一个消费者都可以消费到所订阅Topic的全部消息。

CLUSTERING:同一个消费者组内的每一个消费者只能消费所订阅Topic的一部分消息,并且该组内的所有消费者消费的消息总和就是所订阅Topic的内容总和,这样可以达到负载均衡消费的目的。

代码@5:设置消费者订阅的主题,后面一个参数用来对消息的Tag进行过滤,consumer.subscribe("TopicTest","Tag1 || Tag2") 表示消费“TopicTest”主题下标签是“Tag1”或者“Tag2”的消息,使用null或者“*”表示消费该主题下所有消息。

代码@6:创建消息监听器,用来处理收到消息之后的业务操作。消息监听器有两种:

MessageListenerConcurrently:同步消息监听器,多线程进行消息消费,提高消费效率,但是不能保证消息被顺序消费。

consumer.registerMessageListener((List<MessageExt> msgList,
ConsumeConcurrentlyContext context) -> {
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});

MessageListenerOrderly:顺序消息监听器,一个Message Queue交个一个线程处理,保证了对于同一个Message Queue来说消息是被顺序消费的。由于消费的线程数等于Message Queue的数量,所以消费效率较低。

consumer.registerMessageListener((List<MessageExt> msgList,
ConsumeOrderlyContext context) -> {
    return ConsumeOrderlyStatus.SUCCESS;
});

DefaultMQPullConsumer

public class PullConsumer {

    /**存储MessageQueue和它对应的偏移量*/
    private static final Map<MessageQueue,Long> OFFSET_TABLE = new HashMap<>();

    private static final String GROUP_NAME = "GROUP_NAME_ONE";

    private static final String NAME_SERVER_ADDRESS = "192.168.0.149:9876";

    private static final String TOPIC_NAME = "TopicTest_pull";

    public static void main(String[] args) throws Exception{
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(GROUP_NAME);
        consumer.setNamesrvAddr(NAME_SERVER_ADDRESS);
        consumer.start();

        while (true){
            //从指定topic中拉取所有消息队列
            Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues(TOPIC_NAME);//@1

            for(MessageQueue mq:mqs){

                //获取队列下标偏移
                long offset = getMessageQueueOffset(mq);
                System.out.println("consumer from the queue:"+mq+":"+offset);
                //拉取指定消息队列的消息(阻塞式拉取,超时10秒)
                PullResult pullResult = consumer.pullBlockIfNotFound(mq,null,offset,32);//@2
                //存储队列下标偏移
                putMessageQueueOffset(mq,pullResult.getNextBeginOffset());

                switch (pullResult.getPullStatus()){//@3
                    case FOUND:
                        List<MessageExt> messageExtList = pullResult.getMsgFoundList();
                        for (MessageExt m : messageExtList) {
                            System.out.println("MQ"+mq.getQueueId()+"读取到消息:"+new String(m.getBody()));
                        }
                        break;
                    case NO_MATCHED_MSG:
                        System.out.println("MQ"+mq.getQueueId()+":NO_MATCHED_MSG");
                        break;
                    case NO_NEW_MSG:
                        System.out.println("MQ"+mq.getQueueId()+":NO_NEW_MSG");
                        break;
                    case OFFSET_ILLEGAL:
                        System.out.println("MQ"+mq.getQueueId()+":OFFSET_ILLEGAL");
                        break;
                }

            }
        }
        //consumer.shutdown();
    }

    //保存上次消费的消息下标
    private static void putMessageQueueOffset(MessageQueue mq,long nextBeginOffset) {
        OFFSET_TABLE.put(mq, nextBeginOffset);
    }

    //获取上次消费的消息的下标
    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = OFFSET_TABLE.get(mq);
        if(offset != null){
            return offset;
        }
        return 0;
    }

}

代码@1:DefaultMQPullConsumer可以主动拉取订阅主题包含的所有消息队列,用户需要自己维护这些消息队列的消费下标偏移量,特别是在出现异常情况下,否则可能会造成消息被重复消费或者消息跳过没有消费等情况。本例队列下标偏移只是简单地交给OFFSET_TABLE处理,生产环境建议将下标偏移存储到文件或者数据库。
RocketMQ使用Long类型来表示队列的下标偏移offset,随着消费者不断消费消息,offset会不断地增长。

代码@2:PullResult pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums)通过该方法消费者可以主动拉取指定从消息队列的offset下标偏移量开始最多maxNums个的消息,subExpression用来过滤Tag。

代码@3:从PullResult可以获取到拉取状态PullStatus,PullStatus包含四个值:

public enum PullStatus {
    /**
     * 拉取到消息
     */
    FOUND,
    /**
     * 没有新消息可以被拉取消费
     */
    NO_NEW_MSG,
    /**
     * 过滤结果不匹配
     */
    NO_MATCHED_MSG,
    /**
     * 非法偏移量,过大或者过小
     */
    OFFSET_ILLEGAL
}

事务消息

RocketMQ还支持对事务消息的处理,想要了解的读者朋友可以查看这篇文章:

RocketMQ支持事务消息机制:https://www.jianshu.com/p/cc5c10221aa1

关注公众号了解更多原创博文

Alt

发布了122 篇原创文章 · 获赞 127 · 访问量 93万+

猜你喜欢

转载自blog.csdn.net/u010739551/article/details/104496451
今日推荐