• 2-rocketmq-消息发送和接收


    quick start

    添加依赖

    <dependency>
      <groupId>org.apache.rocketmq</groupId>
      <artifactId>rocketmq-client</artifactId>
      <version>4.7.1</version>
    </dependency>
    

    生产者

    public class Producer {
        public static void main(String[] args) throws MQClientException, InterruptedException {
            /**
             * 生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组rocketmq支持事务消息,在发送事务消息时,如果事务消息异常(producer挂了),broker端会来回查事务的状态,这个时候会根据group名称来查找对应的producer来执行相应的回查逻辑。相当于实现了producer的高可用
             */
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            // namesrv地址 多个地址用 ; 隔开  从namesrv上拉取broker信息
            producer.setNamesrvAddr("localhost:9876");
            producer.start();
            for (int i = 0; i < 1000; i++) {
                try {
                  	/**
                  	 * 创建消息实例,指定topic,tag,消息内容。tag
                  	 */
                    Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                    );
                    // 发送消息并获取发送结果   同步发送
                    SendResult sendResult = producer.send(msg);
                    System.out.printf("%s%n", sendResult);
                } catch (Exception e) {
                    e.printStackTrace();
                    Thread.sleep(1000);
                }
            }
            producer.shutdown();
        }
    }
    

    SendResult中,有一个sendStatus状态,表示消息的发送状态。一共有四种状态

    1. FLUSH_DISK_TIMEOUT : 表示没有在规定时间内完成刷盘(需要Broker 的刷盘策Ill创立设置成
      SYNC_FLUSH 才会报这个错误) 。
    2. FLUSH_SLAVE_TIMEOUT :表示在主备方式下,并且Broker 被设置成SYNC_MASTER 方式,没有
      在设定时间内完成主从同步。
    3. SLAVE_NOT_AVAILABLE : 这个状态产生的场景和FLUSH_SLAVE_TIMEOUT 类似, 表示在主备方
      式下,并且Broker 被设置成SYNC_MASTER ,但是没有找到被配置成Slave 的Broker 。
    4. SEND OK :表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被
      同步到了Slave 上?消息在Slave 上是否被写入磁盘?需要结合所配置的刷盘策略、主从策略来
      定。这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是SEND OK

    消费者

    public class Consumer {
        public static void main(String[] args) throws InterruptedException, MQClientException {
            //groupName 将多个consumer分组,提高并发处理能力。需要和MessageModel配合
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
            // 多个地址 ;分开 获取broker地址 并定时向broker发送心跳 可以从master/slave获取订阅
            consumer.setNamesrvAddr("localhost:9876");
            // 两种消息模式  BROADCASTING   CLUSTERING
            consumer.setMessageModel(MessageModel.BROADCASTING);
            //设置consumer第一次启动从队列头部还是尾部开始消费
          	//如果非第一次启动,那么按上一次消费的位置继续消费(取决于本地的offeset数据)
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
            // topic 可通过tag过滤消息  * 或 null 代表全部
            consumer.subscribe("TopicTest", "*");
            /**注册消息处理回调
             * MessageListenerConcurrently 普通监听
             * MessageListenerOrderly 顺序监听
             */
            consumer.registerMessageListener(new MessageListenerConcurrently() {
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                    // todo 消息处理逻辑
                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                  	// 返回消费状态
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            // 启动consumer
            consumer.start();
            System.out.printf("Consumer Started.%n");
        }
    }
    

    消费状态

    ConsumeConcurrentlyStatus {
      	// 消费成功
        CONSUME_SUCCESS,
      // 使用失败,稍后尝试使用
        RECONSUME_LATER;
    

    消息发送及消费的基本原理

    集群部署,一个master可以有多个slave,一个slave只能有一个master.consumer可以从master获者slave中订阅消息

    2m-2s示例:

    image-20200709175842041

    rocketMQ 没有实现master选举(通过配置文件来指定主从)

    当master挂了后 消费者依然能正常消费消息(slave提供读服务)

    通过groupName实现分区,提高消费者的处理能力

    消费者

    两种消费者类型

    • DefaultMQPushConsumer 由系统控制读取操作

    DefaultMQPushConsumer

    自动保存offset,自动做负载均衡

    public class Consumer {
        public static void main(String[] args) throws InterruptedException, MQClientException {
            //groupName 将多个consumer分组,提高并发处理能力。需要和MessageModel配合
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
            // 多个地址 ;分开
            consumer.setNamesrvAddr("localhost:9876");
            // 两种消息模式  BROADCASTING   CLUSTERING
            consumer.setMessageModel(MessageModel.BROADCASTING);
            //第一次启动从 offset头开始
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
            // topic 可通过tag过滤消息  * 或 null 代表全部
            consumer.subscribe("TopicTest", "*");
            //注册消息处理回调
            consumer.registerMessageListener(new MessageListenerConcurrently() {
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                    // todo 消息处理逻辑
                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            // 启动consumer
            consumer.start();
            System.out.printf("Consumer Started.%n");
        }
    }
    

    两种消息模式 BROADCASTING CLUSTERING:

    1、在 Clustering 模式下,同一个 ConsumerGroup ( GroupName 相同 ) 里的每个 Consumer 只消费所订阅消息 的一部分 内 容, 同一个 ConsumerGroup里所有的 Consumer 消 费 的内 容合起来才是所订阅 Topic 内 容 的 整体 ,从而达到负载均衡的目的 (也就是集群消费)

    2、在 Broadcasting 模式下,同一个 ConsumerGroup 里的每个 Consumer 都能消费到所订阅 Topic 的全部消息,也就是一个消息会被多次分发,被多个Consumer 消费 。(也就是广播模式

    image-20200709181438549

    通过长轮询的方式获取消息

    Broker端HOLD住客户端过来的请求一小段时间,在这个时间内有新消息到达就利用现有的连接立刻返回消息给Consumer。主动权在Consumer

    好处是客户端能充分利用资源,不至于处理不过来

    流量控制

    DefaultMQPullConsumer

    需要自己维护offset,需要通过遍历MessageQueue获取消息

     public class PullConsumer {
        private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();
    
        public static void main(String[] args) throws MQClientException {
            DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
            consumer.setNamesrvAddr("127.0.0.1:9876");
            consumer.start();
            // 获取分片
            Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("broker-a");
            for (MessageQueue mq : mqs) {
                System.out.printf("Consume from the queue: %s%n", mq);
                SINGLE_MQ:
                while (true) {
                    try {
                        PullResult pullResult =
                            consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                        System.out.printf("%s%n", pullResult);
                        putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                        switch (pullResult.getPullStatus()) {
                            // 获取到消息
                            case FOUND:
                                break;
                            case NO_MATCHED_MSG:
                                break;
                            // 没有新消息
                            case NO_NEW_MSG:
                                break SINGLE_MQ;
                            case OFFSET_ILLEGAL:
                                break;
                            default:
                                break;
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    
            consumer.shutdown();
        }
    
        private static long getMessageQueueOffset(MessageQueue mq) {
            Long offset = OFFSE_TABLE.get(mq);
            if (offset != null)
                return offset;
    
            return 0;
        }
    
        private static void putMessageQueueOffset(MessageQueue mq, long offset) {
            OFFSE_TABLE.put(mq, offset);
        }
    
    }
    

    Consumer的启动、关闭

    DefaultMQPushConsumer启动时不会检查nameServer地址的正确或者可用性

    // 从指定topic中拉取所有消息队列
    Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("order-topic");
    

    可以通过上面的方法主动拉取消息队列来判断nameServer的可用性

    关闭时调用shutdown()即可

    DefaultMQPullConsumer关闭或者异常退出时需要将offset保存起来

    才能保证下次启动时拉取消息的正确性

    consumerGroup:位于同一个consumerGroup中的consumer实例和producerGroup中的各个produer实例承担的角色类似;同一个group中可以配置多个consumer,可以提高消费端的并发消费能力以及容灾
    和kafka一样,多个consumer会对消息做负载均衡,意味着同一个topic下的不messageQueue会分发给同一个group中的不同consumer

    消费端的负载均衡

    和kafka一样,消费端也会针对Message Queue做负载均衡,使得每个消费者能够合理的消费多个分区的消息。

    消费端会通过RebalanceService线程,10秒钟做一次基于topic下的所有队列负载
    • 消费端遍历自己的所有topic,依次调rebalanceByTopic

    • 根据topic获取此topic下的所有queue

    • 选择一台broker获取基于group的所有消费端(有心跳向所有broker注册客户端信息)

    • 选择队列分配策略实例AllocateMessageQueueStrategy执行分配算法

    什么时候触发负载均衡
    • 消费者启动之后
    • 消费者数量发生变更
    • 每10秒会触发检查一次rebalance
    分配算法

    RocketMQ提供了6中分区的分配算法

    • (AllocateMessageQueueAveragely)平均分配算法(默认)
    • (AllocateMessageQueueAveragelyByCircle)环状分配消息队列
    • (AllocateMessageQueueByConfig)按照配置来分配队列: 根据用户指定的配置来进行负载
    • (AllocateMessageQueueByMachineRoom)按照指定机房来配置队列
    • (AllocateMachineRoomNearby)按照就近机房来配置队列:
    • (AllocateMessageQueueConsistentHash)一致性hash,根据消费者的cid进行

    生产者

    DefaultMQProducer 默认生产者

    public class Producer {
        public static void main(String[] args) throws MQClientException, InterruptedException {
            // producerGroupName
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            // namesrv地址 多个地址用 ; 隔开
            producer.setNamesrvAddr("localhost:9876");
            producer.start();
            for (int i = 0; i < 1000; i++) {
                try {
                    Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                    );
                    // 返回 
                    SendResult sendResult = producer.send(msg);
                    System.out.printf("%s%n", sendResult);
                } catch (Exception e) {
                    e.printStackTrace();
                    Thread.sleep(1000);
                }
            }
            producer.shutdown();
        }
    }
    
    

    消息返回状态:SendResult.sendStatus

    1、FLUSH DISK TIMEOUT : 表示没有在规定时间内完成刷盘(需要Broker 的刷盘策略设置成 SYNC FLUSH 才会报这个错误) 。
    2、 FLUSH SLAVE TIMEOUT :表示在主备方式下,并且 Broker 被设置成 SYNC MASTER 方式,没有在设定时间内完成主从同步 。
    3、SLAVE NOT AVAILABLE : 这个状态产生的场景和 FLUSH SLAVETIMEOUT 类似, 表示在主备 方式下,并且 Broker 被设置成 SYNCMASTER ,但是没有找到被配置成 S lave 的 Broker 。
    4、SEND OK :表示发送成功,发送成功的具体含义,比如消息是否已经被存储到融盘?消息是否被同步到了 S lave 上?消息在 S lave 上是否被写人磁盘?需要结合所配置的刷盘策略、主从策略来定 。 这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是 SEND OK

    延迟消息

    通过Message.setDelayTimeLevel ( int level ) 方法设置延迟时间,只支持预设值(1s/5s/1Os/30s/Im/2m/3m/4m/5m/6m/7m/8m/9m/1 Om/20m/30m/1 h/2h )。 比如setDelayTimeLevel(3)表示延迟 10s 。

    自定义消息发送规则

    实现MessageQueueSelector接口
    三种默认实现:
    SelectMessageQueueByHash
    SelectMessageQueueByMachineRoom
    SelectMessageQueueByRandom

    自定义消息发送可以将消息发送到指定的MessageQueue里

    对事物的支持

    new TransactionMQProducer("groupName");

    设置生产者group,当一个producer挂掉了,消息会分发到其它producer保证消息一定会被回查确定

    消息的可靠性原则

    只有消费者返回CONSUME_SUCCESS消费成功的才会认为消费成功

    返回ConsumeConcurrentlyStatus.RECONSUME_LATER消费失败会被重试

    消息衰减重试

    为了保证消息肯定至少被消费一次,RocketMQ会把这批消息重新发回到broker,在延迟的某个时间点
    (默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。而如果一直这样重复消费都持续
    失败到一定次数(默认16次),就会投递到DLQ死信队列。应用可以监控死信队列来做人工干预
    可以修改broker-a.conf文件
    messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

    重试消息的处理机制

    一般情况下我们在实际生产中是不需要重试16次,这样既浪费时间又浪费性能,理论上当尝试重复次数
    达到我们想要的结果时如果还是消费失败,那么我们需要将对应的消息进行记录,并且结束重复尝试

    consumer.registerMessageListener((MessageListenerConcurrently) (list,
      consumeOrderlyContext) -> {
                    for (MessageExt messageExt : list) {
                        if(messageExt.getReconsumeTimes()==3) {
                         //可以将对应的数据保存到数据库,以便人工干预	
                           System.out.println(messageExt.getMsgId()+","+messageExt.getBody());
                            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                        }
                    } r
                    eturn ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                });
    

    死信队列

    RocketMQ会为每个消费组都设置一个Topic命名为“%DLQ%+consumerGroup"的死信队列

  • 相关阅读:
    Archlinux笔记本安装手记
    linux下activemq安装与配置activemq-5.15.2
    在 CentOS7 上安装 Zookeeper-3.4.9 服务
    VMware虚拟化kvm安装部署总结
    打印机故障总结
    fluentd安装和配置,收集docker日志
    使用Python和AWK两种方式实现文本处理的长拼接案例
    MySQL数据库使用xtrabackup备份实现小例子
    shell脚本实现ftp上传下载文件
    Linux系统中创建大文件,并作为文件系统使用
  • 原文地址:https://www.cnblogs.com/zh-ch/p/14248757.html
Copyright © 2020-2023  润新知