2、RocketMQ的消息模型

⼆、深⼊理解RocketMQ的消息模型

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接⼝中有两个⽅法,onSuccess和onException。当Broker端返回消息处理成功的响应信息
SendResult时,就会调⽤onSuccess⽅法。当Broker端处理消息超时或者失败时,就会调⽤onExcetion⽅法,⽣
产者就可以在onException⽅法中进⾏补救措施。
此时同样有⼏个问题需要注意。⼀是与同步发送机制类似,触发了SendCallback的onException⽅法同样并不⼀
定就表示消息不会向消费者推送。如果Broker端返回响应信息太慢,超过了超时时间,也会触发onException⽅
法。超时时间默认是3秒,可以通过producer.setSendMsgTimeout⽅法定制。⽽造成超时的原因则有很多,消
息太⼤造成⽹络拥堵、⽹速太慢、Broker端处理太慢等都可能造成消息处理超时。
⼆是在SendCallback的对应⽅法被触发之前,⽣产者不能调⽤shutdown()⽅法。如果消息处理完之前,⽣产者
线程就关闭了,⽣产者的SendCallback对应⽅法就不会触发。这是因为使⽤异步发送机制后,⽣产者虽然不⽤阻塞
下来等待Broker端响应,但是SendCallback还是需要附属于⽣产者的主线程才能执⾏。如果Broker端还没有返回
SendResult,⽽⽣产者主线程已经停⽌了,那么SendCallback的执⾏线程也就会随主线程⼀起停⽌,对应的⽅法
⾃然也就⽆法执⾏了。
这种异步发送的机制能够⽐较好的兼容消息的安全性以及⽣产者的⾼吞吐需求,是很多MQ产品都⽀持的⽅式。
RabbitMQ和Kafka都⽀持这种异步发送的机制。但是异步发送机制也并不是万能的,毕竟异步发送机制对消息⽣产
者的主线业务是有侵⼊的。具体使⽤时还是需要根据业务场景考虑。
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_SUCCESS和RECONSUME_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⽆法确定⾃⼰将要处理的
是哪些消息。这就好⽐你上班做⼀天事情,公司负责给你发⼀笔⼯资。如果⼀切正常,那么没什么问题。 但是如果
出问题了呢?公司拖⽋了你的⼯资,这时,你就还是需要能到公司查账,⾄少查你⾃⼰的⼯资记录。从上⼀次发⼯ 资的时候计算你该拿的钱。

对消息队列也⼀样。虽然Offset完全由Broker进⾏维护,但是,RocketMQ也允许Consumer⾃⼰去查账,⾃⼰
指定消费位点。核⼼代码是在Consumer中设定了⼀个属性ConsumeFromWhere,表示在Consumer启动时,从
哪⼀条消息开始进⾏消费。Consumer当然不可能精确的知道Offset的具体参数,所以这个ConsumerFromWhere
并不是直接传⼊Offset位点,⽽是可以传⼊⼀个ConsumerFromWhere对象,这是⼀个枚举值。名字⼀⽬了然。

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

启动多个消费者,⼴播模式下,这些消费者都会消费⼀次消息。多个消费者的代码都相同,group相同
实现思路:
默认模式(也就是集群模式)下,Broker端会给每个ConsumerGroup维护⼀个统⼀的Offset,这个Offset可以保证
⼀个消息,在同⼀个ConsumerGroup内只会被消费⼀次。⽽⼴播模式的实现⽅式,是将Offset转移到消费者端⾃
⾏保管,这样Broker端只管向所有消费者推送消息,⽽不⽤负责维护消费进度。
注意点:
1、Broker端不维护消费进度,意味着,如果消费者处理消息失败了,将⽆法进⾏消息重试。
2、消费者端维护Offset的作⽤是可以在服务重启时,按照上⼀次消费的进度,处理后⾯没有消费过的消息。丢了
也不影响服务稳定性。
public enum ConsumeFromWhere {
CONSUME_FROM_LAST_OFFSET, //从队列的第⼀条消息开始重新消费
CONSUME_FROM_FIRST_OFFSET, //从上次消费到的地⽅开始继续消费
CONSUME_FROM_TIMESTAMP; //从某⼀个时间点开始重新消费
}
consumer.setConsumerTimestamp(“20131223171201”);
consumer.setMessageModel(MessageModel.BROADCASTING);
⽐如⽣产者发送了1~10号消息。消费者当消费到第6个时宕机了。当他重启时,Broker端已经把第10个消息都推
送完成了。如果消费者端维护好了⾃⼰的Offset,那么他就可以在服务重启时,重新向Broker申请6号到10号的消
息。但是,如果消费者端的Offset丢失了,消费者服务依然可以正常运⾏,但是6到10号消息就⽆法再申请了。后
续这个消费者就只能获取10号以后的消息。
实际上,Offset的维护数据是放在
u s e r . h o m e / . r o c k e t m q o f f s e t / {user.home}/.rocketmq_offset/ user.home/.rocketmqoffset/{clientIp} i n s t a n c e N a m e / {instanceName}/ instanceName/{group}/offsets.json ⽂件下的。
消费者端存储⼴播消费的本地offsets⽂件的默认缓存⽬录是 System.getProperty(“user.home”) +
File.separator + “.rocketmq_offsets” ,可以通过定制 rocketmq.client.localOffsetStoreDir 系统属性进⾏修改。
本地offsets⽂件在缓存⽬录中的具体位置与消费者的clientIp 和 instanceName有关。其中instanceName默认
是DEFAULT,可以通过定制系统属性 rocketmq.client.name 进⾏修改。另外,每个消费者对象也可以单独设定
instanceName。
RocketMQ会通过定时任务不断尝试本地Offsets⽂件的写⼊,但是,如果本地Offsets⽂件写⼊失败,
RocketMQ不会进⾏任何的补救。https://blog.csdn.net/roykingw/article/details/126351010

4、顺序消息机制

应⽤场景:
每⼀个订单有从下单、锁库存、⽀付、下物流等⼏个业务步骤。每个业务步骤都由⼀个消息⽣产者通知给下游服
务。如何保证对每个订单的业务处理顺序不乱?
示例代码:
⽣产者核⼼代码:

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实现
实现思路:
基础思路:只有放到⼀起的⼀批消息,才有可能保持消息的顺序
在这里插入图片描述
1、⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,Broker才有可能保持这⼀批消息的顺
序。
2、消费者只有⼀次锁定⼀个MessageQueue,拿到MessageQueue上所有的消息,
注意点:
1、理解局部有序与全局有序。⼤部分业务场景下,我们需要的其实是局部有序。如果要保持全局有序,那就只
保留⼀个MessageQueue。性能显然⾮常低。
2、⽣产者端尽可能将有序消息打散到不同的MessageQueue上,避免过于⼏种导致数据热点竞争。
2、消费者端只能⽤同步的⽅式处理消息,不要使⽤异步处理。更不能⾃⾏使⽤批量处理。
3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏
重试。但是,如果消费者⼀直处理失败,超过最⼤重试次数,那么RocketMQ就会跳过这⼀条消息,处理后⾯的消
息,这会造成消息乱序。
4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回
ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT作为替代。

5、延迟消息

应⽤场景:
延迟消息发送是指消息发送到Apache RocketMQ后,并不期望⽴⻢投递这条消息,⽽是延迟⼀定时间后才投递
到Consumer进⾏消费。
虽然不太起眼,但是这是RocketMQ⾮常有特⾊的⼀个功能。对⽐下RabbitMQ和Kafka。RabbitMQ中只能
通过使⽤死信队列变相实现延迟消息,或者加装⼀个插件来⽀持延迟消息。 Kafka则不太好实现延迟消息。
示例代码:
⽣产者端核⼼代码:

msg.setDelayTimeLevel(3);

只要给消息设定⼀个延迟级别就⾏了,⽆⽐简单。
RocketMQ给消息定制了18个默认的延迟级别,分别对应18个不同的预设好的延迟时间。
在这里插入图片描述
实现思路:
延迟消息的难点其实是性能,需要不断进⾏定时轮训。全部扫描所有消息是不可能的,RocketMQ的实现⽅式是
预设⼀个系统Topic,名字叫做SCHEDULE_TOPIC_XXXX。在这个Topic下,预设18个延迟队列。然后每次只针对这
18个队列⾥的消息进⾏延迟操作,这样就不⽤⼀直扫描所有的消息了。
在这里插入图片描述
注意点:
这样预设延迟时间其实是不太灵活的。5.x版本已经⽀持预设⼀个具体的时间戳,按秒的精度进⾏定时发送。
但是可以看到,这18个延迟级别虽然⽆法调整,但是每个延迟级别对应的延迟时间其实是可以调整的。只需要修
改截图中的参数就⾏。不过通常不建议这么做

6、批量消息

应⽤场景:
⽣产者要发送的消息⽐较多时,可以将多条消息合并成⼀个批量消息,⼀次性发送出去。这样可以减少⽹络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);

注意点:
批量消息的使⽤⾮常简单,但是要注意RocketMQ做了限制。同⼀批消息的Topic必须相同,另外,不⽀持延迟
消息。
还有批量消息的⼤⼩不要超过1M,如果太⼤就需要⾃⾏分割。

7、过滤消息

应⽤场景:
同⼀个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的消息
示例代码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语句,定制复杂的过滤规则。核⼼代码:
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)"));

在这里插入图片描述

sql的实现需要在mq的配置文件配置这个true
实现思路:
实际上,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、使⽤Tag过滤时,如果希望匹配多个Tag,可以使⽤两个竖线(||)连接多个Tag值。另外,也可以使⽤星号(*)
匹配所有。
2、使⽤SQL顾虑时,SQL语句是按照SQL92标准来执⾏的。SQL语句中⽀持⼀些常⻅的基本操作:
数值⽐较,⽐如:>,>=,<,<=,BETWEEN,=;
字符⽐较,⽐如:=,<>,IN;
IS NULL 或者 IS NOT NULL;
逻辑符号 AND,OR,NOT;
2、消息过滤,其实在Broker端和在Consumer端都可以做。Consumer端也可以⾃⾏获取⽤户属性,不感兴趣
的消息,直接返回不成功的状态,跳过该消息就⾏了。但是RocketMQ会在Broker端完成过滤条件的判断,只将
Consumer感兴趣的消息推送给Consumer。这样的好处是减少了不必要的⽹络IO,但是缺点是加⼤了服务端的压
⼒。不过在RocketMQ的良好设计下,更建议使⽤消息过滤机制。
3、Consumer不感兴趣的消息并不表示直接丢弃。通常是需要在同⼀个消费者组,定制另外的消费者实例,消
费那些剩下的消息。但是,如果⼀直没有另外的Consumer,那么,Broker端还是会推进Offset

8、事务消息

应⽤场景:
事务消息是RocketMQ⾮常有特⾊的⼀个⾼级功能。他的基础诉求是通过RocketMQ的事务机制,来保证上下游
的数据⼀致性。
以电商为例,⽤户⽀付订单这⼀核⼼操作的同时会涉及到下游物流发货、积分变更、购物⻋状态清空等多个⼦系
统的变更。这种场景,⾮常适合使⽤RocketMQ的解耦功能来进⾏串联。
在这里插入图片描述
考虑到事务的安全性,即要保证相关联的这⼏个业务⼀定是同时成功或者同时失败的。如果要将四个服务⼀起作
为⼀个分布式事务来控制,可以做到,但是会⾮常麻烦。⽽使⽤RocketMQ在中间串联了之后,事情可以得到⼀定
程度的简化。由于RocketMQ与消费者端有失败重试机制,所以,只要消息成功发送到RocketMQ了,那么可以认
为Branch2.1,Branch2.2,Branch2.3这⼏个分⽀步骤,是可以保证最终的数据⼀致性的。这样,⼀个复杂的分布
式事务问题,就变成了MinBranch1和Branch2两个步骤的分布式事务问题。
然后,在此基础上,RocketMQ提出了事务消息机制,采⽤两阶段提交的思路,保证Main Branch1和Branch2之
间的事务⼀致性。
在这里插入图片描述
具体的实现思路是这样的:
在这里插入图片描述

  1. ⽣产者将消息发送⾄Apache RocketMQ服务端。
  2. Apache RocketMQ服务端将消息持久化成功之后,向⽣产者返回Ack确认消息已经发送成功,此时消息被标
    记为"暂不能投递",这种状态下的消息即为半事务消息。
  3. ⽣产者开始执⾏本地事务逻辑。
  4. ⽣产者根据本地事务执⾏结果向服务端提交⼆次确认结果(Commit或是Rollback),服务端收到确认结果后
    处理逻辑如下:
    ⼆次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
    ⼆次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
  5. 在断⽹或者是⽣产者应⽤重启的特殊情况下,若服务端未收到发送者提交的⼆次确认结果,或服务端收到的⼆
    次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息⽣产者即⽣产者集群中任⼀⽣产者实例
    发起消息回查。
  6. ⽣产者收到消息回查后,需要检查对应消息的本地事务执⾏的最终结果。
  7. ⽣产者根据检查到的本地事务的最终状态再次提交⼆次确认,服务端仍按照步骤4对半事务消息进⾏处理。
    实现时的重点是使⽤RocketMQ提供的TransactionMQProducer事务⽣产者,在TransactionMQProducer中注
    ⼊⼀个TransactionListener事务监听器来执⾏本地事务,以及后续对本地事务的检查。
    注意点:
    1、半消息是对消费者不可⻅的⼀种消息。实际上,RocketMQ的做法是将消息转到了⼀个系统Topic,
    RMQ_SYS_TRANS_HALF_TOPIC。
    2、事务消息中,本地事务回查次数通过参数transactionCheckMax设定,默认15次。本地事务回查的间隔通过
    参数transactionCheckInterval设定,默认60秒。超过回查次数后,消息将会被丢弃。
    3、其实,了解了事务消息的机制后,在具体执⾏时,可以对事务流程进⾏适当的调整。
    在这里插入图片描述
    4、在这里插入图片描述
    Half Message,半消息
    暂时不能被 Consumer消费的消息。Producer已经把消息发送到 Broker端,但是此消息的状态被标记为不能投递,处于这种状态下的消息称为半消息。事实上,该状态下的消息会被放在一个叫做 RMQ_SYS_TRANS_HALF_TOPIC的主题下。

当 Producer端对它二次确认后,也就是 Commit之后,Consumer端才可以消费到;那么如果是Rollback,该消息则会被删除,永远不会被消费到。

事务状态回查
我们想,可能会因为网络原因、应用问题等,导致Producer端一直没有对这个半消息进行确认,那么这时候 Broker服务器会定时扫描这些半消息,主动找Producer端查询该消息的状态。

当然,什么时候去扫描,包含扫描几次,我们都可以配置,在后文我们再细说。

简而言之,RocketMQ事务消息的实现原理就是基于两阶段提交和事务状态回查,来决定消息最终是提交还是回滚的。

在本文,我们的代码就以 订单服务、积分服务 为例。结合上文来看,整体流程如下:
在这里插入图片描述
在订单服务中,我们接收前端的请求创建订单,保存相关数据到本地数据库。

1、事务日志表
在订单服务中,除了有一张订单表之外,还需要一个事务日志表。
它的定义如下:

CREATE TABLE `transaction_log` (
  `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '事务ID',
  `business` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '业务标识',
  `foreign_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '对应业务表中的主键',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

这张表专门作用于事务状态回查。当提交业务数据时,此表也插入一条数据,它们共处一个本地事务中。通过事务ID查询该表,如果返回记录,则证明本地事务已提交;如果未返回记录,则本地事务可能是未知状态或者是回滚状态。

2、TransactionMQProducer
我们知道,通过 RocketMQ发送消息,需先创建一个消息发送者。值得注意的是,如果发送事务消息,在这里我们的创建的实例必须是 TransactionMQProducer。

@Component
public class TransactionProducer {
	
    private String producerGroup = "order_trans_group";
    private TransactionMQProducer producer;

    //用于执行本地事务和事务状态回查的监听器
    @Autowired
    OrderTransactionListener orderTransactionListener;
    //执行任务的线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(50));
            
    @PostConstruct
    public void init(){
        producer = new TransactionMQProducer(producerGroup);
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setSendMsgTimeout(Integer.MAX_VALUE);
        producer.setExecutorService(executor);
        producer.setTransactionListener(orderTransactionListener);
        this.start();
    }
    private void start(){
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }
    //事务消息发送 
    public TransactionSendResult send(String data, String topic) throws MQClientException {
        Message message = new Message(topic,data.getBytes());
        return this.producer.sendMessageInTransaction(message, null);
    }
}

上面的代码中,主要就是创建事务消息的发送者。在这里,我们重点关注 OrderTransactionListener,它负责执行本地事务和事务状态回查。
3 OrderTransactionListener

@Component
public class OrderTransactionListener implements TransactionListener {

    @Autowired
    OrderService orderService;

    @Autowired
    TransactionLogService transactionLogService;

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        logger.info("开始执行本地事务....");
        LocalTransactionState state;
        try{
            String body = new String(message.getBody());
            OrderDTO order = JSONObject.parseObject(body, OrderDTO.class);
            orderService.createOrder(order,message.getTransactionId());
            state = LocalTransactionState.COMMIT_MESSAGE;
            logger.info("本地事务已提交。{}",message.getTransactionId());
        }catch (Exception e){
            logger.info("执行本地事务失败。{}",e);
            state = LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return state;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        logger.info("开始回查本地事务状态。{}",messageExt.getTransactionId());
        LocalTransactionState state;
        String transactionId = messageExt.getTransactionId();
        if (transactionLogService.get(transactionId)>0){
            state = LocalTransactionState.COMMIT_MESSAGE;
        }else {
            state = LocalTransactionState.UNKNOW;
        }
        logger.info("结束本地事务状态查询:{}",state);
        return state;
    }
}

在通过 producer.sendMessageInTransaction发送事务消息后,如果消息发送成功,就会调用到这里的executeLocalTransaction方法,来执行本地事务。在这里,它会完成订单数据和事务日志的插入。

该方法返回值 LocalTransactionState 代表本地事务状态,它是一个枚举类。

public enum LocalTransactionState {
    //提交事务消息,消费者可以看到此消息
    COMMIT_MESSAGE,
    //回滚事务消息,消费者不会看到此消息
    ROLLBACK_MESSAGE,
    //事务未知状态,需要调用事务状态回查,确定此消息是提交还是回滚
    UNKNOW;
}

那么, checkLocalTransaction 方法就是用于事务状态查询。在这里,我们通过事务ID查询transaction_log这张表,如果可以查询到结果,就提交事务消息;如果没有查询到,就返回未知状态。

注意,这里还涉及到另外一个问题。如果是返回未知状态,RocketMQ Broker服务器会以1分钟的间隔时间不断回查,直至达到事务回查最大检测数,如果超过这个数字还未查询到事务状态,则回滚此消息。

当然,事务回查的频率和最大次数,我们都可以配置。在 Broker 端,可以通过这样来配置它:

brokerConfig.setTransactionCheckInterval(10000); //回查频率10秒一次
brokerConfig.setTransactionCheckMax(3);  //最大检测次数为3

4、详细代码

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    TransactionLogMapper transactionLogMapper;
    @Autowired
    TransactionProducer producer;

    Snowflake snowflake = new Snowflake(1,1);
    Logger logger = LoggerFactory.getLogger(this.getClass());

    //执行本地事务时调用,将订单数据和事务日志写入本地数据库
    @Transactional
    @Override
    public void createOrder(OrderDTO orderDTO,String transactionId){

        //1.创建订单
        Order order = new Order();
        BeanUtils.copyProperties(orderDTO,order);
        orderMapper.createOrder(order);

        //2.写入事务日志
        TransactionLog log = new TransactionLog();
        log.setId(transactionId);
        log.setBusiness("order");
        log.setForeignKey(String.valueOf(order.getId()));
        transactionLogMapper.insert(log);

        logger.info("订单创建完成。{}",orderDTO);
    }

    //前端调用,只用于向RocketMQ发送事务消息
    @Override
    public void createOrder(OrderDTO order) throws MQClientException {
        order.setId(snowflake.nextId());
        order.setOrderNo(snowflake.nextIdStr());
        producer.send(JSON.toJSONString(order),"order");
    }
}

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

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

 <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整合RocketMQ

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

<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</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>4.9.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.5.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>

使⽤SpringBoot集成时,要⾮常注意版本!!!
配置⽂件

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;
@Component
public class SpringProducer {
@Resource
private RocketMQTemplate rocketMQTemplate;
public void sendMessage(String topic,String msg){
this.rocketMQTemplate.convertAndSend(topic,msg);
 }
}

另外,这个rocketMQTemplate不光可以发消息,还可以主动拉消息,消费者的声明也很简单。所有属性通过@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、Push模式
Push模式对于@RocketMQMessageListener注解的处理⽅式,⼊⼝在rocketmq-spring-boot-2.2.2.jar中的
org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration类中。
怎么找到的?评经验猜以及碰运⽓。
这个ListenerContainerConfiguration类继承了Spring当中的SmartInitializingSingleton接⼝,当Spring容器当
中所有⾮懒加载的实例加载完成后,就会触发他的afterSingletonsInstantiated⽅法进⾏初始化。在这个⽅法中会
去扫描所有带有注解@RocketMQMessageListener注解的类,将他注册到内部⼀个Container容器当中。

public void afterSingletonsInstantiated() {
Map<String, Object> beans =
this.applicationContext.getBeansWithAnnotation(RocketMQMessageListener.class)
 .entrySet().stream().filter(entry ->
!ScopedProxyUtils.isScopedTarget(entry.getKey()))
 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
beans.forEach(this::registerContainer);
 }

这⾥这个Container可以认为是客户端实例的⼀个容器,通过这个容器来封装RocketMQ的原⽣API。
registerContainer的⽅法挺⻓的,我这⾥截取出跟今天的主题相关的⼏⾏重要的源码:

private void registerContainer(String beanName, Object bean) {
 .....
 //获取Bean上⾯的注解
RocketMQMessageListener annotation =
clazz.getAnnotation(RocketMQMessageListener.class);
 ...
//检查注解的配置情况
validate(annotation);
String containerBeanName = String.format("%s_%s",
DefaultRocketMQListenerContainer.class.getName(),
counter.incrementAndGet());
GenericApplicationContext genericApplicationContext =
(GenericApplicationContext) applicationContext;
 //将扫描到的注解转化成为Container,并注册到上下⽂中。
genericApplicationContext.registerBean(containerBeanName,
DefaultRocketMQListenerContainer.class,
 () -> createRocketMQListenerContainer(containerBeanName, bean,
annotation));
DefaultRocketMQListenerContainer container =
genericApplicationContext.getBean(containerBeanName,
DefaultRocketMQListenerContainer.class);
 //启动容器,这⾥就相当于是启动了消费者
if (!container.isRunning()) {
try {
container.start();
 } catch (Exception e) {
log.error("Started container failed. {}", container, e);
throw new RuntimeException(e);
 }
 }
log.info("Register the listener to container, listenerBeanName:{},
containerBeanName:{}", beanName, containerBeanName);
 }

到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.BR
OADCASTING);
break;
case CLUSTERING:
consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CL
USTERING);
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的原⽣消费者对象。其中的使⽤⽅式,其实有很多地⽅是很值得借鉴的,尤其是消
费监听的处理。
2、Pull模式
Pull模式的实现其实是通过在RocketMQTemplate实例中注⼊⼀个DefaultLitePullConsumer实例来实现的。只
要注⼊并启动了这个DefaultLitePullConsumer示例后,后续就可以通过template实例的receive⽅法,来调⽤
DefaultLitePullConsumer的poll⽅法,主动去Pull获取消息了。
初始化DefaultLitePullConsumer的代码依然是在rocketmq-spring-boot-2.2.2.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在此基础上做了很多优化。
有兴趣可以⾃⼰研究⼀下。

四、RocketMQ最佳实践

1、合理分配Topic、Tag

⼀个应⽤尽可能⽤⼀个Topic,⽽消息⼦类型则可以⽤tags来标识。tags可以由应⽤⾃由设置,只有⽣产者在发
送消息设置了tags,消费⽅在订阅消息时才可以利⽤tags通过broker做消息过滤:message.setTags(“TagA”)。
Kafka的⼀⼤问题是Topic过多,会造成Partition⽂件过多,影响性能。⽽RocketMQ中的Topic完全不会对消
息转发性能有影响。但是Topic过多,还是会加⼤RocketMQ的元数据维护的性能消耗。所以,在使⽤时,还
是需要对Topic进⾏合理的分配。
使⽤Tag区分消息时,尽量直接使⽤Tag过滤,不要使⽤复杂的SQL过滤。因为消息过滤机制虽然可以减少⽹
络IO,但是毕竟会加⼤Broker端的消息处理压⼒。所以,消息过滤的逻辑,还是越简单越好。

2、使⽤Key加快消息索引

分配好Topic和Tag之后,⾃然就需要优化Key属性了,因为Key也可以参与消息过滤。通常建议每个消息要分配
⼀个在业务层⾯的唯⼀标识码,设置到Key属性中。这有两个⽅⾯的作⽤:
⼀是可以配合Tag进⾏更精确的消息过滤。
另⼀个更重要的⽅⾯是,RocketMQ的Broker端会为每个消息创建⼀个哈希索引。应⽤可以通过topic、key来查
询某⼀条历史的消息内容,以及消息在集群内的处理情况。在管理控制台就可以看到。为了减少哈希索引潜在的哈
希冲突问题,所有官⽅建议,客户端要尽量保证key的唯⼀性。

3、关注错误消息重试

litePullConsumer.setCustomizedTraceTopic(consumerConfig.getCustomizedTraceTopic());
litePullConsumer.setNamespace(consumerConfig.getNamespace());
return litePullConsumer;
 }

我们已经知道RocketMQ的消费者端,如果处理消息失败了,Broker是会将消息重新进⾏投送的。⽽在重试时,
RocketMQ实际上会为每个消费者组创建⼀个对应的重试队列。重试的消息会进⼊⼀个 “%RETRY%”+ConsumeGroup
的队列中。
在这里插入图片描述
多关注重试队列,可以及时了解消费者端的运⾏情况。这个队列中出现了⼤量的消息,就意味着消费者的运⾏出
现了问题,要及时跟踪进⾏⼲预。
然后RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间如下:
在这里插入图片描述
重试次数:
如果消息重试16次后仍然失败,消息将不再投递。转为进⼊死信队列。
然后关于这个重试次数,RocketMQ可以进⾏定制。例如通过consumer.setMaxReconsumeTimes(20);将重试次
数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔均为2⼩时。
配置覆盖:
消息最⼤重试次数的设置对相同GroupID下的所有Consumer实例有效。并且最后启动的Consumer会覆盖之前启
动的Consumer的配置。

4、⼿动处理死信队列

当⼀条消息消费失败,RocketMQ就会⾃动进⾏消息重试。⽽如果消息超过最⼤重试次数,RocketMQ就会认为
这个消息有问题。但是此时,RocketMQ不会⽴刻将这个有问题的消息丢弃,⽽会将其发送到这个消费者组对应的
⼀种特殊队列:死信队列。
通常,⼀条消息进⼊了死信队列,意味着消息在消费处理的过程中出现了⽐较严重的错误,并且⽆法⾃⾏恢复。
此时,⼀般需要⼈⼯去查看死信队列中的消息,对错误原因进⾏排查。然后对死信消息进⾏处理,⽐如转发到正常
的Topic重新进⾏消费,或者丢弃。
死信队列的名称是%DLQ%+ConsumGroup
在这里插入图片描述
死信队列的特征:
⼀个死信队列对应⼀个ConsumGroup,⽽不是对应某个消费者实例。
如果⼀个ConsumeGroup没有产⽣死信队列,RocketMQ就不会为其创建相应的死信队列。
⼀个死信队列包含了这个ConsumeGroup⾥的所有死信消息,⽽不区分该消息属于哪个Topic。
死信队列中的消息不会再被消费者正常消费。
死信队列的有效期跟正常消息相同。默认3天,对应broker.conf中的fileReservedTime属性。超过这个最⻓时
间的消息都会被删除,⽽不管消息是否消费过。
注:默认创建出来的死信队列,他⾥⾯的消息是⽆法读取的,在控制台和消费者中都⽆法读取。这是因为这
些默认的死信队列,他们的权限perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁写,6:可读可写)。需要
⼿动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。

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

在MQ系统中,对于消息幂等有三种实现语义:
at most once 最多⼀次:每条消息最多只会被消费⼀次
at least once ⾄少⼀次:每条消息⾄少会被消费⼀次
exactly once 刚刚好⼀次:每条消息都只会确定的消费⼀次
这三种语义都有他适⽤的业务场景。
其中,at most once是最好保证的。RocketMQ中可以直接⽤异步发送、sendOneWay等⽅式就可以保证。
⽽at least once这个语义,RocketMQ也有同步发送、事务消息等很多⽅式能够保证。
⽽这个exactly once是MQ中最理想也是最难保证的⼀种语义,需要有⾮常精细的设计才⾏。RocketMQ只能保证at
least once,保证不了exactly once。所以,使⽤RocketMQ时,需要由业务系统⾃⾏保证消息的幂等性。
但是,对于exactly once语义,阿⾥云上的商业版RocketMQ是明确有API⽀持的,⾄于如何实现的,就不得⽽知
了。
消息幂等的必要性
在互联⽹应⽤中,尤其在⽹络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可
以概括为以下情况:
发送时消息重复
当⼀条消息已被成功发送到服务端并完成持久化,此时出现了⽹络闪断或者客户端宕机,导致服务端对客户端
应答失败。 如果此时⽣产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且
Message ID 也相同的消息。
投递时消息重复
消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候⽹络闪断。 为
了保证消息⾄少被消费⼀次,消息队列 RocketMQ 的服务端将在⽹络恢复后再次尝试投递之前已被处理过的
消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
负载均衡时消息重复(包括但不限于⽹络抖动、Broker 重启以及订阅⽅应⽤重启)
当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收
到重复消息。
处理⽅式
从上⾯的分析中,我们知道,在RocketMQ中,是⽆法保证每个消息只被投递⼀次的,所以要在业务上⾃⾏来保
证消息消费的幂等性。
⽽要处理这个问题,RocketMQ的每条消息都有⼀个唯⼀的MessageId,这个参数在多次投递的过程中是不会改
变的,所以业务上可以⽤这个MessageId来作为判断幂等的关键依据。
但是,这个MessageId是⽆法保证全局唯⼀的,也会有冲突的情况。所以在⼀些对幂等性要求严格的场景,最好
是使⽤业务上唯⼀的⼀个标识⽐较靠谱。例如订单ID。⽽这个业务标识可以使⽤Message的Key来进⾏传递。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值