一、客户端使用MQ基本代码示例
1、添加maven依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>5.3.0</version>
</dependency>
2、生产者代码示例
java
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
//初始化一个消息生产者
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 指定nameserver地址
producer.setNamesrvAddr("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();
}
}
3、消费者代码示例
java
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
//构建一个消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
//指定nameserver地址
consumer.setNamesrvAddr("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");
}
}
4、代码逻辑解读
生产者:
-
创建消息生产者producer,并指定生产者组名
-
指定Nameserver地址
-
启动producer。可以认为这是消息生产者与服务端建立连接的过程。
-
创建消息对象,指定Topic、Tag和消息体
-
发送消息
-
关闭生产者producer,释放资源。
消费者:
-
创建消费者Consumer,必须指定消费者组名
-
指定Nameserver地址
-
订阅主题Topic和Tag
-
设置回调函数,处理消息
-
启动消费者consumer。消费者会一直挂起,持续处理消息。
二、消息确认机制
1、生产者确认机制
生产者发送消息的方式有三种:
(1)单向发送:消息生产者只管往Broker发送消息,而全然不关心Broker端有没有成功接收到消息。
java
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方法没有返回值,如果发送失败,生产者无法补救。
单向发送有一个好处,就是发送消息的效率更高。适用于一些追求消息发送效率,而允许消息丢失的业务场景。比如日志。
(2)同步发送:消息生产者在往Broker端发送消息后,会阻塞当前线程,等待Broker端的相应结果。
java
SendResult sendResult = producer.send(msg);
SendResult来自于Broker的反馈。producer在send发出消息,到Broker返回SendResult的过程中,无法做其他的事情。在SendResult中有一个SendStatus属性,这个SendStatus是一个枚举类型,其中包含了Broker端的各种情况。
java
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之间传输一个来回后才能结束。如果网速比较慢,同步发送的耗时就会很长。
(3)异步发送:生产者在向Broker发送消息时,会同时注册一个回调函数。接下来生产者并不等待Broker的响应。当Broker端有响应数据过来时,自动触发回调函数进行对应的处理。
java
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接口中有两个方法,onSuccess和onException。当Broker端返回消息处理成功的响应信息SendResult时,就会调用onSuccess方法。当Broker端处理消息超时或者失败时,就会调用onExcetion方法,生产者就可以在onException方法中进行补救措施。
此时同样有几个问题需要注意。一是与同步发送机制类似,触发了SendCallback的onException方法同样并不一定就表示消息不会向消费者推送,例如:如果Broker端返回响应信息太慢,超过了超时时间,也会触发onException方法。二是在SendCallback的对应方法被触发之前,生产者不能调用shutdown()方法。如果消息处理完之前,生产者线程就关闭了,生产者的SendCallback对应方法就不会触发。这是因为使用异步发送机制后,生产者虽然不用阻塞下来等待Broker端响应,但是SendCallback还是需要附属于生产者的主线程才能执行。
2、消费者确认机制
消费者收到消息后,向 Broker 响应消息来进行确认。
java
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_SUCCESS和RECONSUME_LATER。如果消费者返回CONSUME_SUCCESS,那么消息自然就处理结束了。但是如果消费者没有处理成功,返回的是RECONSUME_LATER,Broker就会过一段时间再发起消息重试。
为了兼顾重试机制的成功率和性能,RocketMQ设计了一套非常完善的消息重试机制:
(1)失败重试
当消费者没能正常消费消息时,Broker 会进行消息重试,也就是将消息移到重试Topic中。
为了让这些重试的消息不会影响Topic下其他正常的消息,Broker会给每个消费者组设计对应的重试Topic,名称为 %RETRY%+ConsumeGroup。这是因为,MessageQueue是一个具有严格FIFO特性的数据结构,如果需要重试的这些消息还是放在原来的MessageQueue中,就会对当前MessageQueue产生阻塞,让其他正常的消息无法处理。
RocketMQ默认的最大重试次数是16次。重试的间隔时间如下图所示,是延迟消息的后16个延迟级别。

如果我们修改了重试次数为20次,那么超过16次后每次重试间隔为2小时。同一个消费者组中,如果多个消费者都设置了重试次数,那么后设置的会覆盖先设置的。
(2)死信队列
Broker不可能无限制的向消费失败的消费者推送消息,当超过最大重试次数后,消息会移到死信队列,它相当于windows当中的回收站。我们可以人工介入对死信队列中的消息进行补救,也可以直接彻底删除这些消息。
死信队列的 topic 名称为 %DLQ%+ConsumGroup,一个消费者组只有一个死信队列,且只有死信消息产生时,才会生成死信队列。
当我们对死信队列中的消息进行补救时,通常会创建一个新的消费者组获取死信队列中的消息,对消息内容进行修正后,重新发送到正常的 topic 中。
需要注意的是,死信队列被创建出来后,它的权限 perm 被设置为 2(2:禁读,4:禁写,6:可读可写),所以它里面的消息是无法读取的。在补救前,需要将死信队列的权限修改为 6。
死信队列的有效期跟正常消息相同,默认3天,对应broker.conf中的fileReservedTime属性。超过这个最长时间的消息都会被删除,而不管消息是否消费过。
(3)尽量保证同一消费者组具有相同的逻辑
RocketMQ中设定的消费者组都是订阅主题和消费逻辑相同的服务备份,所以当消息重试时,Broker会往消费者组中任意一个实例推送。因此,我们在编码时,尽量要保证一个消费者组处理业务的逻辑相同。
(4)消费逻辑尽量避免异步
Broker端最终只通过消费者组返回的状态来确定消息有没有处理成功。至于消费者组自己的业务执行是否正常,Broker端是没有办法知道的。因此,在实现消费者的业务逻辑时,应该要尽量使用同步实现方式,保证在自己业务处理完成之后再向Broker端返回状态。
3、幂等性保证
在 MQ 系统中,幂等性有三种实现语义:
1、at most once:每条消息最多被消费一次。对于 at most once,生产者使用 sendOneWay 发送消息即可。
2、at least once:每条消息至少被消费一次。对于 at least once,利用生产者和消费者的消息确认机制,即可确保消息成功发送和接收。
3、exactly once:每条消息正好被消费一次。对于 exactly once,难以通过 MQ 本身直接实现。通常的方法是利用消息确认机制确保 at least once,再通过对消息设置业务主键进行消息去重来确保 at most once,两者组合实现 exactly once。
如何使用消息的业务主键去重呢?
当消息发送到 RocketMQ 时,RocketMQ 会为消息生成唯一的 msgId,该 msgId 在消息重复生产和消费时都不会发生改变,通常可用于区分每条消息。但这个 msgId 并不能完全确保全局唯一,在对幂等性要求严格的场景,可以在发送消息时设置全局唯一的 message key,并在获取消息时根据 message key 来去重(MQ 会对 message key 进行索引,我们除了可以使用 message key 保证幂等性,还能用它来快速查找消息)。
什么时候会出现消息重复呢?
1、发送时重复:生产者客户端已成功发送消息且消息已在服务端持久化,但由于网络阻塞或客户端宕机,导致服务端向客户端应答失败。故障恢复后,客户端由于未收到应答,会认为消息发送失败而重新发送,服务端就会存在两条内容相同并且 msgId 也相同的消息。
2、接收时重复:消费者客户端已成功收到消息并完成业务处理,但由于网络阻塞或客户端宕机,导致客户端向服务端应答失败。故障恢复后,服务端由于未收到应带,会认为消息投递失败而重新投递,客户端就会收到两条内容相同并且 msgId 也相同的消息。
3、Rebalance 导致重复:当 Broker 或消费者出现重启、扩容和缩容时,会触发 Rebalance,此时可能导致消息重复。