一、引言
RocketMQ的研发团队为了帮我们用好它,煞费苦心地在example包内直接写了几十demo。这些demo十分具有价值,几乎可以涵盖所有RocketMQ应用场景!下面我们来一起看看每个demo的使用、作用、应用场景和具体实现吧
先拉个代码:github.com/apache/rock…
目前地球已完成星系飞船建造,马上就要进行星际旅行。
二、demo讲解
1.星系远航计划
1.1 引言
让我们来看example包中的quickstart子包,这里定义了最典型demo,我们来看看一个数据的传输的过程是什么样的。
1.2 example.quickstart.Producer
1.2.1 发送交互模式
这里其实演示了生产者的三种重要发送模式,即同步发送、异步发送和单向发送
同步发送模式:投递数据后会同步等待结果。
异步发送模式:投递数据后不会同步等待结果,而是通过异步回调获取结果
单程发送模式:投递数据后既不会同步等结果,也不会通过异步回调获取结果,投递完之后就不管了
这三种发送模式,其实是让使用者根据具体场景对性能和可靠性进行权衡选择
性能:单程发送>异步发送>同步发送
可靠性:单程发送<异步发送<同步发送
我们从producer.send(msg)点进去看源码可以发现,RocketMQ其实定义了以上三种交互模式的枚举
public enum CommunicationMode {
SYNC,
ASYNC,
ONEWAY,
}
1)同步发送模式
这里会通过切换broker尝试重试,提升可靠性
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) {
...
// 获取重试次数,默认是2次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
...
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
// 没有获取到成功的结果时,会尝试重试(此时要特别注意只有开启了重试切换Broker,才会重试,不然就直接返回错误的结果了)
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
return sendResult;
default:
break;
}
}
...
}
2)异步发送模式
SendCallback是一个接口函数参数,用匿名类实现一下成功和异常方法就好
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// do something
}
@Override
public void onException(Throwable e) {
// do something
}
});
收到broker消息后把结果作为回调函数onSuccess的参数传入,同理异常也是这样传递的
private void sendMessageAsync(...) {
...
sendCallback.onSuccess(sendResult);
...
}
3)单程发送模式
...
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
...
switch (communicationMode) {
...
// 虽然是在循环里,但是直接return null了,所以也不会去进行切换broker的尝试
case ONEWAY:
return null;
...
}
}
...
1.2.2 超时机制
超时时间默认是3000ms,会在整个投递周期通过函数传递,在每一次函数传递前会减去本函数消耗的时间,然后在函数中会有超时时间的检测代码,一旦超时则停止运行,并抛出异常
// 判断当前运行是否超时
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout < costTime) {
callTimeout = true;
break;
}
// 将剩余的超时时间减少,传递到下个函数
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
// 抛出异常
MQClientException mqClientException = new MQClientException(info, exception);
if (callTimeout) {
throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
}
1.2.3 tag标签
在创建消息对象时,传入的tag标签,可以用于筛选数据
消费者可配置只获取某tag标签的数据
Message msg = new Message(*TOPIC* , TAG, "Hello RocketMQ");
1.3 example.quickstart.Consumer
1.3.1 消费者组
接下来是RocketMQ的重磅概念消费者组,消费者组有什么用呢?
高并发:同一个消费者组内的消费者可负责均衡,并可通过增加新的消费者实现水平扩展
高可用:某消费者发生故障,消息将分发给其他正常消费者
消息顺序性:在同一个消费组内可以保证消息按发送的顺序被消费
1.3.2 消费开始位置
CONSUME_FROM_LAST_OFFSET:从上次偏移量开始消费。之前没有消费记录,则从最后的偏移量开始消费。适用于需要顺序消费的场景
CONSUME_FROM_FIRST_OFFSET:从最早的偏移量开始消费。及时已有消费记录,也会从最早的偏移量重新开始消费。适用于需要消费历史数据的场景
CONSUME_FROM_TIMESTAMP:从指定时间戳开始消费。消费者会从该时间戳之后的消息开始消费。适用于需要从某个时间点开始消费历史数据的场景
1.3.3 指定订阅tags
tags用于标记当前消费者能获取到哪些tag的数据,tag是消息的一个属性(即消息的标签)
如果是"*",则会消费所有tag的数据
如果是"order||store",则会消费order、store两个tag的数据
// 指定tags为*,则该消费者会消费所有tag
consumer.subscribe(TOPIC, "*");
// 如果是else会通过|切割多个tag,并消费对应的多个tag。比如 order|store,则会订阅order、store两个tag
public final static String SUB_ALL = "*";
if (null == subString || subString.equals(SubscriptionData.SUB_ALL) || subString.length() == 0) {
subscriptionData.setSubString(SubscriptionData.SUB_ALL);
} else {
String[] tags = subString.split("\\|\\|");
if (tags.length > 0) {
for (String tag : tags) {
if (tag.length() > 0) {
String trimString = tag.trim();
if (trimString.length() > 0) {
subscriptionData.getTagsSet().add(trimString);
subscriptionData.getCodeSet().add(trimString.hashCode());
}
}
}
} else {
throw new Exception("subString split error");
}
}
1.3.4 消息监听器类型
并发消息监听器:可配置多个线程并发消费多个消息
顺序消息监听器:单个线程依次消费消息
通过给监听器注册函数传入不一样的监听器实现该功能
// 并发消息监听器
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
...
});
// 顺序消费监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
...
}
2.日用品装载
2.1 example.simple
该包中列举了最常使用的生产者、消费者demo,具体包括客户端鉴权、同步/异步/单程生产者、推/拉/轻量级拉模式消费者
2.1.1 客户端鉴权
生产者和消费者都可以传入鉴权对象
// 获取鉴权对象方法
static RPCHook getAclRPCHook() {
return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY,ACL_SECRET_KEY));
}
// 生产者鉴权
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook());
// 推模式消费者鉴权
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5", getAclRPCHook(), new AllocateMessageQueueAveragely());
// 拉模式消费者鉴权
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_6", getAclRPCHook());
2.1.2 同步/异步/单程生产者
详细可见1.2.1
2.1.3 推/拉模式消费者
推模式:Broker主动推送消息给消费者,消费者注册消息监听器,消息到达时异步通知。适用于实时性要求高,消费者可及时处理的场景
拉模式:消费者主动地调用api获取消息,可控制拉取的频率和批量大小。适用于实时性要求低,消费者需要精确控制拉取的场景(速度、数据量、进度)
轻量级拉模式:拉模式的变种,具有更低的资源开销和更高的消费性能。适用于实时性要求较高,又需要精确控制拉取的场景 注:4.9.4版本已将拉模式的实现类设置为废弃状态,可见官方推荐使用轻量级拉模式代替拉模式
// RocketMQ定义了枚举用以在代码中区分推和拉的场景
public enum ConsumeType {
CONSUME_ACTIVELY("PULL"),
CONSUME_PASSIVELY("PUSH");
}
// 推模式很好理解,定义一个并发监听器匿名类,实现consumeMessage方法就行,消息到的时候会去执行consumeMessage方法
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;
}
});
// 拉模式需要自己获取消息队列,并计算和存储偏移量,pull方法的最后一个参数是每次拉取的数据量
Set<MessageQueue> messageQueues = consumer.fetchMessageQueuesInBalance(topic);
for (MessageQueue messageQueue : messageQueues) {
long offset = this.consumeFromOffset(messageQueue);
pullResult = consumer.pull(messageQueue, "*", offset, 32);
}
// 轻量级拉取给消息队列选择、偏移量处理提供了默认实现,但也可以指定消息队列和偏移量进行消费
// 使用默认实现
DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("lite_pull_consumer_test");
litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
while (running) {
List<MessageExt> messageExts = litePullConsumer.poll();
}
// 指定消息队列和偏移量
DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("please_rename_unique_group_name");
litePullConsumer.seek(assignList.get(0), 10);// 依次是消息队列的选择和偏移量的值
while (running) {
List<MessageExt> messageExts = litePullConsumer.poll();
litePullConsumer.commitSync();
}
2.1.4 推模式并发消费失败重试机制
当推模式并发消费状态是RECONSUME_LATER时,消息会尝试被重新投递,然后被同个消费组的消费者再次消费
当抛出异常或返回null会将返回状态改为RECONSUME_LATER
public enum ConsumeConcurrentlyStatus {
// Success consumption
CONSUME_SUCCESS,
// Failure consumption,later try to consume
RECONSUME_LATER;
}
重试通过三个队列实现,失败后先将消息投递给3级的延迟队列,时间到之后会被投递到重试队列(每个消费者组有一个重试队列),下一次再失败会投递4级的延迟队列,重复这个过程直到超过16次,则会将数据推送到死信队列(每个消费者组有一个死信队列)
3. 燃料装载
3.1 example.broadcast.PushConsumer
3.1.1 消费消息模式
广播模式:每一个消费者都会收到一个主题的所有消息
集群模式:多个消费者组合成一个消费者组消费同一个主题的数据时,消息被均匀的分给消费者组里的每一个消费者
public enum MessageModel {
BROADCASTING("BROADCASTING"),
CLUSTERING("CLUSTERING");
}
1)广播消费消息模式
定义Consumer对象后,可通过修改messageModel实现广播消费消息模式(默认是集群模式)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setMessageModel(MessageModel.BROADCASTING);
2)集群消费消息模式
在构建消费者对象时,修改MessageModel类型即可
final MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService("GroupName1");
scheduleService.setMessageModel(MessageModel.CLUSTERING);
4. 人员清点
4.1 example.batch.SimpleBatchProducer
利用ArrayList简单的实现了批量投递,减少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()));
SendResult sendResult = producer.send(messages);
4.2 example.batch.SplitBatchProducer
其实就是提供了大批量数据上报的自动分页上报(一次最多报1000000个字符串),防止单次IO上报过多数据
private static final int SIZE_LIMIT = 1000 * 1000;
//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);
}
5. 非相关人员疏散
5.1 tag过滤
老朋友了,具体可看1.2.3和1.3.3
5.2 sql过滤
订阅的时候可以指定sql,进行细致化的过滤
1)生产者
指定tags和用户属性a
String[] tags = new String[] {"TagA", "TagB", "TagC"};
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);
2) 消费者
消费者订阅的时候指定tag和用户属性的规则
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)"));
6. 最后检查
6.1 example.namespace
演示如果使用命名空间,其实就是给生产者、消费者进行业务划分的
// 生产者
DefaultMQProducer producer = new DefaultMQProducer(NAMESPACE, PRODUCER_GROUP);
// 拉模式消费者
DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer(NAMESPACE, CONSUMER_GROUP);
// 推模式生产者
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer(NAMESPACE, CONSUMER_GROUP);
6.2 example.openmessaging
这里其实是openMessage标准的实现,该标准旨在以统一的方式与不同的消息中间件进行交互,有需要的可以看看
6.3 example.operation
提供了命令行实现,可以打好java包,作为linux命令工具使用
6.4 example.schedule
给出了延时队列的使用demo,核心就看下面这个地方
message.setDelayTimeLevel(3);
6.5 example.tracemessage
是OpenTracing开放分布式式追踪标准的使用实现,该标准用于跟踪和分析分布式系统的请求流程和性能瓶颈
具体实现是自定义Tracer类实现,并通过OpenTracingHook勾子把Tracer对象传进去即可
Tracer tracer = initTracer();
producer.getDefaultMQProducerImpl().registerSendMessageHook(new SendMessageOpenTracingHookImpl(tracer));
7. 开始倒计时
7.1 example.transaction
重磅知识点事务消息的发送
核心就在对TransactionListener接口的实现
本地事务逻辑呢,写在executeLocalTransaction就好,如果本地事务失败,就返回ROLLBACK_MESSAGE,那么事务则不会传递给消费者
检查本地事务执行状态,是用来在网络不稳定的情况时,broker主动调用producer的checkLocalTransaction方法,判断目前事务状态如果是COMMIT_MESSAGE,则将消息传递给消费者
public class MyTransactionListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行本地事务逻辑,并根据事务执行结果返回对应的状态
// 返回值可以是 COMMIT_MESSAGE、ROLLBACK_MESSAGE 或 UNKNOW
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 检查本地事务的执行状态,并返回对应的状态
// 返回值可以是 COMMIT_MESSAGE、ROLLBACK_MESSAGE 或 UNKNOW
}
}
8. 起飞失败,人员无伤亡
8.1 example.rpc
RocketMQ也可以很好的承接远程调用的工作
1) 同步生产消费
核心逻辑是生产者发送消息后,等消费者返回对应结果
// 生产者
Message msg = new Message(topic,
"",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
long begin = System.currentTimeMillis();
Message retMsg = producer.request(msg, ttl);
// 消费者,通过 MessageUtil.createReplyMessage包装消息,会使用REPLY_TOPIC实现原路返回投递
Message replyMessage = MessageUtil.createReplyMessage(msg, replyContent);
SendResult replyResult = replyProducer.send(replyMessage, 3000);
2)异步生产
无非传入匿名类并实现回调函数,等待结果回调
producer.request(msg, new RequestCallback() {
@Override
public void onSuccess(Message message) {
long cost = System.currentTimeMillis() - begin;
System.out.printf("request to <%s> cost: %d replyMessage: %s %n", topic, cost, message);
}
@Override
public void onException(Throwable e) {
System.err.printf("request to <%s> fail.", topic);
}
}, ttl);
三、结语
我们学习技术和使用的过程,何尝不像星系飞船的首次起飞呢?稍有不慎便产生技术债务或生产环境问题。
由此我们可以看出如果能深刻地理解官方demo,则可以站在全局的体系化的角度看待每一个业务需求,给出最佳的解决方案。
高山仰止,景行行止,虽不能至,然心向往之。
星际穿越何其困难,虽不能至,但也要保持积极向上的心态和行动,迎接下一次挑战。