RocketMQ 入门学习笔记

前言

本文主要侧重点在 java 的客户端,具体的如何搭建 RocketMQ 服务端不在本文讨论范围内

一、一些概念

1.1、rocketMQ 的组成

在这里插入图片描述

  • name server 用来保存 Broker 相关 Topic 等元信息并给 Producer ,提供 Consumer 查找 Broker 信息。
  • producer 消息生产者,负责产生消息
  • consumer 消息消费者,负责消费消息
    • push consumer:Consumer的一种,应用通常向Consumer对象注册一个Listener接口,一旦收到消息,Consumer对象立刻回调Listener接口方法
    • pull consumer :Consumer的一种,应用通常主动调用Consumer的拉消息方法从Broker拉消息,主动权由应用控制
  • Producer Group:多个 producer 可以同属于一个 Producer Group
  • Consumer Group:多个consumer 可以同属于一个 Consumer Group,同个分组中的消费者一定要订阅相同的topic
  • broker:消息中转角色,负责存储消息

1.2、消费方式

广播消费和集群消费

  • 广播消费:一条消息被多个Consumer消费,即使这些 Consumer属于同一个 Consumer Group,消息也会被Consumer Group 中的每个 Consumer都消费一次。
  • 集群消费:一个Consumer Group中的Consumer实例平均分摊消费消息。例如某个Topic有9条消息,其中一个Consumer Group有3个 实例(可能是3个进程或3台机器),那么每个实例只消费其中的3条消息。

1.3、消费者发送消息的方式

  • 同步可靠:客户端阻塞,等待服务端响应结果,成功后,客户端继续运行
  • 异步可靠:客户端不阻塞,设置一个钩子,待服务端响应成功后,调用钩子
  • 单向:不关心服务端的响应结果,直接发给服务端就完事了

1.4、消息类型

  • 普通消息
  • 顺序消息
  • 延时消息
  • 批量消息

二、四种消息

2.1、普通消息的发送和消费

public class Test {

    public static String PRODUCER_GROUP_NAME = "producer_group_1";
    public static String CONSUMER_GROUP_NAME = "consumer_group_1";
    public static String NAME_SERVER_HOST = "127.0.0.1:9876";
    public static String TOPIC_NAME = "TEST_TOPIC";

    public static void main(String[] args) throws Exception {
        // 创建生产者实例
        DefaultMQProducer producer = new DefaultMQProducer();
        // 设置name server 信息
        producer.setNamesrvAddr(NAME_SERVER_HOST);
        // 设置 producer group
        producer.setProducerGroup(PRODUCER_GROUP_NAME);

        // 启动生产者实例
        producer.start();

        // 启动两个消费者
        openConsumer("xfz1");
        openConsumer("xfz2");
        // 循环发送消息
        for (int i = 1; i < 40; i++) {
            Message msg = new Message();
            msg.setTopic(TOPIC_NAME);
            msg.setTags("tag1");
            msg.setBody(("消息:" + i).getBytes());

            // 使用生产者实例发送消息
            producer.send(msg);
        }

        // 销毁生产者
        producer.shutdown();

    }

    public static void openConsumer(String name) {
        // 开启一个消费者线程
        new Thread(() -> {
            // 创建消费者实例
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
            // 设置name server
            consumer.setNamesrvAddr(NAME_SERVER_HOST);
            // 设置消费者组
            consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
            // 设置消费者name
            consumer.setInstanceName(name);
            // 订阅 topic
            try {
                consumer.subscribe(TOPIC_NAME, "tag1");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
            // 设置钩子
            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                for(MessageExt msg :msgs){
                    System.out.println("消费者:"+ consumer.getInstanceName()+" 消费了消息:"+new String(msg.getBody()));
                }
                // 睡眠500ms
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            // 启动consumer
            try {
                consumer.start();
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

注意点:

  • DefaultMQProducer、DefaultMQPushConsumer 都继承了 ClientConfig ,就是说,这两个类可以直接使用和设置 rocketMQ 的客户端配置,在上面的例子中,我只设置了name server 的host
  • 发送消息时需要设置消息的 topic、tag、body
  • DefaultMQPushConsumer 在有消息时,会调用我们设置好的钩子,即 MessageListenerConcurrently 的 consumeMessage方法
  • Consumer 要设置实例名称

程序结果:
在这里插入图片描述

2.2、有序消息

2.2.1、rocketMQ 是如何做到有序的?

要了解这个问题,需要明白

  • producer 和 message、 broker、queue 的关系
  • consumer 和 message、broker、queue 的关系

producer 和 message、 broker、queue 的关系

broker 是消息 queue 的提供者,而 broker 支持分布式,即集群部署。当一个生产者实例发出了一条消息时,需要先选择一个 broker 的一个 queue。
在这里插入图片描述

consumer 和 message、broker、queue 的关系

而消费时,多个queue是同时拉取提交到消费者。
在这里插入图片描述

rocketMQ 如何做到有序

我们知道,同一个 queue 是可以做到 FIFO 的,如果我们想办法把所有消息投递到同一个 broker 的同一个 queue,自然消息就变成有序的了。但是,只是使用同一个 broker 的同一个 queue 就可以了吗?并不是,因为同一个 queue 的消息有可能被多个消费者的消费,而每个消费者又有可能有多个线程在消费,为了保持 queue 中的消息顺序消费,我们还需要保证同一时刻,只有一个消费者的一个消费线程在消费 queue。

消息置入同一个 queue 方案:rocketMQ 支持在生产者投递消息时,设置队列的选择算法,即含有 MessageQueueSelector 的 send 方法。

保证同一时刻,只有一个消费者的一个消费线程在消费:使用 DefaultMQPushConsumer 的 MessageListenerOrderly 这个监听器处理消息。

2.2.2、全局有序

所有消息全部进入同一个队列,消费时,只有一个消费者的一个线程可以消费这个队列的消息
在这里插入图片描述

生产者设置 MessageQueueSelector,选择 queue

                // 使用生产者实例发送消息
                try {
//                    producer.send(msg);
                    producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            return mqs.get(0);
                        }
                    },null);
                } catch (Exception e) {
                    e.printStackTrace();
                }

这里,select 返回的 queue,即为消息即将投递的 queue,我们直接把第一个 queue 返回,相当于所有的消息直接投递到第一个 queue

消费者顺序消费

            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                for (MessageExt msg : msgs) {
                    System.out.println("消费者:" + consumer.getInstanceName() + " 消费了消息:" + new String(msg.getBody()) + " 消费线程:" + Thread.currentThread().getName()+" 消息所在队列id:"+msg.getQueueId());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });

全局有序demo代码

public class Test3 {

    public static String PRODUCER_GROUP_NAME = "producer_group_1";
    public static String CONSUMER_GROUP_NAME = "consumer_group_1";
    public static String NAME_SERVER_HOST = "127.0.0.1:9876";
    public static String TOPIC_NAME = "TEST_TOPIC";

    public static void main(String[] args) throws Exception {

        // 启动生产者
        createProducer("prd1");

        Thread.sleep(5000);
        // 启动两个消费者
        createConsumer("xfz1");
        createConsumer("xfz2");
        createConsumer("xfz3");

    }

    public static void createConsumer(String name) {
        // 开启一个消费者线程
        new Thread(() -> {
            // 创建消费者实例
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
            // 设置name server
            consumer.setNamesrvAddr(NAME_SERVER_HOST);
            // 设置消费者组
            consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
            // 设置消费者name
            consumer.setInstanceName(name);

            consumer.setConsumeThreadMax(1);
            consumer.setConsumeThreadMin(1);
            // 订阅 topic
            try {
                consumer.subscribe(TOPIC_NAME, "tag1");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
            // 设置钩子
            consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
                for (MessageExt msg : msgs) {
                    System.out.println("消费者:" + consumer.getInstanceName() + " 消费了消息:" + new String(msg.getBody()) + " 消费线程:" + Thread.currentThread().getName()+" 消息所在队列id:"+msg.getQueueId());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            // 启动consumer
            try {
                consumer.start();
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();
    }

    public static void createProducer(String name) {
        new Thread(() -> {
            // 创建生产者实例
            DefaultMQProducer producer = new DefaultMQProducer();
            // 设置name server 信息
            producer.setNamesrvAddr(NAME_SERVER_HOST);
            // 设置 producer group
            producer.setProducerGroup(PRODUCER_GROUP_NAME);
            // 设置生产者名称
            producer.setInstanceName(name);

            // 启动生产者实例
            try {
                producer.start();
            } catch (MQClientException e) {
                e.printStackTrace();
            }
            // 循环发送消息
            for (int i = 1; i < 10000; i++) {
                Message msg = new Message();
                msg.setTopic(TOPIC_NAME);
                msg.setTags("tag1");
                msg.setBody(("消息" + i).getBytes());
                // 使用生产者实例发送消息
                try {
//                    producer.send(msg);
                    producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                            if (((Integer)arg & 1) == 1) {
                                // 奇数
                                return mqs.get(0);
                            }else {
                                // 偶数
                                return mqs.get(1);
                            }

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

        }).start();
    }
}

2.2.3、局部有序

全局有序是局部有序的一种特殊情况,全局有序是所有的消息全部进入同一个 queue,这样所有的消息都会有序;而局部有序是某个场景下的一部分消息(比如id是偶数的消息,id小于1000的消息等等场景)进入同一个 queue,这样就可以保证在同一个场景下(同一个 queue)中,消息是有序的。
下面写一个demo,将 id 是奇数的消息和 id 是偶数的消息分别放入 queue,并消费

生产者设置 MessageQueueSelector,选择 queue

奇数放入第一个 queue,偶数放入第二个 queue。

                    producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                            if ((num & 1) == 1) {
                                // 奇数
                                return mqs.get(0);
                            }else {
                                // 偶数
                                return mqs.get(1);
                            }

                        }
                    }, null);

2.3、延时消息

RocketMQ 支持发送延迟消息,但不支持任意时间的延迟消息的设置,仅支持内置预设值的延迟时间间隔的延迟消息。

预设值的延迟时间间隔为:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h

message 设置延时

msg.setDelayTimeLevel(3);

2.4、批量消息

批量发送消息可提高传递小消息的性能。同时也需要满足以下特征

  • 批量消息要求必要具有同一topic、相同消息配置
  • 不支持延时消息
  • 建议一个批量消息最好不要超过1MB大小

代码:

       List<Message> msgs = new ArrayList<>();
        for (int i = 1; i < 40; i++) {
            Message msg = new Message();
            msg.setTopic(TOPIC_NAME);
            msg.setTags("tag1");
            msg.setBody(("消息:" + i).getBytes());
            msgs.add(msg);

        }
        // 使用生产者实例发送消息
        producer.send(msgs);

三、最佳实践

3.1、消息发送最佳实践

3.1.1、一个应用尽可能用一个Topic

一个应用尽可能用一个Topic,消息子类型用tags来标识,tags可以由应用自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用tags在broker做消息过滤。

3.1.2、打印合适的消息日志

消息发送成功或者失败,要打印消息日志,务必要打印sendresult和key字段。

3.1.3、发送成功的几个状态

send消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在sendResult里定义。

  • SEND_OK
    消息发送成功
  • FLUSH_DISK_TIMEOUT
    消息发送成功,但是服务器刷盘超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失
  • FLUSH_SLAVE_TIMEOUT
    消息发送成功,但是服务器同步到Slave时超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失
  • SLAVE_NOT_AVAILABLE
    消息发送成功,但是此时slave不可用,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失

3.1.4、消息发送的重试

3.1.4.1、只有同步发送才会进行失败重试,重新投递消息

rocketMQ 有3种类型的发送

  • 同步
    投递消息的线程阻塞,等待获取投递消息的结果,对应 send(Message msg)
  • 异步
    投递消息的线程不阻塞,等投递结果返回时,调用设置好的钩子,对应 send(Message msg, SendCallback sendCallback)
  • oneway
    投递消息的线程不阻塞,不关心投递结果,对应 sendOneway(Message msg)

只有同步发送时,才会在消息投递失败时,进行重试

3.2、消息消费最佳实践

3.2.1、一个 consumer group 只订阅一个 topic

rocketMQ 文档中明确指出,强烈建议一个 consumer group 只订阅一个 topic。

但是,能不能多个 consumer group 订阅同一个 topic 呢?答案是可以的。
多个 consumer group 订阅同一个 topic 时,Topic的一条消息会广播给所有订阅的ConsumerGroup,就是每个ConsumerGroup都会收,但是在一个ConsumerGroup内部给个Consumer是负载消费消息的,翻译一下就是:一条消息在一个group内只会被一个Consumer消费

3.2.2、消费端需要做到幂等

rocketmq 无法避免消息的重复消费,消费端需要做幂等处理。处理方式:

  • 1、增加唯一性约束,如果出现重复插入,则直接报错,不进行处理即可
  • 2、加锁(悲观锁或者乐观锁)
    比如,如果希望更新一个字段,这个时候可以加悲观锁:
    直接在jvm 层面加锁或者加分布式锁控制,或者在mysql 做select for update
    也可以加乐观锁:使用MVCC
发布了52 篇原创文章 · 获赞 7 · 访问量 2万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章