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"的死信队列

猜你喜欢

转载自blog.csdn.net/m0_53121042/article/details/112344326