RocketMQ概念与使用



JAVA后端开发知识总结(持续更新…)


RocketMQ概念与使用



一、概述

  MetaQ是一款分布式队列模型的消息中间件,基于发布订阅模式,支持Topic与Queue两种模式,有Push和Pull两种消费方式,支持严格的消息顺序,亿级别的消息堆积能力,支持消息回溯和多个维度的消息查询

  MetaQ和RocketMQ区别:两者等价,在阿里内部称为MetaQ 3.0,对外称为RocketMQ 3.0。MetaQ基于RocketMQ内核采用拉模型,主要解决顺序消息海量堆积问题。

1.1 集群架构

在这里插入图片描述

  • NameServer
    • 专为 RocketMQ 设计的轻量级域名服务器,具有简单、可集群横向扩展、无状态、节点之间互不通信等特点。
    • 由于ZooKeeper功能过重,RocketMQ(即MetaQ 3.x)去掉了对ZooKeeper依赖,采用自己的NameServer。
    • 只需要保持最终一致性,即为AP模式
    • Broker集群、Producer集群、Consumer集群都需要与NameServer集群进行通信。
  • Broker
    • 消息中转角色,负责接收消息,存储消息,转发消息。
    • 一个Broker集群由多组Master/Slave组成,BrokerId为0表示Master,非0表示Slave。
    • 每个Broker节点在启动时,都会遍历轮询 NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报。
  • Consumer
    • 通过NameServer集群获得Topic的路由信息,连接到对应的Broker上,分为 Push Consumer / Pull Consumer,定时向Master、Slave发送心跳。
    • 前者向Consumer对象注册一个Listener接口,收到消息后回调Listener接口方法,采用long-polling长轮询实现push。
    • 后者由Consumer主动拉取信息,同Kafka。
  • Producer
    • 消息生产者,通过NameServer集群获得Topic的路由信息,包括Topic下面有哪些Queue,这些Queue分布在哪些Broker上等。
    • Producer只会将消息发送到Master节点上,因此只需与Master节点建立长连接,定时向Master发送心跳。

1.2 消息领域模型

  • Message:单位消息,消息队列中信息传递的载体;
  • Topic:软分区,对应相同的topic时,生产者对应消费者的分区标识。消息主题,一级消息类型,通过 Topic 对消息进行分类;
  • Tag:消息在topic基础上的二级分类;
  • Message Queue:硬分区,物理上区分topic,一个topic对应多个message queue,消息是发送到多个broker上的多个队列上;
  • Consumer Group(ID):一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致;
  • Producer Group(ID):一类 Producer 的集合名称,这类 Producer 通常发送一类消息,且发送逻辑一致;
  • Offset:绝对偏移值,message queue中有两类offset(commitOffset和offset),前者存储在OffsetStore中表示消费到的位置,后者是在PullRequest中为拉取消息位置;
  • 广播消费:Producer 向一些队列轮流发送消息,队列集合称为 Topic,每一个 consumer 实例都消费这个 Topic 对应的所有队列中的消息;
  • 集群消费:多个 Consumer 实例平均消费(Rebalance) 这个 topic 对应的队列集合中的消息。《Kafka Rebalance机制分析》
  • Producer 实例:Producer 的一个对象实例,不同的 Producer 实例可以运行在不同进程内或者不同机器上。Producer 实例线程安全,可在同一进程内多线程之间共享。
  • Consumer 实例:Consumer 的一个对象实例,不同的 Consumer 实例可以运行在不同进程内或者不同机器上。一个 Consumer 实例内配置线程池消费消息。

1.3 注意事项

1.3.1 发布订阅

  • 对于重要消息,业务方需要重发补偿的机制;
  • 对于大Message,配置压缩与解压缩,推荐进行拆分:
    • MetaQ采用RPC方式,可能会导致网络层的Buffer异常;
    • 服务器存储是LRU CACHE系统,过大的消息会占用较多Cache;
  • 设置Message Key属性进行唯一标识,如各类id;
  • 发送消息时,自由设置Tag,完成订阅方过滤需求;
  • 订阅Topic时,可以使用表达式方式过滤消息(Message Tag);
  • 非顺序消息消费,耗时时间不做限制,顺序消息消费,耗时时间有限制。

1.3.2 消息重复

  • MetaQ不能保证消息不重复:分布式环境下的超时问题,Rebalance短暂不一致,订阅者意外宕机;
  • 通过DB等去重,或者通过主动拉的方式,可保证拉消息绝对不重复。

1.3.3 消息重试

  • 非顺序消息消费失败重试,可以指定下次到达Consumer的时间,消费失败重试次数有限制,超过重试次数,消息进入死信队列。
  • 顺序消息消费失败重试,某个队列正在消费的消息消费失败,会将当前队列挂起。

1.3.4 消费幂等

  • 发送时消息重复【消息 Message ID 不同】:MQ Producer 发送消息场景下,消息已成功发送到服务端并完成持久化,此时网络闪断或者客户端宕机导致服务端应答给客户端失败。如果此时 MQ Producer 意识到消息发送失败并尝试再次发送消息,MQ 消费者后续会收到两条内容相同但是 Message ID 不同的消息。
  • 投递时消息重复【消息 Message ID 相同】:MQ Consumer 消费消息场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,MQ 服务端将在网络恢复后再次尝试投递之前已被处理过的消息,MQ 消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

真正安全的幂等处理,不建议以 Message ID 作为处理依据,最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置。

Message message = new Message();
message.setKey("ORDERID_10");
SendResult sendResult = producer.send(message);

1.3.5 消息堆积

真实堆积

  • 消费正常,发送方量级增大超出消费上限,出现堆积;
  • 消费变慢,出现堆积,通过单机视角jstack、arthas、排查RT是否增长;
  • 顺序消费,某个队列某条数据消费失败,导致当前队列block,堆积增大,需要解决消费失败问题。

1.4 集群

双主双从

在这里插入图片描述

工作流程

  1. 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来。
  2. Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及所有的Topic信息,注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
  3. 收发消息前先创建Topic,创建Topic时需指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
  4. Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
  5. Consumer跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。

《集群搭建步骤》

1.5 mqadmin集群管理工具

略。。。

二、基本使用

  • RocketMQ客户端依赖导入
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.4.0</version>
</dependency>

2.1 普通消息发送

一般步骤

  1. 创建消息生产者Producer,并制定生产者组名;
  2. 指定Nameserver地址;
  3. 启动Producer;
  4. 创建消息对象,指定主题Topic、Tag和消息体;
  5. 发送消息;
  6. 关闭生产者Producer。

注意

  1. 一个应用创建一个Producer,ProducerGroupName需由应用来保证唯一(全局对象或者单例)。
  2. 默认会在topic不存在时自动创建,并且为topic生成4个MessageQueue。
  3. Producer对象在使用之前必须调用start()初始化一次(注意不是每次发消息时)。
  4. 一个Producer对象可以同步发送(send(message))多个topic、多个tag的消息。
  5. 应用退出时,shutdown一下。
  • 发送同步消息
public class SyncProducer {

    public static void main(String[] args) throws Exception {
        // 1.创建消息生产者 Producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        // 2.指定 Nameserver 地址:集群配置(用;隔离)
        producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.启动 Producer
        producer.start();

        for (int i = 0; i < 10; i++) {
            // 4.创建消息对象,指定主题 Topic、Tag 和消息体
            /**
             * 参数一:消息主题 Topic
             * 参数二:消息 Tag
             * 参数三:消息内容:字节数组
             */
            Message msg = new Message("base", "Tag1", ("Hello World" + i).getBytes());
            // 5.发送同步消息
            //  阻塞等待回传
            SendResult result = producer.send(msg);
            // 获取发送状态
            SendStatus status = result.getSendStatus();
            // 消息 ID 和接收队列 ID
			String msgId = result.getMsgId();
			int queueId = result.getMessageQueue().getQueueId();
            System.out.println("发送结果:" + result + msgId + queueId);

            // 线程睡 1 秒
            TimeUnit.SECONDS.sleep(1);
        }
        // 6.关闭生产者 Producer
        producer.shutdown();
    }
}
  • 发送异步消息

  在send()方法中实现 SendCallback() 回调接口。

  异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。

public class AsyncProducer {

    public static void main(String[] args) throws Exception {
        // 1.创建消息生产者 Producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        // 2.指定 Nameserver 地址
        producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.启动 Producer
        producer.start();

        for (int i = 0; i < 10; i++) {
            // 4.创建消息对象,指定主题 Topic、Tag 和消息体
            Message msg = new Message("base", "Tag2", ("Hello World" + i).getBytes());
            // 5.发送异步消息
            producer.send(msg, new SendCallback() {
                /**
                 * 发送成功的回调函数
                 */
                public void onSuccess(SendResult sendResult) {
                    System.out.println("发送结果:" + sendResult);
                }

                /**
                 * 发送失败的回调函数
                 */
                public void onException(Throwable e) {
                    System.out.println("发送异常:" + e);
                }
            });

            // 线程睡 1 秒
            TimeUnit.SECONDS.sleep(1);
        }

        // 6.关闭生产者 Producer
        producer.shutdown();
}

  • 发送单向消息
  • 这种方式主要用在不特别关心发送结果的场景,例如日志发送。
  • 利用sendOneway(msg) 发送消息,不返回结果。
/**
 * 发送单向消息
 */
public class OneWayProducer {

    public static void main(String[] args) throws Exception, MQBrokerException {
        // 1.创建消息生产者 Producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        // 2.指定 Nameserver 地址
        producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.启动 Producer
        producer.start();

        for (int i = 0; i < 3; i++) {
            // 4.创建消息对象,指定主题 Topic、Tag 和消息体
            Message msg = new Message("base", "Tag3", ("Hello World,单向消息" + i).getBytes());
            // 5.发送单向消息
            producer.sendOneway(msg);

            // 线程睡 1 秒
            TimeUnit.SECONDS.sleep(5);
        }

        // 6.关闭生产者 Producer
        producer.shutdown();
    }
}

2.2 普通消息消费

一般步骤

  1. 创建消费者Consumer,制定消费者组名;
  2. 指定Nameserver地址;
  3. 订阅主题Topic和Tag;
  4. 设置回调函数,处理消息;
  5. 启动消费者Consumer。

注意

  1. 一个应用创建一个Consumer,ConsumerGroupName需由应用来保证唯一,(全局对象或者单例)。
  2. setConsumeFromWhere设置从哪里开始消费,subscribe订阅指定topic下的Tags(可以通配符)。
  3. setConsumeMessageBatchMaxSize()设置批量消费
  4. 如果为批量消费,要么都成功,要么都失败。
  5. 注册监听器,自定义实现回调方法,在消息到达时执行,此方法由RabbitMQ客户端多线程回调,需要应用来处理并发安全问题。
  6. 消息消费不成功有重试机制,达到阈值后则丢弃。
  7. 调用start()初始化。
  • 默认负载均衡模式——集群消费
  • 消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同。
  • 当前示例为PushConsumer,感官上是从服务器推到了客户端,实际上Consumer内部使用长轮询Pull方式从RabbitMQ服务器拉消息,然后再回调Listener方法。
public class Consumer {

    public static void main(String[] args) throws Exception {
        // 1.创建消费者 Consumer,制定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 2.指定 Nameserver 地址
        consumer.setNamesrvAddr("39.96.86.4:9876;59.110.158.93:9876");
        // 3.订阅主题 Topic 和 Tag
        consumer.subscribe("base", "Tag1")
        
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println(list);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //5.启动消费者consumer
        consumer.start();
    }
}
  • 广播模式消费消息

通过setMessageModel() 进行模式指定。

public class Consumer {

    public static void main(String[] args) throws Exception {
        // 1.创建消费者 Consumer,制定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 2.指定 Nameserver 地址
        consumer.setNamesrvAddr("39.96.86.4:9876;59.110.158.93:9876");
        //  3.订阅主题 Topic 和 Tag
        consumer.subscribe("base", "Tag2");
		// 指定消费模式为广播模式
        consumer.setMessageModel(MessageModel.BROADCASTING);

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                System.out.println(list);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 5.启动消费者 Consumer
        consumer.start();
    }
}

2.3 顺序(queue)消息

消息顺序问题

在这里插入图片描述

  1. RocketMQ可以严格地保证消息有序,可以分为分区(queue)有序和全局有序(只有一个队列的情况)。
  2. Broker存储消息有序的前提是Producer发送消息是有序的:
    • 需要同一线程发送一组消息,而消息不能异步发送,同步发送时才能保证Broker收到的是有序的。
    • 同步保证前一个消息发送成功并返回响应后,再发送下一个。
    • 每次发送选择的是同一个MessageQueue
  3. Consumer消费也要是顺序的,需要保证一个queue只在一个线程内被消费。
  4. 例:一个订单的顺序流程是创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

在这里插入图片描述

2.3.1 顺序发送

  • 在send()中实现一个MessageQueueSelector,重写选择MessageQueue的select()方法。
  • 自定义选择MessageQueue的话,就可以控制将某一类消息发送到对应的MessageQueue中了。
  • send()的最后一个参数,会作为参数被传给select方法中的arg。

在这里插入图片描述

public class Producer {

    public static void main(String[] args) throws Exception {
        // 1.创建消息生产者 Producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        // 2.指定 Nameserver 地址
        producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.启动 Producer
        producer.start();
        // 构建消息(订单)集合
        List<OrderStep> orderSteps = OrderStep.buildOrders();
        // 发送消息
        for (int i = 0; i < orderSteps.size(); i++) {
            String body = orderSteps.get(i) + "";
            Message message = new Message("OrderTopic", "Order", "i" + i, body.getBytes());
            /**
             * 参数一:消息对象
             * 参数二:消息队列的选择器
             * 参数三:选择队列的业务标识(订单 ID)
             */
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                /**
                 *
                 * @param mqs:队列集合
                 * @param msg:消息对象
                 * @param arg:业务标识的参数
                 */
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    long orderId = (long) arg;
                    long index = orderId % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderSteps.get(i).getOrderId());

            System.out.println("发送结果:" + sendResult);
        }
        producer.shutdown();
    }
}
  • 对于官方Demo,可以看出,orderId为6的订单被顺序放入queueId为6的队列,且queueOffset同时在顺序增长。

2.3.2 顺序消费

  • 注册的监听器中需要实现MessageListenerOrderly接口。
  • 在集群模式下,利用分布式锁,保证了一个queue只会被一个消费者锁定和消费。
  • 加锁保证只有一个线程可以消费,从ProcessQueue中获取出来的消息是有序的,Consumer保证了消费的有序性。
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        // 1.创建消费者 Consumer,制定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 2.指定 Nameserver 地址
        consumer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.订阅主题 Topic 和 Tag
        consumer.subscribe("OrderTopic", "*");

        // 4.注册消息监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println("线程名称:【" + Thread.currentThread().getName() + "】:" + new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        // 5.启动消费者
        consumer.start();
        System.out.println("消费者启动");
    }
}

顺序消费结果

在这里插入图片描述

2.4 延迟消息

  • 比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
  • 该逻辑可以类比RabbitMQ中用死信队列构造延时队列,来实现分布式事务
  • RocketMQ并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18。
// MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
  • 发送端:直接在Message中设置延迟时间——setDelayTimeLevel(2)
public class Producer {

    public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
        // 1.创建消息生产者 Producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        // 2.指定 Nameserver 地址
        producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.启动 Producer
        producer.start();

        for (int i = 0; i < 10; i++) {
            // 4.创建消息对象,指定主题 Topic、Tag 和消息体
            Message msg = new Message("DelayTopic", "Tag1", ("Hello World" + i).getBytes());
            // 设定延迟时间
            msg.setDelayTimeLevel(2);
            // 5.发送消息
            SendResult result = producer.send(msg);
            SendStatus status = result.getSendStatus();
            System.out.println("发送结果:" + result);

            TimeUnit.SECONDS.sleep(1);
        }

        // 6.关闭生产者 Producer
        producer.shutdown();
    }
}
  • 消费端
public class Consumer {

    public static void main(String[] args) throws Exception {
        // 1.创建消费者 Consumer,制定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 2.指定 Nameserver 地址
        consumer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.订阅主题 Topic 和 Tag
        consumer.subscribe("DelayTopic", "*");

        // 4.设置回调函数,处理消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {

            //接受消息内容
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println("消息ID:【" + msg.getMsgId() + "】,延迟时间:" + (System.currentTimeMillis() - msg.getStoreTimestamp()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 5.启动消费者 Consumer
        consumer.start();
        System.out.println("消费者启动");
    }
}

2.5 批量消息发送

  批量发送消息能显著提高传递小消息的性能,批量消息应该有相同的Topic和waitStoreMsgOK,而且不能是延时消息。批量消息的单次发送不应超过4MB,大于4MB时最好把消息进行分割

  • 批量发送与消息分割
public class ListSplitter implements Iterator<List<Message>> {
   private final int SIZE_LIMIT = 1024 * 1024 * 4;
   private final List<Message> messages;
   private int currIndex;
   public ListSplitter(List<Message> messages) {
           this.messages = messages;
   }
    @Override 
    public boolean hasNext() {
       return currIndex < messages.size();
   }
   	@Override 
    public List<Message> next() {
       int nextIndex = currIndex;
       int totalSize = 0;
       for (; nextIndex < messages.size(); nextIndex++) {
           Message message = messages.get(nextIndex);
           int tmpSize = message.getTopic().length() + message.getBody().length;
           Map<String, String> properties = message.getProperties();
           for (Map.Entry<String, String> entry : properties.entrySet()) {
               tmpSize += entry.getKey().length() + entry.getValue().length();
           }
           tmpSize = tmpSize + 20; // 增加日志的开销 20 字节
           if (tmpSize > SIZE_LIMIT) {
               // 单个消息超过了最大的限制
               // 忽略,否则会阻塞分裂的进程
               if (nextIndex - currIndex == 0) {
                  // 假如下一个子列表没有元素,则添加这个子列表,然后退出循环,否则只是退出循环
                  nextIndex++;
               }
               break;
           }
           if (tmpSize + totalSize > SIZE_LIMIT) {
               break;
           } else {
               totalSize += tmpSize;
           }

       }
       List<Message> subList = messages.subList(currIndex, nextIndex);
       currIndex = nextIndex;
       return subList;
   }
}
// 把大的消息分裂成若干个小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
  try {
      List<Message>  listItem = splitter.next();
      producer.send(listItem);
  } catch (Exception e) {
      e.printStackTrace();
      //处理error
  }
}

2.6 过滤消息

2.6.1 Tag过滤

  消费者将接收包含TAGA或TAGB或TAGC的消息,但是限制是一个消息只能有一个Tag标签,这对于复杂的场景可能不起作用。如需订阅某 Topic 下所有类型的消息,Tag 用符号 * 表示。

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");

2.6.2 SQL过滤

  可以使用SQL表达式筛选消息,SQL特性可以通过发送消息时的属性来进行计算,在RocketMQ定义的语法下,可以实现一些简单的逻辑:

  • 数值比较,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比较,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 逻辑符号 AND,OR,NOT;

支持类型

  • 数值:123,3.1415;
  • 字符:‘abc’,必须用单引号
  • NULL,特殊的常量
  • 布尔值:TRUE 或 FALSE
  • Produce

Message中调用putUserProperty() 传入一些SQL要用的参数。

public class Producer {

    public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
        // 1.创建消息生产者 Producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        // 2.指定 Nameserver 地址
        producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
        // 3.启动 Producer
        producer.start();

        for (int i = 0; i < 10; i++) {
            // 4.创建消息对象,指定主题 Topic、Tag 和消息体
            Message msg = new Message("FilterSQLTopic", "Tag1", ("Hello World" + i).getBytes());
            // 设置一些属性
			msg.putUserProperty("i", String.valueOf(i));
            // 5.发送消息
            SendResult result = producer.send(msg);
            SendStatus status = result.getSendStatus();
            System.out.println("发送结果:" + result);

            TimeUnit.SECONDS.sleep(1);
        }

        // 6.关闭生产者 Producer
        producer.shutdown();
    }
}
  • Consumer

subscribe()传入选择器MessageSelector,调用bySql() 进行SQL过滤语句编写。

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");

// 只有订阅的消息有属性 i:i >= 0 and i <= 3
consumer.subscribe("FilterSQLTopic", MessageSelector.bySql("a between 0 and 3");
consumer.registerMessageListener(new MessageListenerConcurrently() {
   @Override
   public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
       return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
   }
});
consumer.start();

2.7 事务消息——解决分布式事务问题

  发送事务消息是一个二阶段过程,类似于数据库的二阶段提交,包括两个过程:

  1. 发送Half消息:应用发送Half消息到Broker,消费方此时不能拉取到消息,需要等待后续Half消息状态。
  2. 发送事务状态:应用根据自身的事务执行情况,对Half消息发送事务状态:Commit、Rollback、UNKNOW(当前无法判决事务状态,如事务无法及时完成)。

2.7.1 基本流程

事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction:提交事务,它允许消费者消费此消息。
  • TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
  • TransactionStatus.Unknown:中间状态,它代表需要检查消息队列来确定状态。

事务流程

在这里插入图片描述

  • 事务消息的发送及提交
  1. 发送消息(half消息);
  2. 服务端响应消息写入结果;
  3. 根据发送结果执行本地事务,如果写入失败,此时half消息对业务不可见,本地逻辑不执行;
  4. 根据本地事务状态执行Commit或者Rollback,Commit操作生成消息索引,消息对消费者可见。
  • 事务补偿
  1. 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次回查
  2. Producer收到回查消息,检查回查消息对应的本地事务的状态;
  3. 根据本地事务状态,重新Commit或者Rollback;
  4. 补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

2.7.2 基本使用

  • Producer

  使用 TransactionMQProducer类创建生产者,并指定唯一的 ProducerGroup,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复。

  • setTransactionListener() 添加事务监听器作为执行本地事务的入口。

  当发送HALF消息成功时,使用 executeLocalTransaction() 方法来执行本地事务,它返回三个事务状态之一。checkLocalTranscation() 方法用于检查本地事务状态,并回应消息队列的检查请求。

public class txProducer {

    public static void main(String[] args) throws Exception {
        // 1.创建消息生产者 Producer,并制定生产者组名
        TransactionMQProducer producer = new TransactionMQProducer("group5");
        // 2.指定 Nameserver 地址
        producer.setNamesrvAddr("39.96.86.4:9876;59.110.158.93:9876");

        // 添加事务监听器
        producer.setTransactionListener(new TransactionListener() {
            /**
             * 在该方法中执行本地事务
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                if (StringUtils.equals("TAGA", msg.getTags())) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if (StringUtils.equals("TAGB", msg.getTags())) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                } else if (StringUtils.equals("TAGC", msg.getTags())) {
                    return LocalTransactionState.UNKNOW;
                }
                return LocalTransactionState.UNKNOW;
            }
            /**
             * 该方法是 MQ 进行消息事务状态回查的逻辑
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                System.out.println("消息的Tag:" + msg.getTags());
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });

        // 3.启动 Producer
        producer.start();
        String[] tags = {"TAGA", "TAGB", "TAGC"};
        for (int i = 0; i < 3; i++) {
            // 4.创建消息对象,指定主题 Topic、Tag 和消息体
            Message msg = new Message("TransactionTopic", tags[i], ("Hello World" + i).getBytes());
            // 5.发送消息
            // 传入 null 对所有消息进行事务控制
            SendResult result = producer.sendMessageInTransaction(msg, null);
            // 发送状态
            SendStatus status = result.getSendStatus();
            System.out.println("发送结果:" + result);
            TimeUnit.SECONDS.sleep(2);
        }
        // 6.关闭生产者 Producer
        //producer.shutdown();
    }
}
  • 生产者2
  • LocalTransactionExecuter接口:发送 Half 消息成功后,会立即调用这里判断当前的事务状态,可以写业务操作事务的代码。
  • TransactionCheckListener接口:进行消息事务状态回查。
  • 必须要有一致的业务逻辑。
public class txProducer2 {
    public static void main(String[] args) throws MQClientException, InterruptedException {
    	// 监听器
        TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("rename_your_producerGroup");
        // 本地 check 线程池的 corePoolSize
        producer.setCheckThreadPoolMinSize(2);
        // 本地 check 线程池的 maximumPoolSize
        producer.setCheckThreadPoolMaxSize(2);
        // 本地 check 线程池的队列长度
        producer.setCheckRequestHoldMax(2000);
        // 设置 checkListener,Broker 定期 check 的时候被调用
        producer.setTransactionCheckListener(transactionCheckListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        // 
        TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
        for (int i = 0; i < 100; i++) {
            try {
                Message msg =
                    new Message("TopicTest", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                // 过程 1、发送 Half 消息
                TransactionSendResult result = producer.sendMessageInTransaction(msg, tranExecuter, null);

                System.out.printf("msgId:%s, transaction result:%s %n", result.getMsgId(), result.getLocalTransactionState());

                if (result.getLocalTransactionState() != LocalTransactionState.COMMIT_MESSAGE) {
                    System.out.printf("halfMsg sendResult%s, transaction result:%s, errorMsg:%s %n", result.getSendStatus(), result.getLocalTransactionState(), result.getErrorMessage());
                }
                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }

    public static class TransactionExecuterImpl implements LocalTransactionExecuter {
        private AtomicInteger transactionIndex = new AtomicInteger(1);

        @Override
        public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) {
            // 过程 2、发送事务状态
            // 发送 Half 消息成功后,会立即调用这里判断当前的事务状态,这里可以写业务操作事务的代码。
            // 如果应用还没执行完成宕机,或者暂时无法判断事务状态,返回 UNKNOW 状态
            // Broker 定期发送消息来 Check 这个 Half 消息,会调用下面的 TransactionCheckListener 接口。
            // 如果这里返回 null 或者抛出异常,同样设置为 UNKNOW 状态
            int value = transactionIndex.getAndIncrement();
            if (value == 0) {
                //注意:如果抛出异常,等于设置 UNKNOW
                throw new RuntimeException("Could not find db");
            }
            else if ((value % 5) == 0) {
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
            else if ((value % 4) == 0) {
                return LocalTransactionState.COMMIT_MESSAGE;
            }
            return LocalTransactionState.UNKNOW;
        }
    }

    public static class TransactionCheckListenerImpl implements TransactionCheckListener {
        private AtomicInteger transactionIndex = new AtomicInteger(0);

        @Override
        public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
            // 过程 2 发送 UNKNOWN 状态或者 Broker 没能在限定时间内接收状态,Broker 会定期 check 状态,则调用此接口
            // 如果这里返回 null 或者抛出异常,同样设置为 UNKNOW 状态
            System.out.printf("server checking TrMsg %s%n", msg);
            int value = transactionIndex.getAndIncrement();
            if ((value % 6) == 0) {
                // 注意:如果抛出异常,等于设置 UNKNOW
                throw new RuntimeException("Could not find db");
            } else if ((value % 5) == 0) {
                return LocalTransactionState.ROLLBACK_MESSAGE;
            } else if ((value % 4) == 0) {
                return LocalTransactionState.COMMIT_MESSAGE;
            }
            return LocalTransactionState.UNKNOW;
        }
    }
}
  • 总结
  1. 事务消息不支持延时消息和批量消息。
  2. 为了避免单个消息被检查多次导致半队列消息累积,默认将单个消息的检查次数限制为 15 次,可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。
  3. 如果已经检查某条消息超过 N 次的话,Broker 将丢弃此消息,并在默认情况下同时打印错误日志,可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
  4. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 配置的特定时长后被检查,当发送事务消息时,可以通过设置CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
  5. 事务性消息可能不止一次被检查或消费。
  6. 提交给用户的目标主题消息可能会失败,它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失,并且事务完整性得到保证,可使用同步的双重写入机制
  7. 事务消息的生产者 ID 不能与其它类型消息的生产者 ID 共享,事务消息允许反向查询,MQ服务器能通过生产者 ID 查询到消费者。

2.8 消费者主动Pull

《RocketMq : 消费消息的两种方式 pull 和 push》

  • 示例
public class PullConsumer {
    private static final Map<MessageQueue, Long> offseTable = new HashMap<MessageQueue, Long>();


    public static void main(String[] args) throws MQClientException {
        DefaultPullConsumer consumer = new DefaultPullConsumer("please_rename_unique_group_name_5");

        consumer.start();

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");
        for (MessageQueue mq : mqs) {
            System.out.println("Consume from the queue: " + mq);
            PullResult pullResult;
            try {
                pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                System.out.println(pullResult);
                putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                switch (pullResult.getPullStatus()) {
                    case FOUND:
                        // TODO
                        break;
                    case NO_MATCHED_MSG:
                        break;
                    case NO_NEW_MSG:
                        break;
                    case OFFSET_ILLEGAL:
                        break;
                    default:
                        break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        consumer.shutdown();
    }

    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = offseTable.get(mq);
        if (offset != null)
            return offset;

        return 0;
    }

    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        offseTable.put(mq, offset);
    }
}

三、RocketMQ补充

3.1 消息的存储 + 高可用性机制 + 负载均衡

《RocketMQ:消息的存储、高可用性机制、负载均衡》

  • ACK机制保证持久化存储
  • 消息存储为顺序写
  • mmap零拷贝机制,提高消息存盘和网络发送的速度

3.2 消息重试

  • 广播模式下,消费抛出异常,不会重试
  • 顺序消息消费,RocketMQ会自动不断进行消息重试,直到返回成功才停止重试。此时消息消费被阻塞,因此耗时时间有限制。
  • 要保证消费的顺序性,所以一旦某个队列的某条消息消费失败,那么这个队列暂停消费,其它队列不受影响:
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
          // 设置挂起当前 Queue 的消费,挂起时间 5000ms
          context.setSuspendCurrentQueueTimeMillis(5000);
          return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
  • 非顺序消息消费,耗时时间不做限制,默认自动重试16次,只对集群消费有效,超过最大重试次数后会被放入死信队列
  • 重试次数:

在这里插入图片描述

  • 每次消费失败后,应用可以设置消息下次间隔多长时间到达Consumer,正常情况下,间隔时间会比较准确:
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
              ConsumeConcurrentlyContext context) {
              
          MessageExt msg = msgs.get(0);

          // 获取这条消息是第几次重试消费
          long value = msg.getReconsumeTimes();

          // 设置定时 Level 为 5,一分钟后重试消费这条消息
          context.setDelayLevelWhenNextConsume(5);

          return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
  • 重试配置方式
  • MessageListener可以传入consumer.subscribe() 中。

在这里插入图片描述

在这里插入图片描述

  • 重试次数配置
consumer.getMetaPushConsumerImpl().setMaxReconsumeTimes();

在这里插入图片描述

在这里插入图片描述

3.3 死信队列

特性

在这里插入图片描述

参考文档

《深入理解NameServer》

《RocketMQ消息发送流程解读》

《RocketMQ源码目录篇》

《RocketMQ学习》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值