RocketMQ(二):揭秘发送消息核心原理(源码与设计思想解析)
上篇文章主要介绍消息中间件并以RocketMQ架构展开描述其核心组件以及MQ运行流程
本篇文章以Product的视角来看看发送消息的核心原理与设计思想,最后以图文并茂的方式描述出发送消息的核心流程
消息发送方式
RocketMQ中普通消息提供三种发送方式:同步、异步、单向
上篇文章中我们已经使用封装好的API延时过同步发送
在使用三种方式前,我们先来理解它们的理论知识
同步发送:发送完消息后,需要阻塞直到收到Broker的响应,通常用于数据一致性较高的操作,需要确保消息到达Broker并持久化
同步发送收到响应并不一定就是成功,还需要根据响应状态进行判断
SendResult响应状态包括:
- SEND_OK:发送成功
- FLUSH_DISK_TIMEOUT:刷盘超时
- FLUSH_SLAVE_TIMEOUT:同步到备超时
- SLAVE_NOT_AVAILABLE:备不可用
(这些状态与设置的刷盘策略有关,后续保证消息可靠的文章再进行详细展开说明,本篇文章还是回归主线探究发送消息)
异步发送:发送完消息后立即响应,不需要阻塞等待,但需要设置监听器,当消息成功或失败时进行业务处理,可以在失败时进行重试等其他逻辑保,通常用于追求响应时间的场景
异步发送相当于同步发送,需要新增SendCallback回调来进行后续成功/失败的处理,并且异步发送没有返回值
@GetMapping("/asyncSend")
public String asyncSend() {
producer.sendAsyncMsg(topic, "tag", "async hello world!", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("消息发送成功{}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.info("消息发送失败", throwable);
//记录后续重试
}
});
return "asyncSend ok";
}
原生API封装:
public void sendAsyncMsg(String topic, String tag, String jsonBody, SendCallback sendCallback) {
Message message = new Message(topic, tag, jsonBody.getBytes(StandardCharsets.UTF_8));
try {
producer.send(message, sendCallback);
} catch (MQClientException | RemotingException | InterruptedException e) {
throw new RuntimeException(e);
}
}
单向发送:只要发出消息就响应,性能最好,通常用于追求性能,不追求可靠的场景,如:异步日志收集
由于单向发送的特性,即不需要回调也没有返回结果
@GetMapping("/sendOnewayMsg")
public String onewaySend() {
producer.sendOnewayMsg(topic, "tag", "oneway hello world!");
return "sendOnewayMsg ok";
}
原生API封装:
public void sendOnewayMsg(String topic, String tag, String jsonBody) {
Message message = new Message(topic, tag, jsonBody.getBytes(StandardCharsets.UTF_8));
try {
producer.sendOneway(message);
} catch (MQClientException | RemotingException | InterruptedException e) {
throw new RuntimeException(e);
}
}
发送消息原理
在研究发送消息的原理前,不妨来思考下,如果让我们实现,我们要思考下需要哪些步骤?
像我们平时进行业务代码编写前的第一步就是进行参数校验,因为要防止参数“乱填”的意外情况
然后由于需要找到对应的Broker,那肯定要获取Topic路由相关信息
这个路由信息前文说过是从NameServer集群定时获取即时更新的,那么客户端的内存里肯定会进行存储
像这样的数据肯定是类似于多级缓存的,先在本地缓存,如果本地没有或者本地是旧数据,那么就网络通信再去远程(NameServer)获取一份后再更新本地缓存
获取完路由信息后,可以通过设置的Topic获取对应的MessageQueue队列信息,因为Topic下可能有很多队列,因此需要负载均衡算法决定要发送的队列
rocketmq发送消息还提供超时、重试等机制,因此在这个过程中需要计算时间、重试次数
最后发送消息会进行网络通信,我们要选择合适的工具进行RPC
总结一下,如果让我们设计起码要有这些流程:参数校验、获取路由信息、根据负载均衡算法选择队列、计算超时,重试次数、选择网络通信RPC工具…
在设计完流程后,如果我们是一位”成熟的设计师“,那么一定会将这些步骤中通用的步骤抽象成模板,模板可以作为三种发送消息通用方式,而那些变动的就是策略,解耦互不影响,并在重要的流程前后留下”钩子“,方便让使用者进行扩展
rocketmq流程与我们设计、思考的流程类似,先准备一张最终的流程图,方便跟着流程图一起阅读源码:
sendDefaultImpl 通用发送消息模板
通过三种发送方式,都会来到DefaultMQProducerImpl.sendDefaultImpl
这个就是通用方法的模板
代码块中只展示部分关键代码,流程如下:
- 参数校验 Validators.checkMessage
- 获取路由信息 tryToFindTopicPublishInfo
- 选择一个要发送的MessageQueue selectOneMessageQueue
- 发送消息 sendKernelImpl
在3、4步骤中还会进行重试、超时判断等
private SendResult sendDefaultImpl(
//消息
Message msg,
//方式
final CommunicationMode communicationMode,
//异步的回调
final SendCallback sendCallback,
//超时时间
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
//参数校验