这篇文章我们聊聊广播消费,因为广播消费在某些场景下真的有奇效。笔者会从基础概念实现机制实战案例注意事项四个方面一一展开,希望能帮助到大家。

1 基础概念

RocketMQ 支持两种消息模式:集群消费( Clustering )和广播消费( Broadcasting )。

集群消费

同一 Topic 下的一条消息只会被同一消费组中的一个消费者消费。也就是说,消息被负载均衡到了同一个消费组的多个消费者实例上。

广播消费

当使用广播消费模式时,每条消息推送给集群内所有的消费者,保证消息至少被每个消费者消费一次。

2 源码解析

首先下图展示了广播消费的代码示例。

public class PushConsumer {
public static final String CONSUMER_GROUP = "myconsumerGroup";
public static final String DEFAULT_NAMESRVADDR = "localhost:9876";
public static final String TOPIC = "mytest";
public static final String SUB_EXPRESSION = "TagA || TagC || TagD"; public static void main(String[] args) throws InterruptedException, MQClientException {
// 定义 DefaultPushConsumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
// 定义名字服务地址
consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 定义消费读取位点
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 定义消费模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 订阅主题信息
consumer.subscribe(TOPIC, SUB_EXPRESSION);
// 订阅消息监听器
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
try {
for (MessageExt messageExt : msgs) {
System.out.println(new String(messageExt.getBody()));
}
}catch (Exception e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}); consumer.start();
System.out.printf("Broadcast Consumer Started.%n");
}
}

和集群消费不同的点在于下面的代码:

consumer.setMessageModel(MessageModel.BROADCASTING);

接下来,我们从源码角度来看看广播消费和集群消费有哪些差异点 ?

首先进入 DefaultMQPushConsumerImpl 类的 start 方法 , 分析启动流程中他们两者的差异点:

▍ 差异点1:拷贝订阅关系

private void copySubscription() throws MQClientException {
try {
Map<String, String> sub = this.defaultMQPushConsumer.getSubscription();
if (sub != null) {
for (final Map.Entry<String, String> entry : sub.entrySet()) {
final String topic = entry.getKey();
final String subString = entry.getValue();
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subString);
this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
}
}
if (null == this.messageListenerInner) {
this.messageListenerInner = this.defaultMQPushConsumer.getMessageListener();
}
// 注意下面的代码 , 集群模式下自动订阅重试主题
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
break;
case CLUSTERING:
final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL);
this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
break;
default:
break;
}
} catch (Exception e) {
throw new MQClientException("subscription exception", e);
}
}

在集群模式下,会自动订阅重试队列,而广播模式下,并没有这段代码。也就是说广播模式下,不支持消息重试

▍ 差异点2:本地进度存储

switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);

我们可以看到消费进度存储的对象是: LocalFileOffsetStore , 进度文件存储在如下的主目录 /{用户主目录}/.rocketmq_offsets

public final static String LOCAL_OFFSET_STORE_DIR = System.getProperty(
"rocketmq.client.localOffsetStoreDir",
System.getProperty("user.home") + File.separator + ".rocketmq_offsets");

进度文件是 /mqClientId/{consumerGroupName}/offsets.json

this.storePath = LOCAL_OFFSET_STORE_DIR + File.separator + this.mQClientFactory.getClientId() + File.separator + this.groupName + File.separator + "offsets.json";

笔者创建了一个主题 mytest , 包含4个队列,进度文件内容如下:

消费者启动后,我们可以将整个流程简化如下图,并继续整理差异点:

▍ 差异点3:负载均衡消费该主题的所有 MessageQueue

进入负载均衡抽象类 RebalanceImplrebalanceByTopic方法 。

private void rebalanceByTopic(final String topic, final boolean isOrder) {
switch (messageModel) {
case BROADCASTING: {
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
if (mqSet != null) {
boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
// 省略代码
} else {
log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
}
break;
}
case CLUSTERING: {
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
// 省略代码
if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet); Collections.sort(mqAll);
Collections.sort(cidAll); AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy; List<MessageQueue> allocateResult = null;
try {
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
// 省略日志打印代码
return;
}
Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}
boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
//省略代码
}
break;
}
default:
break;
}
}

从上面代码我们可以看到消息模式为广播消费模式时,消费者会订阅该主题下所有的 messageQueue ,这一点也可以从本地的进度文件 offsets.json 得到印证。

▍ 差异点4:不支持顺序消息

顺序消费会向 Borker 申请锁 。消费者根据分配的队列 messageQueue ,向 Borker 申请锁 ,如果申请成功,则会拉取消息,如果失败,则定时任务每隔20秒会重新尝试。

if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
ConsumeMessageOrderlyService.this.lockMQPeriodically();
} catch (Throwable e) {
log.error("scheduleAtFixedRate lockMQPeriodically exception", e);
}
}
}, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
}

从上面的代码,我们发现只有在集群消费的时候才会定时申请锁,这样就会导致广播消费时,无法为负载均衡的队列申请锁,导致拉取消息服务一直无法获取消息数据。

为了再次验证,我们修改例子,消费模式从并发消费修改为顺序消费 。

consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
try {
for (MessageExt messageExt : msgs) {
System.out.println(new String(messageExt.getBody()));
}
}catch (Exception e) {
e.printStackTrace();
}
return ConsumeOrderlyStatus.SUCCESS;
});

从图中,笔者观察到拉取消息的线程无法发起拉取消息请求到 Broker ,因为负载均衡后的队列无法获取到锁。

因此,广播消费模式并不支持顺序消息

▍ 差异点5:并发消费消费失败时,没有重试

进入并发消息消费类ConsumeMessageConcurrentlyService 的处理消费结果方法processConsumeResult

switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
MessageExt msg = consumeRequest.getMsgs().get(i);
log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
}
break;
case CLUSTERING:
List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
MessageExt msg = consumeRequest.getMsgs().get(i);
boolean result = this.sendMessageBack(msg, context);
if (!result) {
msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
msgBackFailed.add(msg);
}
} if (!msgBackFailed.isEmpty()) {
consumeRequest.getMsgs().removeAll(msgBackFailed); this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
}
break;
default:
break;
}

消费消息失败后,集群消费时,消费者实例会通过 CONSUMER_SEND_MSG_BACK 请求,将失败消息发回到 Broker 端。

但在广播模式下,仅仅是打印了消息信息。因此,广播模式下,并没有消息重试

3 实战案例

笔者第一次接触广播消费的业务场景是神州专车司机端消息推送。 用户下单之后,订单系统生成专车订单,派单系统会根据相关算法将订单派给某司机,司机端就会收到派单推送。

推送架构图如下:

司机端启动后,会通过负载均衡和推送服务创建长连接,推送服务会保存 TCP 连接引用 (比如司机编号和 TCP channel 的引用)。

推送服务是一个 TCP 服务(自定义协议),同时也是一个消费者服务,消息模式是广播消费。

派单服务是生产者,将派单数据发送到 MetaQ , 每个推送服务都会消费到该消息,推送服务判断本地内存中是否存在该司机的 TCP channel , 若存在,则通过 TCP 连接将数据推送给司机端。

肯定有同学会问:假如网络原因,推送失败怎么处理 ?有两个要点:

  1. 司机端定时主动拉取派单信息;
  2. 当推送服务没有收到司机端的 ACK 时 ,也会一定时限内再次推送,达到阈值后,不再推送。

4 注意事项

集群消费和广播消费模式下,各功能的支持情况如下:

功能 集群消费 广播消费
顺序消息 支持 不支持
重置消费位点 支持 不支持
消息重试 支持 不支持
消费进度 服务端维护 客户端维护

参考资料 :

https://www.51cto.com/article/714277.html

https://ost.51cto.com/posts/21100


如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!

深入理解RocketMQ 广播消费的更多相关文章

  1. 广播消费:允许一个 Group ID 所标识的所有 Consumer 都会各自消费某条消息一次。

    什么是消息队列 RocketMQ?_消息队列 RocketMQ-阿里云 https://help.aliyun.com/document_detail/29532.html 2019-01-30 16 ...

  2. RocketMQ(7)---RocketMQ顺序消费

    RocketMQ顺序消费 如果要保证顺序消费,那么他的核心点就是:生产者有序存储.消费者有序消费. 一.概念 1.什么是无序消息 无序消息 无序消息也指普通的消息,Producer 只管发送消息,Co ...

  3. 深入理解RocketMQ的消费者组、队列、Broker,Topic

    1.遇到的问题:上测试环境,上次描述的鸟问题又出现了,就是生产者发3条数据,我这边只能收到1条数据. 2.问题解决: (1)去控制台看我的消费者启动情况,貌似没什么问题 , (2)去测试服务器里看日志 ...

  4. 一次 RocketMQ 顺序消费延迟的问题定位

    一次 RocketMQ 顺序消费延迟的问题定位 问题背景与现象 昨晚收到了应用报警,发现线上某个业务消费消息延迟了 54s 多(从消息发送到MQ 到被消费的间隔): 2021-06-30T23:12: ...

  5. RocketMQ - 消费者消费方式

    RocketMQ的消费方式包含Pull和Push两种 Pull方式:用户主动Pull消息,自主管理位点,可以灵活地掌控消费进度和消费速度,适合流计算.消费特别耗时等特殊的消费场景.缺点也显而易见,需要 ...

  6. 【转】RocketMQ事务消费和顺序消费详解

    RocketMQ事务消费和顺序消费详解 转载说明:该文章纯转载,若有侵权或给原作者造成不便望告知,仅供学习参考. 一.RocketMq有3中消息类型 1.普通消费 2. 顺序消费 3.事务消费 顺序消 ...

  7. rocketmq广播消息的(五)

    一.简介 广播消费指的是:一条消息被多个consumer消费,即使这些consumer属于同一个ConsumerGroup,消息也会被ConsumerGroup中的每个Consumer都消费一次,广播 ...

  8. RocketMq顺序消费

    部分内容出处   https://www.jianshu.com/p/453c6e7ff81c rocketmq内部有4个默认的队里,在发送消息时,同一组的消息需要按照顺序,发送到相应的mq中,同一组 ...

  9. 关于RocketMQ消息消费与重平衡的一些问题探讨

    其实最好的学习方式就是互相交流,最近也有跟网友讨论了一些关于 RocketMQ 消息拉取与重平衡的问题,我姑且在这里写下我的一些总结. ## 关于 push 模式下的消息循环拉取问题 之前发表了一篇关 ...

  10. 重新理解RocketMQ Commit Log存储协议

    本文作者:李伟,社区里大家叫小伟,Apache RocketMQ Committer,RocketMQ Python客户端项目Owner ,Apache Doris Contributor,腾讯云Ro ...

随机推荐

  1. NameError: name 'List' is not defined

    当在python出现该问题是,使用from typing import List.

  2. PicoRV32-on-PYNQ-Z2: An FPGA-based SoC System——RISC-V On PYNQ项目复现

    本文参考: 1️⃣ 原始工程 2️⃣ 原始工程复现教程 3️⃣ RISCV工具链安装教程 本文工程: https://bhpan.buaa.edu.cn:443/link/4B08916BF2CDB4 ...

  3. FPGA加速技术详解:从原理到应用

    目录 FPGA加速技术详解:从原理到应用 背景介绍: 随着计算机性能的不断提高和运算能力的增强,GPU.CPU等高性能计算硬件已经可以满足大部分计算任务的需求.然而,对于大规模.复杂的实时数据处理和高 ...

  4. 安装VMware Workstation 16 Pro

    下载 官网:https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html 注:我是在新毒霸软件管 ...

  5. go NewTicker 得使用

    转载请注明出处: 在 Go 语言中,time.NewTicker 函数用于创建一个周期性触发的定时器.它会返回一个 time.Ticker 类型的值,该值包含一个通道 C,定时器会每隔一段时间向通道 ...

  6. Github入门教程(新版)

    GitHub 的介绍与使用 GitHub 注册一个账号 直接在首页注册即可啦 要注意的是 第一项 username 别人是可见的 后面修改也会比较麻烦,所以起个好名字很重要 个人主页介绍 刚注册好的页 ...

  7. java使用SFTP连接服务器下载,上传文件

    package mocha.framework.util; /* * @author Xiehj * @version 2019年10月28日 上午9:37:28 */ import java.io. ...

  8. 在行情一般的情况下,就说说23级应届生如何找java工作

    Java应届生找工作,不能单靠背面试题,更不能在简历中堆砌和找工作关系不大的校园实践经历,而是更要在面试中能证明自己的java相关商业项目经验.其实不少应届生Java求职者不是说没真实Java项目经验 ...

  9. 数据库是要拿来用的,不是用来PK先进性的

    周五参加了WAIC后又和一家上海本地的数据库厂商交流了一下午.等我要买高铁票回南京的时候已经买不到票了.好不容易刷到一张到苏州北的高铁票,我就上了车.上车后突然想起还不如就回苏州老家住一晚算了.到家后 ...

  10. [shell]在curl测试的data参数中引用变量

    在curl测试的data参数中引用变量 前言 在使用curl接口进行接口传参时,常会使用如下方法: #!/bin/bash url="http://192.168.0.10:8000/api ...