【业务实战1】一文学会RocketMQ核心操作

281 阅读11分钟

一、引言

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,则可以站在全局的体系化的角度看待每一个业务需求,给出最佳的解决方案。

高山仰止,景行行止,虽不能至,然心向往之。

星际穿越何其困难,虽不能至,但也要保持积极向上的心态和行动,迎接下一次挑战。