一文搞懂RocketMq

基本概念

下面是一张 RocketMQ 的部署结构图,里面涉及了 RocketMQ 核心的四大组件:Name Server、Broker、Producer、Consumer ,每个组件都可以部署成集群模式进行水平扩展。

 

  • 生产者

生产者(Producer)负责产生消息,生产者向消息服务器发送由业务应用程序系统生成的消息。 RocketMQ 提供了三种方式发送消息:同步、异步、单向

  • 同步发送

同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。

  • 异步发送

异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务

  • 单向发送

单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集

  • 生产者组

生产者组(Producer Group)是一类 Producer 的集合,这类 Producer 通常发送一类消息并且发送逻辑一致,所以将这些 Producer 分组在一起。从部署结构上看生产者通过 Producer Group 的名字来标记自己是一个集群。

  • 消费者

消费者(Consumer)负责消费消息,消费者从消息服务器拉取信息并将其输入用户应用程序。站在用户应用的角度消费者有两种类型:拉取型消费者、推送型消费者。

  • 拉取型消费者

拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。

  • 推送型消费者

推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。

  • 消费者组

消费者组(Consumer Group)一类 Consumer 的集合名称,这类 Consumer 通常消费同一类消息并且消费逻辑一致,所以将这些 Consumer 分组在一起。消费者组与生产者组类似,都是将相同角色的分组在一起并命名,分组是个很精妙的概念设计,RocketMQ 正是通过这种分组机制,实现了天然的消息负载均衡。消费消息时通过 Consumer Group 实现了将消息分发到多个消费者服务器实例,比如某个 Topic 有9条消息,其中一个 Consumer Group 有3个实例(3个进程或3台机器),那么每个实例将均摊3条消息,这也意味着我们可以很方便的通过加机器来实现水平扩展。

  • 消息服务器

消息服务器(Broker)是消息存储中心,主要作用是接收来自 Producer 的消息并存储, Consumer 从这里取得消息。它还存储与消息相关的元数据,包括用户组、消费进度偏移量、队列信息等。

  • 名称服务器

名称服务器(NameServer)用来保存 Broker 相关元信息并给 Producer 和 Consumer 查找 Broker 信息。NameServer 被设计成几乎无状态的,可以横向扩展,节点之间相互之间无通信,通过部署多台机器来标记自己是一个伪集群。每个 Broker 在启动的时候会到 NameServer 注册,Producer 在发送消息前会根据 Topic 到 NameServer 获取到 Broker 的路由信息,Consumer 也会定时获取 Topic 的路由信息。

  • 消息

消息(Message)就是要传输的信息。一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 key 并在 Broker 上查找此消息以便在开发期间查找问题。

  • 主题

主题(Topic)可以看做消息的规类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。一个 Topic 也可以被 0个、1个、多个消费者订阅。

  • 标签

标签(Tag)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。标签有助于保持您的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助

  • 消息队列

消息队列(Message Queue),主题被划分为一个或多个子主题,即消息队列。一个 Topic 下可以设置多个消息队列,发送消息时执行该消息的 Topic ,RocketMQ 会轮询该 Topic 下的所有队列将消息发出去。下图 Broker 内部消息情况:

 

  • 消息消费者模式

消息消费模式有两种:集群消费(Clustering)和广播消费(Broadcasting)。默认情况下就是集群消费,该模式下一个消费者集群共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。而广播消费消息会发给消费者组中的每一个消费者进行消费。

  • 消息顺序

消息顺序(Message Order)有两种:顺序消费(Orderly)和并行消费(Concurrently)。顺序消费表示消息消费的顺序同生产者为每个消息队列发送的顺序一致,所以如果正在处理全局顺序是强制性的场景,需要确保使用的主题只有一个消息队列。并行消费不再保证消息顺序,消费的最大并行数量受每个消费者客户端指定的线程池限制

  • 消息分类

延时消息、事务消息、顺序消息

  • 延时消息

Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

  • 事务消息

消息队列 RocketMQ 提供类似 X/Open XA 的分布事务功能,通过消息队列 RocketMQ 的事务消息能达到分布式事务的最终一致。

  • 顺序消息

消息投递至同一个消息队列中实现顺序消费

  • 消息过滤

Consumer 可以根据消息标签(Tag)对消息进行过滤,确保 Consumer 最终只接收被过滤后的消息类型。消息过滤在消息队列 RocketMQ 的服务端完成。

  • 死信队列

死信队列用于处理无法被正常消费的消息,即死信消息。

  • 消息重试

可能存在各种异常情况,导致消息无法最终被Consumer消费掉,因此就有了消息失败重试机制,消息重试分为2种:Producer端重试和Consumer端重试。

  • Producer端重试

生产者端的消息失败,也就是Producer往MQ上发消息没有发送成功,比如网络抖动导致生产者发送消息到MQ失败。

  • Consumer端重试

消费者端的失败,分为2种情况,

一个是exception(消息正常的到了消费者,结果消费者发生异常,处理失败了。例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。),

一个是timeout(比如由于网络原因导致消息压根就没有从MQ到消费者上,那么在RocketMQ内部会不断的尝试发送这条消息,直至发送成功为止!(比如集群中一个broker失败,就尝试另一个broker)延续Exception的思路,也就是消费端没有给RocketMQ返回消费的状态,即没有return ConsumeConcurrentlyStatus.CONSUME_SUCCESS或return ConsumeConcurrentlyStatus.RECONSUME_LATER,这样的就认为没有到达Consumer端

  • 刷盘方式

分为同步刷盘和异步刷盘

  • 同步刷盘

在消息到达MQ后,RocketMQ需要将数据持久化,同步刷盘是指数据到达内存之后,必须刷到commitlog日志之后才算成功,然后返回producer数据已经发送成功。

  • 异步刷盘

同步刷盘是指数据到达内存之后,返回producer说数据已经发送成功。,然后再写入commitlog日志。

  • commitlog

commitlog就是来存储所有的元信息,包含消息体,类似于Mysql、Oracle的redolog,所以主要有CommitLog在,Consume Queue即使数据丢失,仍然可以恢复出来。

  •  consumequeue

索引文件,存储数据指向到物理文件中的配置。以便Consume快速通过consumequeue找到commitlog中的数据

创建队列

DefaultMQProducer producer = new DefaultMQProducer("dada");

  // 设置nameServer的地址

  producer.setNamesrvAddr("47.112.12.45:9876");

  // 启动生产者

  producer.start();

  // broker名称  topic 名称  默认创建的消息队列的数量 目前创建8个

  producer.createTopic("dada_im","dada-topic",4);

  System.out.println("topic 创建成功");

  producer.shutdown();

发送消息的三种方式

  • 同步

/**同步发送消息*/

  private static void sysnc() throws Exception {

    /*设置生产者组*/

    DefaultMQProducer producer = new DefaultMQProducer("dada");

    /*设置NameServer名称服务器*/

    producer.setNamesrvAddr("47.112.12.45:9876");

    producer.start();

    /*设置消息*/

    Message message = new Message("dada-topic", "mytag", "同步发送消息".getBytes("UTF-8"));

    /*发送消息*/

    SendResult result = producer.send(message);

    /*打印发送结果*/

    System.out.println(result);

    producer.shutdown();

  }
  • 异步

/**异步发送消息*/

  private static void async() throws Exception {

    /*设置生产者组*/

    DefaultMQProducer producer = new DefaultMQProducer("dada");

    /*设置NameServer名称服务器*/

    producer.setNamesrvAddr("47.112.12.45:9876");

    producer.start();

    /*设置消息*/

    Message message = new Message("dada-topic", "异步发送消息".getBytes("UTF-8"));

    /*发送消息*/

    producer.send(message, new SendCallback() {

        /**成功之后的回调*/

        @Override

        public void onSuccess(SendResult sendResult) {

            System.out.println("发送成功了!" + sendResult);

        }

  

        /**失败之后的回调*/

        @Override

        public void onException(Throwable throwable) {

            System.out.println("发送失败了!" + throwable);

        }

    });

  }
  • 单向
/**单向发送消息*/

  private static void unidirectional() throws Exception {

    /*设置生产者组*/

    DefaultMQProducer producer = new DefaultMQProducer("dada");

    /*设置NameServer名称服务器*/

    producer.setNamesrvAddr("47.112.12.45:9876");

    producer.start();

    /*设置消息*/

    Message message = new Message("dada-topic", "mytag", "单向发送消息".getBytes("UTF-8"));

    /*发送消息*/

    producer.sendOneway(message);

    producer.shutdown();

  }


两种形式的消费者

  • Push

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dadaConsumer");

  consumer.setNamesrvAddr("47.112.12.45:9876");

  consumer.subscribe("dada_topic","*");

  // 有序的监听消息

  consumer.registerMessageListener((MessageListenerOrderly) (list, consumeOrderlyContext) -> {

    list.forEach(x->{

        try {

            System.out.println(Thread.currentThread().getName() + " "+x.getQueueId()+" "+new String( x.getBody(),"UTF-8"));

        } catch (UnsupportedEncodingException e) {

            e.printStackTrace();

        }

    });

   return ConsumeOrderlyStatus.SUCCESS;

  });

  // 启动消费者

  consumer.start();

 

  • Pull据说没有人用

四种消息过滤

  • 订阅全部tag
consumer.subscribe("dada-topic", "*");
  • 订阅tag为add的
consumer.subscribe("dada-topic", "add");
  • 订阅tag为add或者update的
consumer.subscribe("dada-topic", "add || update");
  • 生产者:设置自定义属性
private static void tagFilter() throws Exception {

    DefaultMQProducer producer = new DefaultMQProducer("dada");

    producer.setNamesrvAddr("47.112.12.45:9876");

    producer.start();

    String msg="这是一个用户的消息";

    Message message = new Message("dada-topic-filter","mytag",msg.getBytes("UTF-8"));

    // 设置属性 用于过滤消息

    message.putUserProperty("sex","女");

    message.putUserProperty("age","18");

    // 发送消息

    SendResult result = producer.send(message);

    System.out.println("消息发送结果"+result);

    producer.shutdown();

  }

 

消费者:

private static void firterCustomer() throws MQClientException {

    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dada-consumer");

    consumer.setNamesrvAddr("47.112.12.45:9876");

      consumer.subscribe("dada-topic-filter", MessageSelector.bySql("sex='女' AND age>=18"));

    // 并发消息 和顺序消息 这里面用的并发消息

    consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {

        list.forEach(x -> {

            try {

                System.out.println("消息" + new String(x.getBody(), "UTF-8"));

            } catch (UnsupportedEncodingException e) {

                e.printStackTrace();

            }

        });

        // 返回给服务器一个状态值 成功或者失败

        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

    });

    // 启动消费者

    consumer.start();

  }


四种模式的消费者

一、并行消费

private static void firterCustomer() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dada-consumer");
    consumer.setNamesrvAddr("47.112.12.45:9876");
    consumer.subscribe("dada-topic-filter", MessageSelector.bySql("sex='女' AND age>=18"));
    // 并发消息 和顺序消息 这里面用的并发消息
    consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
        list.forEach(x -> {
            try {
                System.out.println("消息" + new String(x.getBody(), "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        });
        // 返回给服务器一个状态值 成功或者失败
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    });
    // 启动消费者
    consumer.start();
}


二、顺序消费

生产者

private static void producer() throws MQClientException, UnsupportedEncodingException, RemotingException, MQBrokerException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("dada");
    producer.setNamesrvAddr("47.112.12.45:9876");
    producer.start();
    for (int i = 0; i < 100; i++) {
        int orderId=i%10;             // 模拟生成订单id
        String msgStr="order-->"+i+", id = "+ orderId ;
        Message message = new Message("dada_topic","ORDER_MSG", msgStr.getBytes(RemotingHelper.DEFAULT_CHARSET));
        /**
         *  第一个参数  消息
         *  第二个参数  选择我们的消息队列
         *  第三个参数  args会传入到我们的第二个参数的select方法的args里面去
         */
        SendResult send = producer.send(message, new MessageQueueSelector() {
            @Override
            public MessageQueue select(List<MessageQueue> list, Message message, Object args) {
                Integer id = (Integer) args;
                int index = id % list.size();
                return list.get(index);
            }
        }, orderId);
        System.out.println(send);
    }
}


消费者

private static void pushConsumer() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dadaConsumer");
    consumer.setNamesrvAddr("47.112.12.45:9876");
    consumer.subscribe("dada_topic","*");
    // 有序的监听消息
    consumer.registerMessageListener((MessageListenerOrderly) (list, consumeOrderlyContext) -> {
        list.forEach(x->{
            try {
                System.out.println(Thread.currentThread().getName() + " "+x.getQueueId()+" "+new String( x.getBody(),"UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        });
       return ConsumeOrderlyStatus.SUCCESS;
    });
    // 启动消费者
    consumer.start();
}


三、延时消息


private static void delay() throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("dada");
    producer.setNamesrvAddr("47.112.12.45:9876");
    producer.start();
    Message message = new Message("dada-topic","延时消息".getBytes("UTF-8"));
    /**
     * 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     * 以上支持的延迟时间在msg.setDelayTimeLevel对应的级别依次是1,2,3。。。。
     */
    message.setDelayTimeLevel(3);
    // 发送消息
    producer.send(message, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("发送成功了!"+sendResult);
        }
        @Override
        public void onException(Throwable throwable) {
            System.out.println("发送失败了!"+throwable);
        }
    });
}

四、事务消息

public class TransactionListenerImpl implements TransactionListener {

    private static Map<String,LocalTransactionState> STATE_MAP=new HashMap<>();

    /**
     * 执行业务逻辑
     * @param message   发送的消息对象
     * @param o
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            System.out.println("用户A账户减500元");
            Thread.sleep(500);
//          模拟成功和失败
            System.out.println(1/0);
            System.out.println("用户B账户加500元");
            Thread.sleep(800);
            STATE_MAP.put(message.getTransactionId(),LocalTransactionState.COMMIT_MESSAGE);

            // 二次确认
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (InterruptedException e) {
            System.out.println("异常信息");
            e.printStackTrace();
        }
        STATE_MAP.put(message.getTransactionId(),LocalTransactionState.ROLLBACK_MESSAGE);
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }

    /**
     * 长时间不确认回查的方法
     * @param messageExt
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        System.out.println("走了回查");
        return STATE_MAP.get(messageExt.getTransactionId());
    }}


消费者

private static void consumer() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dadaConsumer");
    consumer.setNamesrvAddr("47.112.12.45:9876");
    consumer.subscribe("dada_topic", "*");
    // 有序的监听
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            list.forEach(x -> {
                try {
                    System.out.println(new String(x.getBody(), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    // 启动消费者
    consumer.start();
}


生产者

private static void producer() throws MQClientException, UnsupportedEncodingException {
    TransactionMQProducer producer = new TransactionMQProducer("dada");
    producer.setNamesrvAddr("47.112.12.45:9876");
    // 设置事务监听器
    producer.setTransactionListener(new TransactionListenerImpl());
    producer.start();
    // 发送消息
    Message message = new Message("dada_topic","用户A给用户B转账500元".getBytes("UTF-8"));
    producer.sendMessageInTransaction(message,null);
    producer.shutdown();
}


消息重试机制

一、produce端重试

private static void producer() throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("dada");
    producer.setNamesrvAddr("47.112.12.45:9876");
    // 发送失败重试三次
    producer.setRetryTimesWhenSendFailed(3);
    producer.start();
    String msg="消息重试 "+new Date().toString();
    Message message = new Message("dada-topic",msg.getBytes("UTF-8"));
    // 发送消息
    producer.send(message, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("发送成功了!"+sendResult);
        }
        @Override
        public void onException(Throwable throwable) {
            System.out.println("发送失败了!"+throwable);
        }
    },1000); // 1ms内没有成功会重试
}


二、customer端重试

如果消费失败exception,那么1S后再次消费,如果失败,那么5S后,再次消费,…直至2H后如果消费还失败,那么该条消息就会终止发送给消费者了!RocketMQ为我们提供了这么多次数的失败重试,但是在实际中也许我们并不需要这么多重试,比如重试3次,还没有成功,我们希望把这条消息存储起来并采用另一种方式处理。

/*消息重试处理机制*/
private static void customer() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dadaConsumer");
    consumer.setNamesrvAddr("47.112.12.45:9876");
    consumer.subscribe("dada-topic", "*");
    // 并发消息 和顺序消息 这里面用的并发消息
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            for (MessageExt messageExt : list) {
                try {
                    System.out.println(" 消费者: 消息" + new String(messageExt.getBody(), "UTF-8"));
                    System.out.println(1/0);
                } catch (Exception e) {
                    e.printStackTrace();
                    if (messageExt.getReconsumeTimes() == 3) {
                        // 该条消息可以存储到DB或者LOG日志中,或其他处理方式
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 成功
                    } else {
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;// 重试
                    }
                }
            }
            // 返回给服务器一个状态值 成功或者失败
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    // 启动消费者
    consumer.start();
}


timeout(比如由于网络原因导致消息压根就没有从MQ到消费者上,那么在RocketMQ内部会不断的尝试发送这条消息,直至发送成功为止!(比如集群中一个broker失败,就尝试另一个broker)延续Exception的思路,也就是消费端没有给RocketMQ返回消费的状态,即没有return ConsumeConcurrentlyStatus.CONSUME_SUCCESS或return ConsumeConcurrentlyStatus.RECONSUME_LATER,这样的就认为没有到达Consumer端

死信队列

当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。

在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。队列的名称是%DLQ%加customergroupName。


 

private static void Mortuustabellae() throws MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dadaConsumer");
    consumer.setNamesrvAddr("47.112.12.45:9876");
    consumer.subscribe("%DLQ%dadaConsumer", "*");
    // 并发消息 和顺序消息 这里面用的并发消息
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
            for (MessageExt messageExt : list) {
                try {
                    System.out.println("死信队列+ : 消息" + new String(messageExt.getBody(), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
            // 返回给服务器一个状态值 成功或者失败
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    // 启动消费者
    consumer.start();
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值