RocketMQ入门详解

RocketMQ入门详解

一、RocketMQ安装

docker安装RocketMQ

下载地址

官方中文文档

可视化监控平台搭建

GitHub地址

Github中文使用文档

clone 到本地的文件路径:docs/1_0_0/UserGuide_CN.md

二、RocketMQ基本概念

  • Producer:就是消息生产者,可以集群部署。它会先和 NameServer 集群中的随机一台建立长连接,得知当前要发送的 Topic 存在哪台 Broker Master上,然后再与其建立长连接,支持多种负载平衡模式发送消息
  • Consumer:消息消费者,也可以集群部署。它也会先和 NameServer 集群中的随机一台建立长连接,得知当前要消息的 Topic 存在哪台 Broker Master、Slave上,然后它们建立长连接,支持集群消费和广播消费消息
  • Consumer Group:消费组;每一个 consumer 实例都属于一个 consumer group,每一条消息只会被同一个 consumer group 里的一个 consumer 实例消费。(不同consumer group可以同时消费同一条消息)
  • Broker:主要负责消息的存储、查询消费,支持主从部署,一个 Master 可以对应多个 Slave,Master 支持读写,Slave 只支持读。Broker 会向集群中的每一台 NameServer 注册自己的路由信息
  • NameServer:管是一个很简单的 Topic 路由注册中心,支持 Broker 的动态注册和发现,保存 Topic 和 Borker 之间的关系。通常也是集群部署,但是各 NameServer 之间不会互相通信, 各 NameServer 都有完整的路由信息,即无状态
  • Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个 Topic;一个消息的接收者可以订阅一个或者多个 Topic 消息
  • Message Queue:相当于是 Topic 的分区;用于并行发送和接收消息

在这里插入图片描述

工作流程

先启动 NameServer 集群,各 NameServer 之间无任何数据交互,Broker 启动之后会向所有 NameServer 定期(每 30s)发送心跳包,包括:IP、Port、TopicInfo,NameServer 会定期扫描 Broker 存活列表,如果超过 120s 没有心跳则移除此 Broker 相关信息,代表下线。

这样每个 NameServer 就知道集群所有 Broker 的相关信息,此时 Producer 上线从 NameServer 就可以得知它要发送的某 Topic 消息在哪个 Broker 上,和对应的 Broker (Master 角色的)建立长连接,发送消息。

Consumer 上线也可以从 NameServer 得知它所要接收的 Topic 是哪个 Broker ,和对应的 Master、Slave 建立连接,接收消息

三、消息发送与消费示例

3.1 环境准备和步骤分析

注意:rocketmq-client 的版本,要与 RocketMQ版本一致

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

消息发送者步骤分析:

  1. 创建消息生产者 producer,并指定生产者组名
  2. 指定 Nameserver 地址
  3. 启动 producer
  4. 创建消息对象,指定主题 TopicTag 和消息体
  5. 发送消息
  6. 关闭生产者 producer

消费者步骤分析:

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

3.2 基本样例

发送同步消息

这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

public class SyncProducer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 设置消息同步发送失败时的重试次数,默认为 2
        producer.setRetryTimesWhenSendFailed(2);
        // 设置消息发送超时时间,默认3000ms
        producer.setSendMsgTimeout(1000000);
        // 启动Producer实例
        producer.start();
        for (int i = 0; i < 100; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message("TopicTest" , "TagA" ,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 发送消息到一个Broker
            SendResult sendResult = producer.send(msg);
            // 通过sendResult返回消息是否成功送达
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
发送异步消息

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

public class AsyncProducer {
	public static void main(String[] args) throws Exception {
    	// 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    	// 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 设置消息异步发送失败时的重试次数,默认为 2
        producer.setRetryTimesWhenSendAsyncFailed(2);
        // 设置消息发送超时时间,默认3000ms
        producer.setSendMsgTimeout(1000000);
    	// 启动Producer实例
        producer.start();
        producer.setRetryTimesWhenSendAsyncFailed(0);

        int messageCount  = 100;
        final CountDownLatch countDownLatch  = new CountDownLatch(messageCount);

    	for (int i = 0; i < 100; i++) {
                final int index = i;
            	// 创建消息,并指定Topic,Tag和消息体
                Message msg = new Message("TopicTest",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                // SendCallback接收异步返回结果的回调
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.printf("%-10d OK %s %n", index,
                            sendResult.getMsgId());
                    }
                    @Override
                    public void onException(Throwable e) {
      	              System.out.printf("%-10d Exception %s %n", index, e);
      	              e.printStackTrace();
                    }
            	});
    	}
        countDownLatch.await();
    	// 如果不再发送消息,关闭Producer实例。
    	producer.shutdown();
    }
}
单向发送消息

这种方式主要用在不特别关心发送结果的场景,例如日志发送。

public class OnewayProducer {
	public static void main(String[] args) throws Exception{
    	// 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    	// 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
    	// 启动Producer实例
        producer.start();
    	for (int i = 0; i < 100; i++) {
        	// 创建消息,并指定Topic,Tag和消息体
        	Message msg = new Message("TopicTest",
                "TagA",
                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
        	);
        	// 发送单向消息,没有任何返回结果
        	producer.sendOneway(msg);

    	}
    	// 如果不再发送消息,关闭Producer实例。
    	producer.shutdown();
    }
}
消费消息:集群模式(负载均衡)

消费者采用集群方式消费消息,一条消息同一个消费者组中只有一个消费者会消费到

public class ClusterConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 指定Namesrv地址信息.
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅Topic
        consumer.subscribe("Test", "*");
        //负载均衡模式消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n",
                        Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

}
消息消息:广播模式

消费者采用广播的方式消费消息,一条消息同一个消费者组中每个消费者都要消费

public class RadioConsumer {
    public static void main(String[] args) throws Exception {
        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        // 指定Namesrv地址信息.
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅Topic
        consumer.subscribe("Test", "*");
        //广播模式消费
        consumer.setMessageModel(MessageModel.BROADCASTING);
        // 注册回调函数,处理消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n",
                        Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //启动消息者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

3.3 顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的

下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列

顺序发送消息
public class Producer {

   public static void main(String[] args) throws Exception {
       DefaultMQProducer producer = new DefaultMQProducer("group2");

       producer.setNamesrvAddr("1.14.252.45:9876");

       producer.start();

       String[] tags = new String[]{"TagA", "TagC", "TagD"};

       // 订单列表
       List<OrderStep> orderList = new Producer().buildOrders();

       Date date = new Date();
       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       String dateStr = sdf.format(date);
       for (int i = 0; i < 10; i++) {
           // 加个时间前缀
           String body = dateStr + " Hello RocketMQ " + orderList.get(i);
           Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());

           SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
               @Override
               public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                   Long id = (Long) arg;  //根据订单id选择发送queue
                   long index = id % mqs.size();
                   return mqs.get((int) index);
               }
           }, orderList.get(i).getOrderId());//订单id

           System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
               sendResult.getSendStatus(),
               sendResult.getMessageQueue().getQueueId(),
               body));
       }

       producer.shutdown();
   }

   /**
    * 订单的步骤
    */
   private static class OrderStep {
       private long orderId;
       private String desc;

       public long getOrderId() {
           return orderId;
       }

       public void setOrderId(long orderId) {
           this.orderId = orderId;
       }

       public String getDesc() {
           return desc;
       }

       public void setDesc(String desc) {
           this.desc = desc;
       }

       @Override
       public String toString() {
           return "OrderStep{" +
               "orderId=" + orderId +
               ", desc='" + desc + '\'' +
               '}';
       }
   }

   /**
    * 生成模拟订单数据
    */
   private List<OrderStep> buildOrders() {
       List<OrderStep> orderList = new ArrayList<OrderStep>();

       OrderStep orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("创建");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111065L);
       orderDemo.setDesc("创建");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("付款");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103117235L);
       orderDemo.setDesc("创建");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111065L);
       orderDemo.setDesc("付款");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103117235L);
       orderDemo.setDesc("付款");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111065L);
       orderDemo.setDesc("完成");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("推送");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103117235L);
       orderDemo.setDesc("完成");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("完成");
       orderList.add(orderDemo);

       return orderList;
   }
}
顺序消费消息
/**
* 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
*/
public class ConsumerInOrder {

   public static void main(String[] args) throws Exception {
       DefaultMQPushConsumer consumer = new
           DefaultMQPushConsumer("group2");
       consumer.setNamesrvAddr("127.0.0.1:9876");
       /**
        * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
        * 如果非第一次启动,那么按照上次消费的位置继续消费
        */
       consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

       consumer.subscribe("TopicTest", "TagA || TagC || TagD");

       consumer.registerMessageListener(new MessageListenerOrderly() {

           final Random random = new Random();

           @Override
           public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
               context.setAutoCommit(true);
               for (MessageExt msg : msgs) {
                   // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                   System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
               }
               try {
                   //模拟业务逻辑处理中...
                   TimeUnit.SECONDS.sleep(random.nextInt(10));
               } catch (Exception e) {
                   e.printStackTrace();
               }
               return ConsumeOrderlyStatus.SUCCESS;
           }
       });

       consumer.start();

       System.out.println("Consumer Started.");
   }
}

3.4 延时消息

比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存

启动消息消费者
public class ScheduledMessageConsumer {
   public static void main(String[] args) throws Exception {
      // 实例化消费者
      DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
      // 订阅Topics
      consumer.subscribe("TestTopic", "*");
      // 注册消息监听者
      consumer.registerMessageListener(new MessageListenerConcurrently() {
          @Override
          public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
              for (MessageExt message : messages) {
                  // Print approximate delay time period
                  System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later");
              }
              return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
          }
      });
      // 启动消费者
      consumer.start();
  }
}
发送延时消息
public class ScheduledMessageProducer {
   public static void main(String[] args) throws Exception {
      // 实例化一个生产者来产生延时消息
      DefaultMQProducer producer = new DefaultMQProducer("ExampleGroup");
       producer.setNamesrvAddr("127.0.0.1:9876");
      // 启动生产者
      producer.start();
      int totalMessagesToSend = 100;
      for (int i = 0; i < totalMessagesToSend; i++) {
          Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
          // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
          message.setDelayTimeLevel(3);
          // 发送消息
          producer.send(message);
      }
       // 关闭生产者
      producer.shutdown();
  }
}
public class ScheduledMessageConsumer {
   public static void main(String[] args) throws Exception {
      // 实例化消费者
      DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleGroup");
      consumer.setNamesrvAddr("127.0.0.1:9876");
      // 订阅Topics
      consumer.subscribe("TestTopic", "*");
      // 注册消息监听者
      consumer.registerMessageListener(new MessageListenerConcurrently() {
          @Override
          public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
              for (MessageExt message : messages) {
                  // Print approximate delay time period
                  System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later");
              }
              return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
          }
      });
      // 启动消费者
      consumer.start();
  }
}
验证

您将会看到消息的消费比存储时间晚10秒

使用限制
// org/apache/rocketmq/store/config/MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级118

3.5 批量消息

批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB

如果您每次只发送不超过 4MB 的消息,则很容易使用批处理,样例如下:

String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
   producer.send(messages);
} catch (Exception e) {
   e.printStackTrace();
   //处理error
}

如果消息的总长度可能大于 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
  }
}

3.6 过滤消息

在大多数情况下,TAG是一个简单而有用的设计,其可以来选择您想要的消息。例如:

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

消费者将接收包含TAGA或TAGB或TAGC的消息。但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息

SQL基本语法
  • 数值比较,比如:>>=<<=BETWEEN=
  • 字符比较,比如:=<>IN
  • IS NULL 或者 IS NOT NULL
  • 逻辑符号 ANDORNOT
  • 数值,比如:1233.1415
  • 字符,比如:'abc',必须用单引号包裹起来
  • NULL,特殊的常量
  • 布尔值,TRUEFALSE

只有使用 push 模式的消费者才能用使用SQL92标准的sql语句,接口如下:

public void subscribe(finalString topic, final MessageSelector messageSelector)
消息生产者

发送消息时,你能通过putUserProperty来设置消息的属性

DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
Message msg = new Message("TopicTest",
   tag,
   ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 设置一些属性
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);

producer.shutdown();
消息消费者

用MessageSelector.bySql来使用sql筛选消息

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 只有订阅的消息有这个属性a, a >=0 and a <= 3
consumer.subscribe("TopicTest", 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();

3.7 事务消息

流程分析

在这里插入图片描述

上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

事务消息发送及提交

(1) 发送消息(half消息)

(2)服务端响应消息写入结果

(3)根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)

(4)根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

事务补偿

(5)对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”

(6)Producer收到回查消息,检查回查消息对应的本地事务的状态

(7)根据本地事务状态,重新Commit或者Rollback

事务消息状态

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

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

创建事务性生产者

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

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        //创建事务监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        //创建消息生产者
        TransactionMQProducer producer = new TransactionMQProducer("group6");
        producer.setNamesrvAddr("192.168.25.135:9876;192.168.25.138:9876");
        //生产者这是监听器
        producer.setTransactionListener(transactionListener);
        //启动消息生产者
        producer.start();
        String[] tags = new String[]{"TagA", "TagB", "TagC"};
        for (int i = 0; i < 3; i++) {
            try {
                Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);
                TimeUnit.SECONDS.sleep(1);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        //producer.shutdown();
    }
}

实现事务的监听接口

当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务。它返回前一节中提到的三个事务状态之一。checkLocalTranscation 方法用于检查本地事务状态,并回应消息队列的检查请求

public class TransactionListenerImpl implements TransactionListener {

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("执行本地事务");
        if (StringUtils.equals("TagA", msg.getTags())) {
            return LocalTransactionState.COMMIT_MESSAGE;
        } else if (StringUtils.equals("TagB", msg.getTags())) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        } else {
            return LocalTransactionState.UNKNOW;
        }

    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("MQ检查消息Tag【"+msg.getTags()+"】的本地事务执行结果");
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}
使用限制
  1. 事务消息不支持延时消息和批量消息
  2. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax 参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为
  3. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数
  4. 事务性消息可能不止一次被检查或消费
  5. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制
  6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者

四、msgId生成算法

含义

	当开发者用rocketmq发送消息的时候通常都会返回如下消息

	SendResult [sendStatus=SEND_OK, msgId=00000000000000000000000000000001000118B4AAC2088BEB070000, offsetMsgId=C0A8216400002A9F00000000001A3D34, messageQueue=MessageQueue [topic=TopicTest, brokerName=192.168.33.100, queueId=1], queueOffset=2025]

	对于客户端来说msgId是由客户端producer自己生成的,offsetMsgId是由broker生成的,

	其中offsetMsgId就是我们在rocketMQ控制台直接输入查询的那个messageId。

概念

	msgId

		该ID 是消息发送者在消息发送时会首先在客户端生成,全局唯一,
		在 RocketMQ 中该 ID 还有另外的一个叫法:uniqId,无不体现其全局唯一性。

	offsetMsgId

		消息偏移ID,该 ID 记录了消息所在集群的物理地址,
		主要包含所存储 Broker 服务器的地址( IP 与端口号)以及所在commitlog 文件的物理偏移量。

msgId

   msgId

		生成步骤

			1.初始化参数LEN,FIX_STRING,COUNTER
			2.初始化buffer
			3.设置开始时间
			4.字节转string工具方法
			5.最终生成msgId

		细节
		
			 其中createUniqId就是最终生成msgId方法。除些之外的方法者是createUniqId调
			 用或者被间接调用的方法,这些方法实现也比较简单。

			StringBuilder sb = new StringBuilder(LEN * 2);
			由此可知msgId的长度是LEN * 2 = 16 * 2 = 32;

			设time = 当前时间 - 本月开始时间(ms);
			从代码得到 FIX_STRING = ip + 进程pid + MessageClientIDSetter.class.getClassLoader().hashCode();
			createUniqIDBuffer 加入time 和 counter 因子。
			最终得到msgId的生成因子是:   ip + 进程pid + MessageClientIDSetter.class.getClassLoader().hashCode() + time + counter(AtomicInteger自增变量)
			最后调用bytes2string进行十六进制的移位和编码就产生了我们的msgId。

		分析算法

			对于每个producer实例来说ip都是唯一的,所以不同producer生成的msgId是不会重复的。
			对于producer单个实例来说的区分因子是:time + counter。
			首先应用不重启的情况下msgId是保证唯一性的,
			应用重启了只要系统的时钟不变msgId也是唯一的。
			所以只要系统的时钟不回拨我们就可以保证msgId的全局唯一。

offsetMsgId

	offsetMsgId

		生成步骤

			broker端生成的offsetMsgId就比较简单了,直接就是主机ip + 物理分区的offset,
			再调用UtilAll.bytes2string进行移位转码就完成了

五、基于springboot优化消息发送消费示例

pom文件

这里rocketmq-client版本要跟服务端的版本一致

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String id;
    private String username;
}

rockermq相关参数配置文件

rocketmq.properties

# 指定namesrv地址
suning.rocketmq.namesrvAddr=127.0.0.1:9876
#生产者group名称
suning.rocketmq.producerGroupName=user_group
#事务生产者group名称
suning.rocketmq.transactionProducerGroupName=order_transaction
#消费者group名称
suning.rocketmq.consumerGroupName=user_consumer_group
#生产者实例名称
suning.rocketmq.producerInstanceName=user_producer_instance
#消费者实例名称
suning.rocketmq.consumerInstanceName=user_consumer_instance
#事务生产者实例名称
suning.rocketmq.producerTranInstanceName=user_producer_transacition
#一次最大消费多少数量消息
suning.rocketmq.consumerBatchMaxSize=1
#广播消费
suning.rocketmq.consumerBroadcasting=false
#消费的topic:tag
suning.rocketmq.subscribe[0]=user-topic:white
#启动的时候是否消费历史记录
suning.rocketmq.enableHistoryConsumer=false
#启动顺序消费
suning.rocketmq.enableOrderConsumer=false

配置类

RocketMQProperties.java

package com.hl.springbootrocketmq.api.config;
 
import java.util.ArrayList;
import java.util.List;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

//@PropertySource:指定要加载的配置文件路径,其实默认是可以不写的,如果是加载classpath下面的配置文件,因为它会自己去寻找,但是有时候不同的版本,不写的话又会出现错误,所以为了不出现错误,通常建议配置一下,这样肯定不会有错的
//@ConfigurationProperties:指定读取配置文件的规则,比如前缀是什么,不存在的字段是否可以忽略等
@PropertySource("classpath:config/rocketmq.properties")
@ConfigurationProperties(prefix = "suning.rocketmq")
@Configuration
@Data
public class RocketMQProperties {
   private String namesrvAddr;
   private String producerGroupName;
   private String transactionProducerGroupName;
   private String consumerGroupName;
   private String producerInstanceName;
   private String consumerInstanceName;
   private String producerTranInstanceName;
   private int consumerBatchMaxSize;
   private boolean consumerBroadcasting;
   private boolean enableHistoryConsumer;
   private boolean enableOrderConsumer;
   private List<String> subscribe = new ArrayList<String>();
 
}

RocketMQConfiguration.java

package com.hl.springbootrocketmq.api.config;
 
import javax.annotation.PostConstruct;

import com.hl.springbootrocketmq.api.rocketmqbean.MessageEvent;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.List;
import java.util.stream.Collectors;

@Configuration
@EnableConfigurationProperties(RocketMQProperties.class)
//@EnableConfigurationProperties:启动自动配置文件属性的获取,通过指定的类
@Slf4j
public class RocketMQConfiguration {
 
   @Autowired
   private RocketMQProperties rocketMQProperties;
 
   //事件监听
   @Autowired
   private final ApplicationEventPublisher publisher = null;
 
   private static boolean isFirstSub = true;
 
   private static final long startTime = System.currentTimeMillis();
 
    /**
     * 容器初始化的时候 打印参数
     */
   @PostConstruct
   public void init() {
      System.err.println(rocketMQProperties.getNamesrvAddr());
      System.err.println(rocketMQProperties.getProducerGroupName());
      System.err.println(rocketMQProperties.getConsumerBatchMaxSize());
      System.err.println(rocketMQProperties.getConsumerGroupName());
      System.err.println(rocketMQProperties.getConsumerInstanceName());
      System.err.println(rocketMQProperties.getProducerInstanceName());
      System.err.println(rocketMQProperties.getProducerTranInstanceName());
      System.err.println(rocketMQProperties.getTransactionProducerGroupName());
      System.err.println(rocketMQProperties.isConsumerBroadcasting());
      System.err.println(rocketMQProperties.isEnableHistoryConsumer());
      System.err.println(rocketMQProperties.isEnableOrderConsumer());
      System.out.println(rocketMQProperties.getSubscribe().get(0));
   }
 
   /**
    * 创建普通消息发送者实例
    * @return
    * @throws MQClientException
    */
   @Bean
   @ConditionalOnClass(DefaultMQProducer.class)
   @ConditionalOnMissingBean(DefaultMQProducer.class)
   @ConditionalOnProperty(prefix = "suning.rocketmq",value = "namesrvAddr")
   public DefaultMQProducer defaultProducer() throws MQClientException {
      DefaultMQProducer producer = new DefaultMQProducer(
            rocketMQProperties.getProducerGroupName());
      producer.setNamesrvAddr(rocketMQProperties.getNamesrvAddr());
      producer.setInstanceName(rocketMQProperties.getProducerInstanceName());
      producer.setVipChannelEnabled(false);
      producer.setRetryTimesWhenSendAsyncFailed(10);
      producer.start();
      log.info("rocketmq producer server is starting....");
      return producer;
   }
 
   /**
    * 创建支持消息事务发送的实例
    * @return
    * @throws MQClientException
    */
   @Bean
   @ConditionalOnProperty(prefix = "suning.rocketmq",value = "transactionProducerGroupName")
   @ConditionalOnClass(TransactionMQProducer.class)
   @ConditionalOnMissingBean(TransactionMQProducer.class)
   public TransactionMQProducer transactionProducer() throws MQClientException {
      TransactionMQProducer producer = new TransactionMQProducer(
            rocketMQProperties.getTransactionProducerGroupName());
      producer.setNamesrvAddr(rocketMQProperties.getNamesrvAddr());
      producer.setInstanceName(rocketMQProperties
            .getProducerTranInstanceName());
      producer.setRetryTimesWhenSendAsyncFailed(10);
      // 事务回查最小并发数
      producer.setCheckThreadPoolMinSize(2);
      // 事务回查最大并发数
      producer.setCheckThreadPoolMaxSize(2);
      // 队列数
      producer.setCheckRequestHoldMax(2000);
      producer.start();
      log.info("rocketmq transaction producer server is starting....");
      return producer;
   }
 
   /**
    * 创建消息消费的实例
    * @return
    * @throws MQClientException
    */
   @Bean
   @ConditionalOnProperty(prefix = "suning.rocketmq",value = "consumerGroupName")
   @ConditionalOnClass(DefaultMQPushConsumer.class)
   @ConditionalOnMissingBean(DefaultMQPushConsumer.class)
   public DefaultMQPushConsumer pushConsumer() throws MQClientException {
      DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(
            rocketMQProperties.getConsumerGroupName());
      consumer.setNamesrvAddr(rocketMQProperties.getNamesrvAddr());
      consumer.setInstanceName(rocketMQProperties.getConsumerInstanceName());
 
      //判断是否是广播模式
      if (rocketMQProperties.isConsumerBroadcasting()) {
         consumer.setMessageModel(MessageModel.BROADCASTING);
      }
      //设置批量消费
      consumer.setConsumeMessageBatchMaxSize(rocketMQProperties
            .getConsumerBatchMaxSize() == 0 ? 1 : rocketMQProperties
            .getConsumerBatchMaxSize());
 
      //获取topic和tag
      List<String> subscribeList = rocketMQProperties.getSubscribe();
      for (String sunscribe : subscribeList) {
         consumer.subscribe(sunscribe.split(":")[0], sunscribe.split(":")[1]);
      }
 
      // 顺序消费
      if (rocketMQProperties.isEnableOrderConsumer()) {
         consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(
                  List<MessageExt> msgs, ConsumeOrderlyContext context) {
               try {
                  context.setAutoCommit(true);
                  msgs = filterMessage(msgs);
                  if (msgs.size() == 0)
                     return ConsumeOrderlyStatus.SUCCESS;
                  publisher.publishEvent(new MessageEvent(msgs, consumer));
               } catch (Exception e) {
                  e.printStackTrace();
                  return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
               }
               return ConsumeOrderlyStatus.SUCCESS;
            }
         });
      }
      // 并发消费
      else {
 
         consumer.registerMessageListener(new MessageListenerConcurrently() {
 
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(
                  List<MessageExt> msgs,
                  ConsumeConcurrentlyContext context) {
               try {
                   //过滤消息
                  msgs = filterMessage(msgs);
                  if (msgs.size() == 0)
                     return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                  publisher.publishEvent(new MessageEvent(msgs, consumer));
               } catch (Exception e) {
                  e.printStackTrace();
                  return ConsumeConcurrentlyStatus.RECONSUME_LATER;
               }
 
               return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
         });
      }
      new Thread(new Runnable() {
         @Override
         public void run() {
            try {
               Thread.sleep(5000);
 
               try {
                  consumer.start();
               } catch (Exception e) {
                        e.printStackTrace();
               }
               log.info("rocketmq consumer server is starting....");
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
 
      }).start();
 
      return consumer;
   }
 
    /**
     * 消息过滤
     * @param msgs
     * @return
     */
   private List<MessageExt> filterMessage(List<MessageExt> msgs) {
      if (isFirstSub && !rocketMQProperties.isEnableHistoryConsumer()) {
         msgs = msgs.stream()
               .filter(item -> startTime - item.getBornTimestamp() < 0)
               .collect(Collectors.toList());
      }
      if (isFirstSub && msgs.size() > 0) {
         isFirstSub = false;
      }
      return msgs;
   }
}

rocketmqBean

package com.hl.springbootrocketmq.api.rocketmqbean;
 
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.context.ApplicationEvent;

import java.util.List;
/**
 * 监听对象
 * @author hl
 * @date 2022/2/23 10:04
 */
public class MessageEvent extends ApplicationEvent {
   private static final long serialVersionUID = -4468405250074063206L;
   private DefaultMQPushConsumer consumer;
   private List<MessageExt> msgs;
 
   public MessageEvent(List<MessageExt> msgs, DefaultMQPushConsumer consumer) throws Exception {
       super(msgs);
       this.consumer = consumer;
       this.setMsgs(msgs);
   }
 
 
 
   public DefaultMQPushConsumer getConsumer() {
       return consumer;
   }
 
   public void setConsumer(DefaultMQPushConsumer consumer) {
       this.consumer = consumer;
   }
   
   public List<MessageExt> getMsgs() {
       return msgs;
   }
 
   public void setMsgs(List<MessageExt> msgs) {
       this.msgs = msgs;
   }
}

controller

package com.hl.springbootrocketmq.api.controller;
 

import com.hl.springbootrocketmq.api.pojo.User;
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import com.alibaba.fastjson.JSON;
 
 
 
import java.util.List;
 
@RestController
public class ProducerController {
   @Autowired
   private DefaultMQProducer defaultProducer;
 
   @Autowired
   private TransactionMQProducer transactionProducer;
 
 
    /**
     * 发送普通消息
     */
   @GetMapping("/sendMessage")
   public void sendMsg() {
      
      for(int i=0;i<100;i++){
         User user = new User();
         user.setId(String.valueOf(i));
         user.setUsername("jhp"+i);
         String json = JSON.toJSONString(user);
         Message msg = new Message("user-topic","white",json.getBytes());
         try {
            SendResult result = defaultProducer.send(msg);
            System.out.println("消息id:"+result.getMsgId()+":"+","+"发送状态:"+result.getSendStatus());
      } catch (Exception e) {
         e.printStackTrace();
      }
      }
      
      
   }
 
    /**
     * 发送事务消息
     * @return
     */
   @GetMapping("/sendTransactionMess")
   public String sendTransactionMsg() {
       SendResult sendResult = null;
       try {
           // a,b,c三个值对应三个不同的状态
           String ms = "c";
           Message msg = new Message("user-topic","white",ms.getBytes());
           // 发送事务消息
           sendResult = transactionProducer.sendMessageInTransaction(msg, (Message msg1, Object arg) -> {
               String value = "";
               if (arg instanceof String) {
                   value = (String) arg;
               }
               if (value.equals("")) {
                   throw new RuntimeException("发送消息不能为空...");
               } else if (value =="a") {
                   return LocalTransactionState.ROLLBACK_MESSAGE;
               } else if (value =="b") {
                   return LocalTransactionState.COMMIT_MESSAGE;
               }
               return LocalTransactionState.ROLLBACK_MESSAGE;
           }, 4);
           System.out.println(sendResult);
       } catch (Exception e) {
           e.printStackTrace();
       }
       return sendResult.toString();
   }
 
    /**
     * 支持顺序发送消息
     */
  @GetMapping("/sendMessOrder")
   public void sendMsgOrder() {
      for(int i=0;i<100;i++) {
          User user = new User();
          user.setId(String.valueOf(i));
          user.setUsername("jhp" + i);
          String json = JSON.toJSONString(user);
          Message msg = new Message("user-topic", "white", json.getBytes());
          try{
              defaultProducer.send(msg, new MessageQueueSelector() {
                  @Override
                  public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                      int index = ((Integer) arg) % mqs.size();
                      return mqs.get(index);
                  }
              },i);
          }
          catch (Exception e){
              e.printStackTrace();
          }
      }
   }
}

消费者

package com.hl.springbootrocketmq.api.consumer;
 
import java.util.List;


import com.hl.springbootrocketmq.api.rocketmqbean.MessageEvent;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
/**
 * 监听消息进行消费
 */
@Component
public class ConsumerService {
   @EventListener(condition = "#event.msgs[0].topic=='user-topic' && #event.msgs[0].tags=='white'")
   public void rocketmqMsgListener(MessageEvent event) {
       try {
          List<MessageExt> msgs = event.getMsgs();
          for (MessageExt msg : msgs) {
               System.err.println("消费消息:"+new String(msg.getBody()));
           }
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}

启动类

@SpringBootApplication
@ComponentScan(basePackages = "com.hl.springbootrocketmq.api")
public class SpringbootRocketmqApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootRocketmqApplication.class, args);
    }

}

测试

然后访问浏览器端 http://localhost:8080/sendMessage 效果如下截图所示

在这里插入图片描述

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值