18、Rocket MQ核⼼编程模型

目录

⼀ 、回顾Rocket MQ的运⾏架构

⼆ 、深⼊理解Rocket MQ的消息模型

1 、Rocket MQ客户端基本流程

  (1)消息⽣产者的固定步骤

  (2)消息消费者的固定步骤

2 、消息确认机制

(1)消息⽣产端采⽤消息确认加多次重试的机制保证消息正常发送到RocketMQ

第⼀种称为单向发送

第⼆种称为同步发送

第三种称为异步发送

(2)消息消费者端采⽤状态确认机制保证消费者⼀定能正常处理对应的消息

(3)消费者组也可以⾃⾏指定起始消费位点

3 、⼴播消息

4 、过滤消息

1)应⽤场景:

5 、顺序消息机制

6 、延迟消息

7 、批量消息

8 、事务消息

9 、ACL权限控制机制

三、SpringBoot整合Rocket MQ

1 、快速实战

2 、如何处理各种消息类型

3 、实现原理

(1)RocketMQTemplate

(2)Push模式消费者

(3)Pull模式

四、Rocket MQ客户端注意事项

1 、消息的ID, Key和Tag

2 、最佳实践

3 、消费者端进⾏幂等控制

4 、关注错误消息重试

5 、⼿动处理死信队列


 、回顾Rocket MQ的运⾏架构

同时还总结出了RocketMQ的消息模型。

 这是我们使⽤RocketMQ时最直接的指导。这—章节,我们就来看下,在这两张图的基础上,如何编写合适的客户端代码,让我们在项⽬中⽤好 RocketMQ

 、深⼊理解Rocket MQ的消息模型

1 Rocket MQ客户端基本流程

RocketMQ基于Maven提供了客户端的核⼼依赖:

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

—个最为简单的消息⽣产者代码如下:

public class Producer {
public static void main(String [] args) throws MQClientException, InterruptedException {
//初始化—个消息⽣产者
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // 指定nameserver地址
producer.setNames rvAddr("192.168.65.112:9876");
// 启动消息⽣产者服务
producer.start();
for (int i = 0; i < 2; i++) {
try {
// 创建消息 。消息由Topic,Tag和body三个属性组成 ,其中Body就是消息内容   Message msg = new Message("TopicTest","TagA",("Hello RocketMQ
" +i).getBytes(RemotingHelper.DEFAULT_CHARSET));
//发送消息 ,获取发送结果
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
//消息发送完后 ,停⽌消息⽣产者服务。
producer.shutdown();
}
}

 —个简单的消息消费者代码如下:

public class Consumer {
public static void main(String [] args) throws InterruptedException, MQClientException {
//构建—个消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
//指定nameserver地址
consumer.setNames rvAddr("192.168.65.112:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 订阅—个感兴趣的话题 ,这个话题需要与消息的topic—致
consumer.subscribe("TopicTest", "*");
// 注册—个消息回调函数 ,消费到消息后就会触发回调。
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {
msgs.forEach(messageExt -> {
try {
System.out.println("收到消息 :"+new String(messageExt.getBody(),
RemotingHelper.DEFAULT_CHARSET));
} catch (UnsupportedEncodingException e) {}
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动消费者服务
consumer.start();
System.out.print("Consumer Started");
}
}

RocketMQ的客户端编程模型相对⽐较固定,基本都有—个固定的步骤 。掌握这个固定步骤,对于学习其他复杂的消息模型也是很有帮助的。 

  (1)消息⽣产者的固定步骤

1.创建消息⽣产者producer ,并指定⽣产者组名

2.指定Nameserver地址

3.启动producer 。 这个步骤⽐较容易忘记 。可以认为这是消息⽣产者与服务端建⽴连接的过程。

4.创建消息对象,指定主题Topic Tag和消息体

5.发送消息

6.关闭⽣产者producer ,释放资源。

  (2)消息消费者的固定步骤

1.创建消费者Consumer ,必须指定消费者组名

2.指定Nameserver地址

3.订阅主题TopicTag

4.设置回调函数,处理消息

5.启动消费者consumer 。消费者会—直挂起,持续处理消息。

其中,最为关键的就是NameServer 。从示例中可以看到, RocketMQ的客户端只需要指定NameServer地址,⽽不需要指定具体的Broker 址。

指定NameServer的⽅式有两种。可以在客户端直接指定,例如 consumer.setNameSrvAddr("127.0.0.1:9876")。然后,也可以通过读取系统环 境变量NAMESRV_ADDR指定 。其中第—种⽅式的优先级更⾼。

2 、消息确认机制

RocketMQ要⽀持互联⽹⾦融场景,那么消息安全是必须优先保障的。⽽消息安全有两⽅⾯的要求,—⽅⾯是⽣产者要能确保将消息发送到 Broker 。另—⽅⾯是消费者要能确保从Broker上争取获取到消息。

(1)消息⽣产端采⽤消息确认加多次重试的机制保证消息正常发送到RocketMQ

针对消息发送的不确定性,封装了三种发送消息的⽅式。

第⼀种称为单向发送

单向发送⽅式下,消息⽣产者只管往Broker发送消息,⽽全然不关⼼Broker端有没有成功接收到消息。这就好⽐⽣产者向Broker发—封电⼦邮 件, Broker有没有处理电⼦邮件,⽣产者并不知道

public class OnewayProducer {
public static void main(String [] args)throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
producer.start();
Message message = new Message("Order","tag","order info : orderId = xxx".getBytes(StandardCharsets.UTF_8));
producer.sendOneway(message);
Thread.sleep(50000);
producer.shutdown();
}
}

sendOneway⽅法没有返回值,如果发送失败,⽣产者⽆法补救。

单向发送有—个好处,就是发送消息的效率更⾼ 。适⽤于—些追求消息发送效率,⽽允许消息丢失的业务场景 。⽐如⽇志。

第⼆种称为同步发送

同步发送⽅式下,消息⽣产者在往Broker端发送消息后,会阻塞当前线程,等待Broker端的相应结果。这就好⽐⽣产者给Broker打了个电话。 通话期间⽣产者就停下⼿头的事情,直到Broker明确表示消息处理成功了,⽣产者才继续做其他的事情。

SendResult sendResult = producer.send(msg);

SendResult来⾃于Broker的反馈 。producer在send发出消息,到Broker返回SendResult的过程中,⽆法做其他的事情。
在SendResult中有—个SendStatus属性,这个SendStatus是—个枚举类型,其中包含了Broker端的各种情况。

public enum SendStatus {
SEND_OK,
FLUSH_DISK_TIMEOUT,
FLUSH_SLAVE_TIMEOUT,
SLAVE_NOT_AVAILABLE, }

在这⼏种枚举值中,SEND_OK表示消息已经成功发送到Broker 。⾄于其他⼏种枚举值,都是表示消息在Broker端处理失败了 。使⽤同步发送 的机制,我们就可以在消息⽣产者发送完消息后,对发送失败的消息进⾏补救 。例如重新发送。

但是此时要注意,如果Broker端返回的SendStatus不是SEND_OK ,也并不表示消息就—定不会推送给下游的消费者 。仅仅只是表示Broker端并 没有完全正确的处理这些消息 。因此,如果要重新发送消息,最好要带上唯—的系统标识,这样在消费者端,才能⾃⾏做幂等判断 。也就是⽤具  有业务含义的OrderID这样的字段来判断消息有没有被重复处理。

这种同步发送的机制能够很⼤程度上保证消息发送的安全性 。但是,这种同步发送机制的发送效率⽐较低 。毕竟,send⽅法需要消息在⽣产者 Broker之间传输—个来回后才能结束 。如果⽹速⽐较慢 同步发送的耗时就会很⻓。

第三种称为异步发送

异步发送机制下,⽣产者在向Broker发送消息时,会同时注册—个回调函数。接下来⽣产者并不等待Broker的响应 。当Broker端有响应数据过   来时, ⾃动触发回调函数进⾏对应的处理。这就好⽐⽣产者向Broker发电⼦邮件通知时,另外找了—个代理⼈专⻔等待Broker的响应 。⽽⽣产者 ⾃⼰则发完消息后就去做其他的事情去了。

producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
countDownLatch.countDown();
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});

SendCallback接⼝中有两个⽅法,onSuccessonException 。当Broker端返回消息处理成功的响应信息SendResult时,就会调⽤     onSuccess⽅法 。当Broker端处理消息超时或者失败时,就会调⽤onExcetion⽅法,⽣产者就可以在onException⽅法中进⾏补救措施。

此时同样有⼏个问题需要注意 。—是与同步发送机制类似,触发了SendCallbackonException⽅法同样并不—定就表示消息不会向消费者推  。如果Broker端返回响应信息太慢,超过了超时时间,也会触发onException⽅法 。超时时间默认是3秒,可以通过producer.setSendMsgTimeout⽅法定制。⽽造成超时的原因则有很多,消息太⼤造成⽹络拥堵 、⽹速太慢 Broker端处理太慢等都可能造成 消息处理超时。

⼆是在SendCallback的对应⽅法被触发之前,⽣产者不能调⽤shutdown()⽅法 。如果消息处理完之前,⽣产者线程就关闭了,⽣产者的

SendCallback对应⽅法就不会触发。这是因为使⽤异步发送机制后,⽣产者虽然不⽤阻塞下来等待Broker端响应,但是SendCallback还是需要 附属于⽣产者的主线程才能执⾏ 。如果Broker端还没有返回SendResult ,⽽⽣产者主线程已经停⽌了,那么SendCallback的执⾏线程也就会随 主线程—起停⽌ ,对应的⽅法⾃然也就⽆法执⾏了。

这种异步发送的机制能够⽐较好的兼容消息的安全性以及⽣产者的⾼吞吐需求,是很多MQ产品都⽀持的⽅式 RabbitMQKafka都⽀持这种异 步发送的机制。但是异步发送机制也并不是万能的,毕竟异步发送机制对消息⽣产者的主线业务是有侵⼊的。 具体使⽤时还是需要根据业务场景 考虑。

RocketMQ提供的这三种发送消息的⽅式,并不存在绝对的好坏之分 。我们更多的是需要根据业务场景进⾏选择 。例如在电商下单这个场景,我 们就应该尽量选择同步发送或异步发送,优先保证数据安全 。然后,如果下单场景的并发⽐较⾼ ,业务⽐较繁忙,就应该尽量优先选择异步发送 的机制。这时,我们就应该对下单服务的业务进⾏优化定制,尽量适应异步发送机制的要求。这样就可以尽量保证下单服务能够⽐较可靠的将⽤  户的订单消息发送到RocketMQ了。

(2)消息消费者端采⽤状态确认机制保证消费者⼀定能正常处理对应的消息

我们之前分析⽣产者的可靠性问题,核⼼的解决思路就是通过确认Broker端的状态来保证⽣产者发送消息的可靠性。对RocketMQ的消费者来 说,保证消息处理可靠性的思路也是类似的。只不过这次换成了Broker等待消费者返回消息处理状态。 

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;
}
});

这个返回值是—个枚举值,有两个选项 CONSUME_SUCCESSRECONSUME_LATER 。如果消费者返回CONSUME_SUCCESS ,那么消息⾃ 然就处理结束了 。但是如果消费者没有处理成功,返回的是RECONSUME_LATER Broker就会过—段时间再发起消息重试。

为了要兼容重试机制的成功率和性能, RocketMQ设计了—套⾮常完善的消息重试机制,从⽽尽可能保证消费者能够正常处理⽤户的订单信息。

1 Broker不可能⽆限制的向消费失败的消费者推送消息 。如果消费者—直没有恢复, Broker显然不可能—直⽆限制的推送,这会浪费集群很多  的性能 。所以, Broker会记录每—个消息的重试次数。如果—个消息经过很多次重试后,消费者依然⽆法正常处理,那么Broker会将这个消息推 ⼊到消费者组对应的死信Topic中。死信Topic相当于windows当中的垃圾桶 。你可以⼈⼯介⼊对死信Topic中的消息进⾏补救,也可以直接彻底   删除这些消息 RocketMQ默认的最⼤重试次数是16次。

2、为了让这些重试的消息不会影响Topic下其他正常的消息, Broker会给每个消费者组设计对应的重试Topic MessageQueue是—个具有严格 FIFO特性的数据结构 。如果需要重试的这些消息还是放在原来的MessageQueue中,就会对当前MessageQueue产⽣阻塞,让其他正常的消息   ⽆法处理 RocketMQ的做法是给每个消费者组⾃动⽣成—个对应的重试Topic。在消息需要重试时,会先移动到对应的重试Topic中。后续

Broker只要从这些重试Topic中不断拿出消息,往消费者组重新推送即可。这样,这些重试的消息有了⾃⼰单独的队列,就不会影响到Topic下的 其他消息了。

3 RocketMQ中设定的消费者组都是订阅主题和消费逻辑相同的服务备份,所以当消息重试时, Broker只要往消费者组中随意—个实例推送即 可。这是消息重试机制能够正常运⾏的基础 。但是,在客户端的具体实现时, MQDefaultMQConsumer并没有强制规定消费者组不能重复 。也   就是说,你完全可以实现出—些订阅主题和消费逻辑完全不同的消费者服务,共同组成—个消费组。在这种情况下, RocketMQ不会报错,但是 消息的处理逻辑就⽆法保持—致了。这会给业务带来很⼤的麻烦。这是在实际应⽤时需要注意的地⽅。

4 Broker端最终只通过消费者组返回的状态来确定消息有没有处理成功 。⾄于消费者组⾃⼰的业务执⾏是否正常, Broker端是没有办法知道的。因此,在实现消费者的业务逻辑时,应该要尽量使⽤同步实现⽅式,保证在⾃⼰业务处理完成之后再向Broker端返回状态 。⽽应该尽量避免 异步的⽅式处理业务逻辑。

(3)消费者组也可以⾃⾏指定起始消费位点

Broker端通过Consumer返回的状态来推进所属消费者组对应的Offset 。但是,这⾥还是会造成—种分裂,消息最终是由Consumer来处理,但  是消息却是由Broker推送过来的,也就是说,Consumer⽆法确定⾃⼰将要处理的是哪些消息。这就好⽐你上班做—天事情,公司负责给你发— 笔⼯资 。如果—切正常,那么没什么问题  但是如果出问题了呢?公司拖⽋了你的⼯资,这时,你就还是需要能到公司查账,⾄少查你⾃⼰的   ⼯资记录 。从上—次发⼯资的时候计算你该拿的钱。

使⽤消息对列要如何解决这样的问题呢?这时,就可以创建另外—个新的消费者组,并通过ConsumerFromWhere属性指定这个消费者组的消费 起点,从⽽让这个新的消费者组去消费之前发送过的历史消息 。⽽这个ConsumerFromWhere属性并不是直接指定Offset的数值, 因为客户端也 不知道Broker端记录的Offset数值是多少 RocketMQ就提供了—个枚举值 。名字一⽬了然。

public enum ConsumeFromWhere {
CONSUME_FROM_LAST_OFFSET, //从对列的最后—条消息开始消费
CONSUME_FROM_FIRST_OFFSET, //从对列的第—条消息开始消费
CONSUME_FROM_TIMESTAMP; //从某—个时间点开始重新消费 }

 另外,如果指定了ConsumerFromWhere.CONSUME_FROM_TIMESTAMP ,这就表示要从—个具体的时间开始 。具体时间点,需要通过 Consumer的另—个属性ConsumerTimestamp。这个属性可以传⼊—个表示时间的字符串。

consumer.setConsumerTimestamp("20131223171201");

 到这⾥ ,我们就从客户端的⻆度分析清楚了要如何保证消息的安全性 。但是消息安全问题其实是—个⾮常体系化的问题,涉及到的不光是客户 端,还需要服务端配合 。关于这个问题,我们会在后⾯的分享过程当中继续带你—起思考。

3 、⼴播消息

应⽤场景:

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

示例代码:

消费者核⼼代码

consumer.setMessageModel(MessageModel.BROADCASTING);

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

实现思路:

默认模式(也就是集群模式)下, Broker端会给每个ConsumerGroup维护—个统—的Offset ,这样, Consumer来拉取消息时,就可以通过Offset保证—个消息,在同—个ConsumerGroup内只会被消费—次 。⽽⼴播模式的本质,是将Offset转移到Consumer端⾃⾏保管,包括Offset 的记录以及更新,全都放到客户端。这样Broker推送消息时,就不再管ConsumerGroup ,只要Consumer来拉取消息,就返回对应的消息。

注意点:

  1. 1 Broker端不维护消费进度,意味着,如果消费者处理消息失败了,将⽆法进⾏消息重试。
  2. 2 Consumer端维护Offset的作⽤是可以在服务重启时,按照上—次消费的进度,处理后⾯没有消费过的消息 如果Offset丢了,Consuer依然 可以拉取消息。

⽐如⽣产者发送了1 ~10号消息 。消费者当消费到第6个时宕机了 。当他重启时, Broker端已经把第10个消息都推送完成了 。如果消费者端维护好 了⾃⼰的Offset ,那么他就可以在服务重启时,重新向Broker申请6号到10号的消息 。但是,如果消费者端的Offset丢失了,消费者服务依然可   以正常运⾏ ,但是610号消息就⽆法再申请了 。后续这个消费者就只能获取10号以后的消息。

4 、过滤消息

1)应⽤场景:

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

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

示例代码1:简单过滤

⽣产者端需要在发送消息时,增加Tag属性 。⽐如我们上⾯举例当中的⼊库 、出库 。核⼼代码:

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");

这样,后续Consumer就只会出处理TagA的消息。

示例代码2SQL过滤

通过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语句,定制复杂的过滤规则 。核⼼代码:

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)"));

注意:如果需要使⽤⾃定义参数进⾏过滤,需要在Broker端,将参数enablePropertyFilter设置成true。这个参数默认是false

实现思路:

实际上,Tags和⽤户⾃定义的属性,都是随着消息—起传递的,所以,消费者端是可以拿到消息的Tags和⾃定义属性的。 ⽐如:

consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println(msg.getTags());
System.out.println(msg.getProperties());
}
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

这样,剩下的就是在Consumer中对消息进⾏过滤了 Broker会在往Consumer推送消息时,在Broker端进⾏消息过滤。是Consumer感兴趣的 消息,就往Consumer推送。

Tag属性的处理⽐较简单,就是直接匹配 。⽽SQL语句的处理会⽐较麻烦—点 RocketMQ也是通过ANLTR引擎来解析SQL语句,然后再进⾏消 息过滤的。

ANLTR是—个开源的SQL语句解析框架 。很多开源产品都在使⽤ANLTR来解析SQL语句 。⽐如ShardingSphere Flink 

注意点:

  1. 1 、使⽤Tag过滤时,如果希望匹配多个Tag ,可以使⽤两个竖线(||)连接多个Tag 。另外,也可以使⽤星号(*)匹配所有。
  2. 2 、使⽤SQL顾虑时,SQL语句是按照SQL92标准来执⾏的。 SQL语句中⽀持—些常⻅的基本操作:
  •         .  数值⽐较, ⽐如:>>=<<=BETWEEN=
    •         .  字符⽐较, ⽐如:=<>IN
      •         .  IS NULL 或者 IS NOT NULL
        •         ·  逻辑符号 ANDORNOT

3、消息过滤,其实在Broker端和在Consumer端都可以做 Consumer端也可以⾃⾏获取⽤户属性,不感兴趣的消息,直接返回不成功的状    态,跳过该消息就⾏了 。但是RocketMQ会在Broker端完成过滤条件的判断,只将Consumer感兴趣的消息推送给Consumer。这样的好处是减 少了不必要的⽹络IO ,但是缺点是加⼤了服务端的压⼒ 。不过在RocketMQ的良好设计下,更建议使⽤消息过滤机制。

4、Consumer不感兴趣的消息并不表示直接丢弃 

通常是需要在同—个消费者组,定制另外的消费者实例,消费那些剩下的消息 。但是,如果 —直没有另外的Consumer ,那么, Broker端还是会推进Offset

5 、顺序消息机制

应⽤场景:

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

示例代码:

⽣产者核⼼代码:

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实现。

实现思路:

RocketMQ实现消息顺序消费,是需要⽣产者和消费者配合才能实现的。

、⽣产者只有将—批有顺序要求的消息,放到同—个MesasgeQueue上,通过MessageQueueFIFO特性保证这—批消息的顺序。 如果不指定MessageSelector对象,那么⽣产者会采⽤轮询的⽅式将多条消息依次发送到不同的MessageQueue上。

2 、消费者需要实现MessageListenerOrderly接⼝ ,实际上在服务端,处理MessageListenerOrderly时,会给—个MessageQueue加锁,拿到 MessageQueue上所有的消息,然后再去读取下—个MessageQueue的消息。

注意点:

1 、理解局部有序与全局有序 。⼤部分业务场景下,我们需要的其实是局部有序 。如果要保持全局有序,那就只保留—个MessageQueue 。性能 显然⾮常低。

2 、⽣产者端尽可能将有序消息打散到不同的MessageQueue上,避免过于集中导致数据热点竞争。

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

4 、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 作为替代。

6 、延迟消息

应⽤场景:

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

 虽然不太起眼,但是这是RocketMQ⾮常有特⾊的—个功能。对⽐下RabbitMQKafka RabbitMQ中只能通过使⽤死信队列变相实现延迟 消息,或者加装—个插件来⽀持延迟消息 Kafka则不太好实现延迟消息。

核⼼⽅法:

当前版本RocketMQ提供了两种实现延迟消息的机制,—种是指定固定的延迟级别,—种是指定消息发送时间。

⽣产者端核⼼代码:

// 指定固定的延迟级别
Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8)); message.setDelayTimeLevel(3); //10秒之后发送
// 指定消息发送时间
Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8)); message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L); //指定10秒之后的时间点

 关于延迟级别, RocketMQ给消息定制了18个默认的延迟级别

应⽤只需要根据⾃⼰的业务要求,选择对应的延迟级别即可。

实现思路:

对于指定固定延迟级别的延迟消息, RocketMQ的实现⽅式是预设—个系统Topic ,名字叫做SCHEDULE_TOPIC_XXXXX。在这个Topic下,预 设了18MessageQueue。这⾥每个对列就对应了—种延迟级别 。然后每次扫描这18个队列⾥的消息,进⾏延迟操作就可以了。

 外指定时间点的延迟消息, RocketMQ是通过时间轮算法实现的。

7 、批量消息

应⽤场景:

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

⽣产者核⼼代码:

List<Message> messages = new ArrayList<>(MESSAGE_COUNT);
for (int i = 0; i < MESSAGE_COUNT; i++) {
messages.add(new Message(TOPIC, TAG, "OrderID" + i, ("Hello world " +
i).getBytes(StandardCharsets.UTF_8)));
}
//split the large batch into small ones:
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
List<Message> listItem = splitter.next();
SendResult sendResult = producer.send(listItem);
System.out.printf("%s", sendResult);
}

注意点:

批量消息的使⽤⾮常简单,但是要注意RocketMQ做了限制。 同—批消息的Topic必须相同,另外,不⽀持延迟消息。

还有批量消息的⼤⼩不要超过1M ,如果太⼤就需要⾃⾏分割。

另外, 当前版本中, RocketMQ也在尝试实现—种⾃动化的消息分割机制。 只不过⽬前还没有放到Example中。详⻅

org.apache.rocketmq.client.producer.ProduceAccumulatorTest testProduceAccumulator_asynctestProduceAccumulator_sync 法。

基于客户端内部—个新增的ProduceAccumulator组件

8 、事务消息

应⽤场景:

事务消息是RocketMQ⾮常有特⾊的—个⾼级功能 。他的基础诉求是通过RocketMQ的事务机制,来保证上下游的数据—致性。

以电商为例,⽤户⽀付订单这—核⼼操作的同时会涉及到下游物流发货 、积分变更 、购物⻋状态清空等多个⼦系统的变更。这种场景,⾮常适 合使⽤RocketMQ的解耦功能来进⾏串联。

考虑到事务的安全性,即要保证相关联的这⼏个业务—定是同时成功或者同时失败的。如果要将四个服务—起作为—个分布式事务来控制,可 以做到,但是会⾮常麻烦 。⽽使⽤RocketMQ在中间串联了之后,事情可以得到—定程度的简化 。由于RocketMQ与消费者端有失败重试机制, 所以,只要消息成功发送到RocketMQ了,那么可以认为Branch2.1 Branch2.2 Branch2.3这⼏个分⽀步骤,是可以保证最终的数据—致性   的。这样,—个复杂的分布式事务问题,就变成了MinBranch1Branch2两个步骤的分布式事务问题。

然后,在此基础上, RocketMQ提出了事务消息机制, 采⽤两阶段提交的思路,保Main Branch1Branch2之间的事务—致性。

 具体的实现思路是这样的:

1 . ⽣产者将消息发送⾄Apache RocketMQ服务端。

2. Apache RocketMQ服务端将消息持久化成功之后, 向⽣产者返回Ack确认消息已经发送成功,此时消息被标记为" 暂不能投递" ,这种状态下 的消息即为半事务消息。

3. ⽣产者开始执⾏本地事务逻辑。

4. ⽣产者根据本地事务执⾏结果向服务端提交⼆次确认结果( Commit或是Rollback),服务端收到确认结果后处理逻辑如下:

⼆次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。

⼆次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。

5. 在断⽹或者是⽣产者应⽤重启的特殊情况下,若服务端未收到发送者提交的⼆次确认结果,或服务端收到的⼆次确认结果为Unknown未知 状态,经过固定时间后,服务端将对消息⽣产者即⽣产者集群中任—⽣产者实例发起消息回查。

6. ⽣产者收到消息回查后,需要检查对应消息的本地事务执⾏的最终结果。

7. ⽣产者根据检查到的本地事务的最终状态再次提交⼆次确认,服务端仍按照步骤4对半事务消息进⾏处理。

示例代码:

参⻅ org.apache.rocketmq.example.transaction.TransactionProducer

实现时的重点是使⽤RocketMQ提供的TransactionMQProducer事务⽣产者,在TransactionMQProducer中注⼊—个TransactionListener事务 监听器来执⾏本地事务, 以及后续对本地事务的检查。

注意点:

1 、半消息是对消费者不可⻅的—种消息 。实际上, RocketMQ的做法是将消息转到了—个系统Topic RMQ_SYS_TRANS_HALF_TOPIC

2 、事务消息中,本地事务回查次数通过参数transactionCheckMax设定,默认15 。本地事务回查的间隔通过参数transactionCheckInterval 设定,默认60 。超过回查次数后,消息将会被丢弃。

3 、其实, 了解了事务消息的机制后,在具体执⾏时,可以对事务流程进⾏适当的调整。

4 、如果你还是感觉不到RocketMQ事务消息机制的作⽤ ,那么可以看看下⾯这个⾯试题:

9 ACL权限控制机制

应⽤场景:

RocketMQ提供了针对队列 、⽤户等不同维度的⾮常全⾯的权限管理机制。通常来说, RocketMQ作为—个内部服务,是不需要进⾏权限控制 的,但是,如果要通过RocketMQ进⾏跨部⻔甚⾄跨公司的合作,权限控制的重要性就显现出来了。

权限控制体系:

1 RocketMQ针对每个Topic ,就有完整的权限控制。 ⽐如,在控制平台中,就可以很⽅便的给每个Topic配置权限。

 

perm字段表示Topic的权限。有三个可选项 2 :禁写禁订阅,4 :可订阅,不能写,6 :可写可订阅

2、在Broker端还提供了更详细的权限控制机制。 主要是在broker.conf中打开acl的标志:aclEnable=true 。然后就可以⽤他提供的

plain_acl.yml来进⾏权限配置了 。并且这个配置⽂件是热加载的,也就是说要修改配置时,只要修改配置⽂件就可以了,不⽤重启Broker服务。 ⽂件的配置⽅式,也⾮常简单,—⽬了然。

#全局⽩名单 ,不受ACL控制
#通常需要将主从架构中的所有节点加进来 globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*
accounts:
#第—个账户
- accessKey: RocketMQ secretKey: 12345678 whiteRemoteAddress:
admin: false
defaultTopicPerm: DENY #默认Topic访问策略是拒绝
defaultGroupPerm: SUB #默认Group访问策略是只允许订阅
topicPerms:
- topicA=DENY #topicA拒绝
- topicB=PUB |SUB #topicB允许发布和订阅消息
- topicC=SUB #topicC只允许订阅 groupPerms:
# the group should convert to retry topic
- groupA=DENY
- groupB=PUB |SUB
- groupC=SUB
#第⼆个账户 , 只要是来⾃192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2
secretKey: 12345678
whiteRemoteAddress: 192.168.1.*
# if it is admin, it could access all resources admin: true

 接下来,在客户端就可以通过accessKeysecretKey提交身份信息了 。客户端在使⽤时,需要先引⼊—个Maven依赖包。

<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
<version>4.9.1</version>
</dependency>

 然后在声明客户端时,传⼊—个RPCHook

//声明时传⼊RPCHook
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook());

private static final String ACL_ACCESS_KEY = "RocketMQ";
private static final String ACL_SECRET_KEY = "1234567";
static RPCHook getAclRPCHook() {
return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY,ACL_SECRET_KEY));
}

三、SpringBoot整合Rocket MQ

1 、快速实战

按照SpringBoot三板斧,快速创建RocketMQ的客户端 。创建Maven⼯程,引⼊关键依赖:

<dependencies>
	<dependency>
		<groupId>org.apache.rocketmq</groupId>
		<artifactId>rocketmq-spring-boot-starter</artifactId>
		<version>2.3.1</version>
		<exclusions>
	<exclusion>
		<groupId>org.apache.rocketmq</groupId>
		<artifactId>rocketmq-client</artifactId>
	</exclusion>
	</exclusions>
	</dependency>
	
	<dependency>
		<groupId>org.apache.rocketmq</groupId>
		<artifactId>rocketmq-client</artifactId>
		<version>5.3.0</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
		<version>3.0.4</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<version>3.0.4</version>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.13.2</version>
		<scope>test</scope>
	</dependency>
</dependencies>

 使⽤SpringBoot集成时,要⾮常注意版本!!!

SpringBoot升级到了3.0.4版本后,JDK要升级到17以上

启动类

@SpringBootApplication

public class RocketMQSBApplication {

    public static void main(String [] args) {

    SpringApplication.run(RocketMQSBApplication.class,args);

    }

}

配置⽂件:

配置⽂件:

rocketmq.name-server=192.168.65.112:9876
rocketmq.producer.group=springBootGroup
#如果这⾥不配 ,那就需要在消费者的注解中配。
#rocketmq.consumer.topic=
rocketmq.consumer.group=testGroup server.port=9000

接下来就可以声明⽣产者,直接使⽤RocketMQTemplate进⾏消息发送。

package com.roy.rocketmq.basic;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author :楼兰
* @description:
**/
@Component
public class SpringProducer {
    @Resource
    private RocketMQTemplate rocketMQTemplate;
        public void sendMessage(String topic,String msg){
            this.rocketMQTemplate.convertAndSend(topic,msg);
        }
}

另外,这个rocketMQTemplate不光可以发消息,还可以主动拉消息。

拉取消息时,需要配置rocketmq.consumer.topicrocketmq.consumer.group参数

消费者的声明也很简单 。所有属性通过@RocketMQMessageListener注解声明。 

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode=
ConsumeMode.CONCURRENTLY,messageModel= MessageModel.BROADCASTING)
    public class SpringConsumer implements RocketMQListener<String> {
        @Override
        public void onMessage(String message) {
            System.out.println("Received message : "+ message);
    }
}

这⾥唯—需要注意下的,就是消息了 SpringBoot框架中对消息的封装与原⽣API的消息封装是不—样的。

2 、如何处理各种消息类型

(1)各种基础的消息发送机制参⻅单元测试类:com.roy.rocketmq.SpringRocketTest

(2)—个RocketMQTemplate实例只能包含—个⽣产者,也就只能往—个Topic下发送消息 。如果需要往另外—个Topic下发送消息,就需要通过 @ExtRocketMQTemplateConfiguration()注解另外声明—个⼦类实例。

(3)对于事务消息机制,最关键的事务监听器需要通过@RocketMQTransactionListener注解注⼊到Spring容器当中。在这个注解当中可以通过 rocketMQTemplateBeanName属性,指向具体的RocketMQTemplate⼦类。

3 、实现原理

(1)RocketMQTemplate

RocketMQTemplate的注⼊过程参⻅org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration.

(2)Push模式消费者

Push模式对于@RocketMQMessageListener注解的处理⽅式,⼊⼝

rocketmq-spring-boot-2.3.1.jar中的org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration类中。

这个ListenerContainerConfiguration配置类会往Spring容器中注⼊—个RocketMQMessageListenerContainerRegistrar对象

@Configuration
@ConditionalOnMissingBean(RocketMQMessageListenerContainerRegistrar.class) public class ListenerContainerConfiguration {
@Bean
public RocketMQMessageListenerContainerRegistrar                 rocketMQMessageListenerContainerRegistrar(RocketMQMessageConverter
rocketMQMessageConverter, ConfigurableEnvironment environment, RocketMQProperties rocketMQProperties) {
    return new RocketMQMessageListenerContainerRegistrar(rocketMQMessageConverter, environment, rocketMQProperties);
}
}

注⼊RocketMQMessageListenerContainerRegistrar后, rocketmq-spring-boot-2.3.1.jar中会另外注⼊—个

RocketMQMessageListenerBeanPostProcessor对象。这个对象继承了SmartLifecycle接⼝, 因此会在初始化完成后,调⽤他的start⽅法。在 这⾥会调⽤RocketMQMessageListenerContainerRegistrarstartContainer⽅法。

@Override
public void start() {
    if ( !isRunning()) {
        this.setRunning(true);
        listenerContainerRegistrar.startContainer();
    }
}

 在这个⽅法中,会启动—个DefaultRocketMQListenerContainer

public void startContainer() {
    for (DefaultRocketMQListenerContainer container : containers) {
        if ( !container.isRunning()) {
            try {
                container.start();
            } catch (Exception e) {
                log.error("Started container failed. {}", container, e);
                throw new RuntimeException(e);
            }
        }
    }
}

这⾥这个DefaultRocketMQListenerContainer实际上就是对RocketMQDefaultMQPushConsumer进⾏封装的—个容 start⽅法实际上就 是在启动—个RocketMQ的原⽣Consumer

⾄于如何创建Consumer实例,⽅法就在DefaultRocketMQListenerContainerafterPropertiesSet⽅法中。其中有个 initRocketMQPushConsuer⽅法,就是在创建原⽣Consuer实例。

registerContainer的⽅法挺⻓的,我这⾥截取出跟今天的主题相关的⼏⾏重要的源码:

这其中最关注的, 当然是创建容器的createRocketMQListenerContainer⽅法中。⽽在这个⽅法中,你基本看不到RocketMQ的原⽣API ,都是 在创建并维护—个DefaultRocketMQListenerContainer对象 。⽽这个DefaultRocketMQListenerContainer类,就是我们今天关注的重点。

DefaultRocketMQListenerContainer类实现了InitializingBean接⼝, ⾃然要先关注他的afterPropertiesSet⽅法。这是Spring提供的对象初始化 的扩展机制。

public void afterPropertiesSet() throws Exception {
    initRocketMQPushConsumer();
    this.messageType = getMessageType();
    this.methodParameter = getMethodParameter();
    log.debug("RocketMQ messageType: {}", messageType);
}

这个⽅法就是⽤来初始化RocketMQ消费者的。在这个⽅法⾥就会创建—个RocketMQ原⽣的DefaultMQPushConsumer消费者 。同样,⽅法很 ⻓ ,抽取出⽐较关注的重点源码。

private void initRocketMQPushConsumer() throws MQClientException {
.....
//检查并创建consumer对象。
if (Objects.nonNull(rpcHook)) {
consumer = new DefaultMQPushConsumer(consumerGroup, rpcHook, new AllocateMessageQueueAveragely(),
enableMsgTrace, this.applicationContext.getEnvironment().
resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
consumer.setVipChannelEnabled(false);
} else {
log.debug("Access-key or secret-key not configure in " + this + ".");
consumer = new DefaultMQPushConsumer(consumerGroup, enableMsgTrace,
this.applicationContext.getEnvironment().
resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
}
// 定制instanceName,有没有很熟悉!!!
consumer.setInstanceName(RocketMQUtil.getInstanceName(nameServer));
.....
//设定⼴播消费还是集群消费。
switch (messageModel) {
case BROADCASTING:
consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING);
break;
case CLUSTERING:
consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CLUSTERING);
break;
default:
throw new IllegalArgumentException("Property 'messageModel ' was wrong.");
}
//维护消费者的其他属性。
...
//指定Consumer的消费监听  --》在消费监听中就会去调⽤onMessage⽅法。
switch (consumeMode) {
case ORDERLY:
consumer.setMessageListener(new DefaultMessageListenerOrderly());
break;
case CONCURRENTLY:
consumer.setMessageListener(new DefaultMessageListenerConcurrently());
break;
default:
throw new IllegalArgumentException("Property 'consumeMode ' was wrong.");
}
}

这整个就是在维护RocketMQ的原⽣消费者对象 。其中的使⽤⽅式,其实有很多地⽅是很值得借鉴的,尤其是消费监听的处理。

(3)Pull模式

Pull模式的实现其实是通过在RocketMQTemplate实例中注⼊—个DefaultLitePullConsumer实例来实现的。只要注⼊并启动了这个

DefaultLitePullConsumer示例后,后续就可以通过template实例的receive⽅法,来调⽤DefaultLitePullConsumerpoll⽅法,主动Pull获取 消息了。

初始化DefaultLitePullConsumer的代码依然是在rocketmq-spring-boot-2.3.1.jar包中。不过处理类是

org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration。这个配置类会配置在jar包中的spring.factories⽂件中,通过 SpringBoot的⾃动装载机制加载进来。

@Bean(CONSUMER_BEAN_NAME)
@ConditionalOnMissingBean(DefaultLitePullConsumer.class)
@ConditionalOnProperty(prefix = "rocketmq", value = {"name-server", "consumer.group", "consumer.topic"}) //解析的 springboot配置属性。
public DefaultLitePullConsumer defaultLitePullConsumer(RocketMQProperties rocketMQProperties)
throws MQClientException {
RocketMQProperties.Consumer consumerConfig = rocketMQProperties.getConsumer();
String nameServer = rocketMQProperties.getNameServer();
String groupName = consumerConfig.getGroup();
String topicName = consumerConfig.getTopic();
Assert.hasText(nameServer, " [rocketmq.name-server] must not be null");
Assert.hasText(groupName, " [rocketmq.consumer.group] must not be null");
Assert.hasText(topicName, " [rocketmq.consumer.topic] must not be null");

...
//创建消费者
DefaultLitePullConsumer litePullConsumer = RocketMQUtil.createDefaultLitePullConsumer(nameServer, accessChannel,
groupName, topicName, messageModel, selectorType, selectorExpression, ak, sk, pullBatchSize, useTLS);
litePullConsumer.setEnableMsgTrace(consumerConfig.isEnableMsgTrace());
litePullConsumer.setCustomizedTraceTopic(consumerConfig.getCustomizedTraceTopic());
litePullConsumer.setNamespace(consumerConfig.getNamespace());
return litePullConsumer;
}

RocketMQUtil.createDefaultLitePullConsumer⽅法中,就是在维护—个DefaultLitePullConsumer实例。这个实例就是RocketMQ的原⽣API 中提供的拉模式客户端。

实际开发中,拉模式⽤得⽐较少 。但是,其实RocketMQ针对拉模式也做了⾮常多的优化 。原本提供了—个DefaultMQPullConsumer类, 进⾏拉模式消息消费, DefaultLitePullConsumer在此基础上做了很多优化。有兴趣可以⾃⼰研究—下。 

四、Rocket MQ客户端注意事项

1 、消息的ID KeyTag

这⾥有个⼩细节需要注意, producer⽣产者端发送的是Message对象,⽽Consumer消费端处理的却是MessageExt对象 。也就是说,虽然都是 传递消息,但是Consumer端拿到的信息会⽐Producer端发送的消息更多。这⾥就有⼏个重点的参数需要理解 。那就是MessageId, Key和

Tag。

MessageIdRocketMQ内部给每条消息分配的唯⼀索引

Producer发送的Message对象是没有msgId属性的。Broker端接收到Producer发过来的消息后,会给每条消息单独分配—个唯—的msgId。这 msgID可以作为消息的唯—主键来使⽤ 

但是需要注意,对于客户端来说,毕竟是不知道这个msgId是如何产⽣的。 实际上,在RocketMQ内部,也会针对批量消息 、事务消息等特殊的 消息机制,有特殊的msgId分配机制。 因此,在复杂业务场景下,不建议使⽤msgId来作为消息的唯—索引,⽽建议采⽤下⾯的key属性, ⾃⾏  指定业务层⾯上的唯—索引。

keyMessage中的补充信息

Producer发送Message消息时, 同样也是没有key属性的。⽽这⾥设置的key ,其实是以RocketMQ中消息的补充属性的形式插⼊进去的。



public void setKeys(String keys) {
    this.putProperty(MessageConst.PROPERTY_KEYS, keys);
}
void putProperty(final String name, final String value) {
    if (null == this.properties) {
        this.properties = new HashMap<>();
    }
    this.properties.put(name, value);
}

从这⾥可以看出, key属性的本质只是Message中的—个补充信息,我们也可以像使⽤key—样,往消息当中添加—些⾃定义的属性 RocketMQ 内部也⼤量运⽤了这些⾃定义的属性,具体可以参⻅源码当中的MessageConst类。

针对key这—个属性,建议在业务中可以添加—些带有业务唯—性的数据,作为MessageId的补充 RocketMQ基于Keys属性,实现了消息溯  、消息压缩等—系列功能。

通过Tag进⾏消息过滤性能⾮常⾼

Tag属性也是Producer发送的Message对象的固有属性 。其作⽤主要是⽤来进⾏消息过滤 。实际上, RocketMQ的服务端会把消息的Tag信息以 某种形式(hashCode)写⼊到检索消息的ConsumeQueue索引中。这样当Consumer消费消息时,就可以通过过滤ConsumeQueue引中的Tag 属性,快速找到⾃⼰感兴趣的消息。

ConsumeQueue索引⽂件后续会做详细介绍。这⾥你可以简单理解为中华字典前⾯的索引,通过这个索引可以快速定位到某—条具体的消息。 由于Tag信息已经包含在索引中了,所以使⽤Tag进⾏适当的消息过滤,性能是⾮常⾼的,这也是官⽅推荐的使⽤RocketMQ的—种最佳实践。

2 、最佳实践

—个应⽤尽可能⽤—个Topic ,⽽消息⼦类型则可以⽤tags来标识 tags可以由应⽤⾃由设置,只有⽣产者在发送消息设置了tags ,消费⽅在订 阅消息时才可以利⽤tags通过broker做消息过滤:message.setTags("TagA")

 Kafka的—⼤问题是Topic过多,会造成Partition⽂件过多,影响性能 。⽽RocketMQ中的Topic完全不会对消息转发性能有影响 。但是Topic 过多,还是会加⼤RocketMQ的元数据维护的性能消耗 。所以,在使⽤时,还是需要对Topic进⾏合理的分配。

使⽤Tag区分消息时,尽量直接使⽤Tag过滤,不要使⽤复杂的SQL过滤 。因为消息过滤机制虽然可以减少⽹络IO ,但是毕竟会加⼤Broker 端的消息处理压⼒ 。所以,消息过滤的逻辑,还是越简单越好。

3 、消费者端进⾏幂等控制

MQ系统中,对于消息幂等有三种实现语义:

        •   at most once 最多—次:每条消息最多只会被消费—次

            at least once ⾄少—次:每条消息⾄少会被消费—次

          ​​​​​​​ exactly once 刚刚好—次:每条消息都只会确定的消费—次 这三种语义都有他适⽤的业务场景。

其中,at most once是最好保证的。RocketMQ中可以直接⽤异步发送 sendOneWay等⽅式就可以保证。

 at least once这个语义, RocketMQ也有同步发送 、事务消息等很多⽅式能够保证。

⽽这个exactly onceMQ中最理想也是最难保证的—种语义,需要有⾮常精细的设计才⾏ RocketMQ只能保证at least once ,保证不了 exactly once 。所以,使⽤RocketMQ时,需要由业务系统⾃⾏保证消息的幂等性。

消息幂等的必要性

在互联⽹应⽤中,尤其在⽹络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况:

     •发送时消息重复

当—条消息已被成功发送到服务端并完成持久化,此时出现了⽹络闪断或者客户端宕机,导致服务端对客户端应答失败 。 如果此时⽣产者 意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

    • 投递时消息重复

消息消费的场景下,消息已投递到消费者并完成业务处理, 当客户端给服务端反馈应答的时候⽹络闪断 。 为了保证消息⾄少被消费—次, 消息队列 RocketMQ 的服务端将在⽹络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID  相同的消息。

.     •负载均衡时消息重复( 包括但不限于⽹络抖动、Broker 重启以及订阅⽅应⽤重启)

当消息队列 RocketMQ Broker 或客户端重启 、扩容或缩容时,会触发 Rebalance ,此时消费者可能会收到重复消息。 处理⽅式

从上⾯的分析中,我们知道,在RocketMQ中,是⽆法保证每个消息只被投递—次的,所以要在业务上⾃⾏来保证消息消费的幂等性。

⽽要处理这个问题, RocketMQ的每条消息都有—个唯—的MessageId ,这个参数在多次投递的过程中是不会改变的,所以业务上可以⽤这个 MessageId来作为判断幂等的关键依据。

但是,这个MessageId是⽆法保证全局唯—的,也会有冲突的情况 。所以在—些对幂等性要求严格的场景,最好是使⽤业务上唯—的—个标识 ⽐较靠谱 。例如订单ID 。⽽这个业务标识可以使⽤MessageKey来进⾏传递。

4 、关注错误消息重试

我们已经知道RocketMQ的消费者端,如果处理消息失败了, Broker是会将消息重新进⾏投送的。⽽在重试时, RocketMQ实际上会为每个消费 者组创建—个对应的重试队列 。重试的消息会进⼊—个 “%RETRY%”+ConsumeGroup 的队列中。

 

多关注重试队列,可以及时了解消费者端的运⾏情况。这个队列中出现了⼤量的消息,就意味着消费者的运⾏出现了问题,要及时跟踪进⾏⼲ 预。​​​​​​​

然后RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间如下:

重试次数

与上次重试的间隔时间

重试次数

与上次重试的间隔时间

1

10 

9

7 分钟

2

30

10

8 分钟

3

1 分钟

11

9 分钟

4

2 分钟

12

10 分钟

5

3 分钟

13

20 分钟

6

4 分钟

14

30 分钟

7

5 分钟

15

1 ⼩时

8

6 分钟

16

2 ⼩时

这个重试时间跟延迟消息的延迟级别是对应的。不过取的是延迟级别的后16级别。

messageDelayLevel=1 s 5s 10s 30s 1 m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

这个重试时间可以将源码中的org.apache.rocketmq.example.quickstart.Consumer⾥的消息监听器返回状态改为RECONSUME_LATER 试—下。

重试次数:

如果消息重试16次后仍然失败,消息将不再投递 。转为进⼊死信队列。

然后关于这个重试次数, RocketMQ可以进⾏定制。例如通过consumer.setMaxReconsumeTimes(20);将重试次数设定为20 。当定制的重试 次数超过16次后,消息的重试时间间隔均为2⼩时。

配置覆盖:

消息最⼤重试次数的设置对相同GroupID下的所有Consumer实例有效 。并且最后启动的Consumer会覆盖之前启动的Consumer的配置。

5 、⼿动处理死信队列

当—条消息消费失败, RocketMQ就会⾃动进⾏消息重试 。⽽如果消息超过最⼤重试次数, RocketMQ就会认为这个消息有问题 。但是此时, RocketMQ不会⽴刻将这个有问题的消息丢弃,⽽会将其发送到这个消费者组对应的—种特殊队列:死信队列。

通常,—条消息进⼊了死信队列,意味着消息在消费处理的过程中出现了⽐较严重的错误,并且⽆法⾃⾏恢复 。此时,—般需要⼈⼯去查看死 信队列中的消息,对错误原因进⾏排查 。然后对死信消息进⾏处理, ⽐如转发到正常的Topic重新进⾏消费,或者丢弃。

死信队列的名称是%DLQ%+ConsumGroup

 

死信队列的特征:

  —个死信队列对应—个ConsumGroup ,⽽不是对应某个消费者实例。

 如果—个ConsumeGroup没有产⽣死信队列, RocketMQ就不会为其创建相应的死信队列。

. —个死信队列包含了这个ConsumeGroup⾥的所有死信消息,⽽不区分该消息属于哪个Topic

  死信队列中的消息不会再被消费者正常消费。

死信队列的有效期跟正常消息相同 。默认3天,对应broker.conf中的fileReservedTime属性 。超过这个最⻓时间的消息都会被删除,⽽不管消息是否消费过

 :默认创建出来的死信队列,他⾥⾯的消息是⽆法读取的,在控制台和消费者中都⽆法读取。这是因为这些默认的死信队列,他们的权 perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁写,6:可读可写) 。需要⼿动将死信队列的权限配置成6 ,才能被消费(可以通过mqadmin指定或者web控制台)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值