RocketMq编程模型即生产环境实现

91 阅读7分钟

RocketMQ编程模式的实现

RocketMQ消息模型:在开发中围绕这个消息模型设置不同的生产者和消费者。

image.png

rocketMQ存消息的方式:messageQueue,通过ofset来标识自己存储了哪些消息。位点-----消息的偏移量,或者说消息的编号

客户端发送消息的流程

  • 在RocketMQ中提供了一个DefaultMQProducer对象,客户端通过这个发就可以了

    • 声明一个生产者
    • 知名一个生产者所属的组(可以有多个生产者属于相同的组,同一个组下面的生产者业务逻辑相同)
  • 指明具体的NameServer----NameServer会记录具体的Broker,只要直到NameServer在哪,就可以从NameServer上面获得所有的Broker地址列表,客户端就可以选择一个NameServer发送消息。

  • 调用start()方法保存producer的状态,可以和Broker进行多次的交互,发一次消息其实也是和Broker会有多次的请求交互的

  • 发送消息的方式:

    • sendoneway()性能最高但是可靠性低
    • sendResult----同步发送
    • 异步发送-----会异步接收消息发送返回的结果,使用最多
    • //有个sendCallBack()接口来实现发送成功或者失败后的回调处理,成功怎么办,失败了怎么办
      producer.send(msg,new SendCallBack(){
          @Override
          public void Onsuccess(SendResult sendResult){
              countDownLatch.countDown();
              System.out.println("%-10d ok %s %n",index,sendResult.getMsgId()); 
          }
      })
          .....
          ....
      

    消费者端:broker推模式和拉取模式,一般用的是推模式,因为不确定是否能拉到消息。

    广播消息

    ⼴播模式和集群模式是RocketMQ的消费者端处理消息最基本的两种模式。集群模式下,⼀个消息,只会被⼀个消费者组中的多个消费者实例 共同 处理⼀次。⼴播模式下,⼀个消息,则会推送给所有消费者实例处理,不再关⼼消费者组。

  • 示例代码:

消费者端:

consumer.setMessageModel(MessageModel.BROADCASTING);

启动多个消费者,⼴播模式下,这些消费者都会消费⼀次消息。

默认模式(也就是集群模式)下,Broker端会给每个ConsumerGroup维护⼀个统⼀的Offset,这个Offset可以保证

⼀个消息,在同⼀个ConsumerGroup内只会被消费⼀次。⽽⼴播模式的实现⽅式,是将Offset转移到消费者端⾃

⾏保管,这样Broker端只管向所有消费者推送消息,⽽不⽤负责维护消费进度。

  • 注意点:

1、Broker端不维护消费进度,意味着,如果消费者处理消息失败了,将⽆法进⾏消息重试。

2、消费者端维护Offset的作⽤是可以在服务重启时,按照上⼀次消费的进度,处理后⾯没有消费过的消息。丢了也不影响服务稳定性。

RocketMQ会通过定时任务不断尝试本地Offsets⽂件的写⼊,但是,如果本地Offsets⽂件写⼊失败,RocketMQ不会进⾏任何的补救。

顺序消费

  • 应用场景

每⼀个订单有从下单、锁库存、⽀付、下物流等⼏个业务步骤。每个业务步骤都由⼀个消息⽣产者通知给下游服务。如何保证对每个订单的业务处理顺序不乱很关键。

  • 示例代码
for (int i = 0; i < 10; i++) {

int orderId = i;

for(int j = 0 ; j <= 5 ; j ++){

Message msg =

new Message("OrderTopicTest", "order_"+orderId, "KEY" +

orderId,

("order_"+orderId+" step " +

j).getBytes(RemotingHelper.DEFAULT_CHARSET));

SendResult sendResult = producer.send(msg, new

MessageQueueSelector() {

@Override

public MessageQueue select(List<MessageQueue> mqs, Message msg,

Object arg) {

Integer id = (Integer) arg;

int index = id % mqs.size();

return mqs.get(index);

}

}, orderId);

System.out.printf("%s%n", sendResult);
}
}

通过MessageSelector--(消息选择器),将orderId相同的消息,都转发到同⼀个MessageQueue中

订单号相同的消息放到消息队列中

消费者核心代码:

consumer.registerMessageListener(new MessageListenerOrderly() {

@Override

public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,

ConsumeOrderlyContext context) {

context.setAutoCommit(true);

for(MessageExt msg:msgs){

System.out.println("收到消息内容 "+new String(msg.getBody()));

}

return ConsumeOrderlyStatus.SUCCESS;

}

});

注⼊⼀个MessageListenerOrderly实现。

  • 实现思路:

基础思路:只有放到⼀起的⼀批消息,才有可能保持消息的顺序。

image.png

1、⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,Broker才有可能保持这⼀批消息的顺

序。

2、消费者只有⼀次锁定⼀个MessageQueue,拿到MessageQueue上所有的消息,

注意点:

1、理解局部有序与全局有序。⼤部分业务场景下,我们需要的其实是局部有序。如果要保持全局有序,那就只

保留⼀个MessageQueue。性能显然⾮常低。

2、⽣产者端尽可能将有序消息打散到不同的MessageQueue上,避免过于集中导致数据热点竞争 2、消费者端只能⽤同步的⽅式处理消息,不要使⽤异步处理。更不能⾃⾏使⽤批量处理。

3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,**如果消费者⼀直处理失败,超过最⼤重试次数,**那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。

4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回

ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT作为替代。

延迟消息

  • 应⽤场景:

延迟消息发送是指消息发送到Apache RocketMQ后,并不期望⽴⻢投递这条消息,⽽是延迟⼀定时间后才投递到Consumer进⾏消费。

相比于RabbitMq和Kafka,RocketMq这一特性是比较独特的,RabbitMQ使用死信队列来变相的实现延迟消息,Kafka则不太好实现延时消息。

  • 示例代码:

生产端核心代码

msg.setDelayTimeLevel(3)

给msg消息设置一个延迟级别就行了,在RocketMq中定制了18个默认的延迟级别,分别对应18个不同的预设好的延时时间

image.png

  • 实现思路

RocketMq实现延时消息的难点主要在于性能方面,需要不断的进行定时轮询,全部扫描所有的消息是不可能的。因此可以考虑根据设定的18个延时级别,预设⼀个系统Topic,名字叫做SCHEDULE_TOPIC_XXXX。在这个Topic下,预设18个延迟队列。然后每次只针对这18个队列⾥的消息进⾏延迟操作,这样就不⽤⼀直扫描所有的消息了。

批量消息

⽣产者要发送的消息⽐较多时,可以将多条消息合并成⼀个批量消息,⼀次性发送出去。这样可以减少⽹络IO,提升消息发送的吞吐量。

示例代码:

List<Message> messages = new ArrayList<>();

messages.add(new Message(topic, "Tag", "OrderID001", "Hello world

0".getBytes()));

messages.add(new Message(topic, "Tag", "OrderID002", "Hello world

1".getBytes()));

messages.add(new Message(topic, "Tag", "OrderID003", "Hello world

2".getBytes()));

producer.send(messages);
  • 注意点

批量消息的使用还是比较简单的,但是RokectMq做了限制,同一批消息的主题必须相同(避免消息混乱)且不支持延迟消息.还有批量消息的⼤⼩不要超过1M,如果太⼤就需要⾃⾏分割。

过滤消息

  • 应⽤场景:

同⼀个Topic下有多种不同的消息,消费者只希望关注某⼀类消息。

例如,某系统中给仓储系统分配⼀个Topic,在Topic下,会传递过来⼊库、出库等不同的消息,仓储系统的不同业务消费者就需要过滤出⾃⼰感兴趣的消息,进⾏不同的业务操作。

image.png

还可以进行sql过滤。

  • 实例代码实现简单的过滤
String[] tags = new String[] {"TagA", "TagB", "TagC"};

for (int i = 0; i < 15; i++) {

Message msg = new Message("TagFilterTest",

tags[i % tags.length],

"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

SendResult sendResult = producer.send(msg);

System.out.printf("%s%n", sendResult);

}

消费者端就可以通过这个Tag属性订阅⾃⼰感兴趣的内容。核⼼代码:

consumer.subscribe("TagFilterTest", "TagA");

这样消费者就只会处理TagA的消息

  • 示例代码2:SQL过滤

通过Tag属性,只能进⾏简单的消息匹配。如果要进⾏更复杂的消息过滤,⽐如数字⽐较,模糊匹配等,就需要

使⽤SQL过滤⽅式。SQL过滤⽅式可以通过Tag属性以及⽤户⾃定义的属性⼀起,以标准SQL的⽅式进⾏消息过滤。

⽣产者端在发送消息时,出了Tag属性外,还可以增加⾃定义属性。核⼼代码:

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

for (int i = 0; i < 15; i++) {

Message msg = new Message("SqlFilterTest",

tags[i % tags.length],

("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)

);

msg.putUserProperty("a", String.valueOf(i));

SendResult sendResult = producer.send(msg);

System.out.printf("%s%n", sendResult);

}

消费者端在进行过滤的时候会指定一个标准的sql语句,定制复杂的sql语句-----跟语句有什么关联?

consumer.subscribe("SqlFilterTest",

MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +

"and (a is not null and a between 0 and 3)"));