目录
本地消息表+定时扫描 方案,和本地消息表事务机制类似,也是采用 本地消息表+定时扫描 相结合的架构方案。
消息的发送流程
Rocketmq和KafKa类似(实质上,最早的Rocketmq 就是KafKa 的Java版本),一条消息从生产到被消费,将会经历三个阶段:
-
生产阶段,Producer 新建消息,而后经过网络将消息投递给 MQ Broker。这个发送可能会发生丢失,比如网络延迟不可达等。
-
存储阶段,消息将会存储在 Broker 端磁盘中,Broker 根据刷盘策略持久化到硬盘中,刚收到Producer的消息在内存中了,但是如果Broker 异常宕机了,导致消息丢失。
-
消费阶段, Consumer 将会从 Broker 拉取消息
以上任一阶段, 都可能会丢失消息,只要这三个阶段0丢失,就能够完全解决消息丢失的问题。
宏观层面的大的阶段和流程,Rocketmq和KafKa类似的。KafKa 零丢失,具体的文章:
生产阶段如何实现0丢失方式
生产阶段有三种send方法:
-
同步发送
-
异步发送
-
单向发送。
三种send方法的 客户端api,具体如下:
/**
* {@link org.apache.rocketmq.client.producer.DefaultMQProducer}
*/
// 同步发送
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {}
// 异步发送,sendCallback作为回调
public void send(Message msg,SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {}
// 单向发送,不关心发送结果,最不靠谱
public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {}
produce要想发消息时保证消息不丢失,可以采用同步发送的方式去发消息,send消息方法只要不抛出异常,就代表发送成功。
发送成功会有多个SendResult 状态,以下对每个状态进行说明:
-
SEND_OK:消息发送成功,Broker刷盘、主从同步成功
-
FLUSH_DISK_TIMEOUT:消息发送成功,但是服务器同步刷盘(默认为异步刷盘)超时(默认超时时间5秒)
-
FlUSH_SLAVE_TIMEOUT:消息发送成功,但是服务器同步复制(默认为异步复制)到Slave时超时(默认超时时间5秒)
-
SLAVE_NOT_AVAILABLE:Broker从节点不存在
注意:同步发送只要返回以上四种状态,就代表该消息在生产阶段消息正确的投递到了RocketMq,生产阶段没有丢失。
如果业务要求严格,可以使用同步发送,并且只取SEND_OK标识消息发送成功,
其他返回值类型的数据,采用40岁老架构师尼恩给大家设计的,在业务维度的 终极0丢失保护措施:本地消息表+定时扫描 (具体参见本文末尾)
是同步发送还是异步发送
根据尼恩的架构设计40个黄金法则 ,AP 和 CP 是天然的矛盾, 到底是 CP 还是 AP的 需要权衡,
-
同步发送的方式 是 CP ,高可靠,但是性能低。
-
异步发送的方式 是 AP ,低可靠,但是性能高。
为了高可靠(CP),可以采取同步发送的方式进行发送消息,发消息的时候会同步阻塞等待broker返回的结果,如果没成功,则不会收到SendResult,这种是最可靠的。
其次是异步发送,再回调方法里可以得知是否发送成功。
最后,单向发送(OneWay)是最不靠谱的一种发送方式,我们无法保证消息真正可达。
当然,具体的如何选择高可用方案,还是要看业务。
为了确保万无一失,可以选择异步发送 + 业务维度的 终极0丢失保护措施 , 实现消息的0丢失。
生产端的失败重试策略
发送消息如果失败或者超时了,则会自动重试。
同步发送默认是重试三次,可以根据api进行更改,比如改为10次:
producer.setRetryTimesWhenSendFailed(10);
其他模式是重试1次,具体请参见源码
/**
* {@link org.apache.rocketmq.client.producer.DefaultMQProducer#sendDefaultImpl(Message, CommunicationMode, SendCallback, long)}
*/
// 自动重试次数,this.defaultMQProducer.getRetryTimesWhenSendFailed()默认为2,如果是同步发送,默认重试3次,否则重试1次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
// 选择发送的消息queue
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
try {
// 真正的发送逻辑,sendKernelImpl。
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
// 如果发送失败了,则continue,意味着还会再次进入for,继续重试发送
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
// 发送成功的话,将发送结果返回给调用者
return sendResult;
default:
break;
}
} catch (RemotingException e) {
continue;
} catch (...) {
continue;
}
}
}
上面的核心逻辑中,调用sendKernelImpl真正的去发送消息
通过核心的发送逻辑,可以看出如下: