今天的博客主题
MQ消息中间件 --》RocketMQ --》RocketMQ进阶(四)
本文主要讲解RocketMQ生产者消费者的消息重试机制及底层实现原理刨根问底
生产者消息重试
生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。
消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。
消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、消费者也会导致重复消费消息。
如下方法可以设置消息重试策略:
retryTimesWhenSendFailed:同步发送失败重投次数,默认为2。生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,向其他broker发送,最大程度保证消息不丢。
超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。
retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。
retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分的重要消息可以开启。
DefaultMQProducer 属性可以设置,默认是2,但是重试机制是关闭的,没有开启。
// 在同步模式下发送失败在内部执行的最大重试次数。 可能会导致消息重复,这由应用程序开发人员来解决。
private int retryTimesWhenSendFailed = 2;
// 在异步模式下发送失败在内部执行的最大重试次数。 可能会导致消息重复,这由应用程序开发人员来解决。
private int retryTimesWhenSendAsyncFailed = 2;
// 在内部发送失败时是否重试其他代理。
private boolean retryAnotherBrokerWhenNotStoreOK = false;
在这个里面提供的属性只要有 getter/setter 方法 都可以去调试更改默认值。
我这边用是rocketmq-spring-boor-starter 只要在配置文件增加配置即可
rocketmq:
name-server: 127.0.0.1:9876
producer:
# 开启内部消息重试
retry-next-server: true
DefaultMQProducerImpl.sendDefaultImpl(); 生产者发送消息实现
private SendResult sendDefaultImpl(){
// 部分代码省略
// ...
// 消息发送次数
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
// 发送消息核心实现
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
// 同步发送 返回结果不是SEND_OK
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
// 发送失败是否重试其他代理(注意:默认未开启)
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
// 跳过当前循环发送,进行下一次发送
continue;
}
}
return sendResult;
default:
break;
}
// 这个发送是被try{}catch(){}了的
// 如果发生了 RemotingException MQClientException MQBrokerException 异常时候都会被重试
}
}
这里就是生产者同步发送消息失败消息重投实现。
实现思路挺简单的,就是简单一循环,只要服务端不给我 SEND_OK 就循环投递消息给服务端
消费者消息重试
消费者消费消息失败后,提供一种重试机制,可以让消息再重新消费一次。
顺序消息的重试:对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。
因此,建议使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
无序消息的重试:对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。
无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
重试次数:消息队列 RocketMQ 默认允许每条消息最多重试 16 次。如果消息重试 16 次后仍然失败,消息将不再投递。
会在 4 小时 46 分钟之内进行 16 次重试。按照时间衰减策略执行。
一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。
消费者的重试是有服务端来控制发起的。其现原理和延迟消息的发送是一样的。
那这个消费者的重试配置就在 broker.conf 或 broker.properties 的服务端配置文件里了。
先看客户端对消费者的实现:
DefaultMQPushConsumerImpl.start() 在 《RocketMQ进阶(二)》里有详细介绍,在这就不啰嗦了。
在这个默认消费者启动方法里会有一个 mQClientFactory.start(); 客户端工厂的启动,工厂在启动的时候启动了执行了 this.pullMessageService.start();
PullMessageService 继承了 ServiceThread 线程类,PullMessageService里的run方法就是所执行的 start()
@Override
public void run() {
log.info(this.getServiceName() + " service started");
// 线程是否被停止了,线程存在一直循环拉取
while (!this.isStopped()) {
try {
// 从拉取请求队列里获取拉取请求上下文
PullRequest pullRequest = this.pullRequestQueue.take();
// 拉取消息
this.pullMessage(pullRequest);
} catch (InterruptedException ignored) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
log.info(this.getServiceName() + " service end");
}
this.pullMessage(pullRequest);
private void pullMessage(final PullRequest pullRequest) {
// 从消费者列表获取消费者信息
final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
// 消费者存在,拉取消息
if (consumer != null) {
DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
impl.pullMessage(pullRequest);
} else {
log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
}
}
impl.pullMessage(pullRequest);
public void pullMessage(final PullRequest pullRequest) {
// 从拉取请求获取消费队列快照
final ProcessQueue processQueue = pullRequest.getProcessQueue();
// 请求是否被抛弃
if (processQueue.isDropped()) {
log.info("the pull request[{}] is dropped.", pullRequest.toString());
return;
}
// 最后一次拉取时间戳
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
// 确保服务正在运行,否则存储到稍后执行拉取请求队列中
try {
this.makeSureStateOK();
} catch (MQClientException e) {
log.warn("pullMessage exception, consumer state not ok", e);
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
return;
}
// 消费是否被暂停,否则存储到稍后执行拉取请求队列中
if (this.isPause()) {
log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
return;
}
// 缓存消息总数
long cachedMessageCount = processQueue.getMsgCount().get();
// 缓存消息大小
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
// 缓存消息总数是否超过阈值,否则存储到稍后执行拉取请求队列中
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
// 缓存消息大小是否超过阈值,否则存储到稍后执行拉取请求队列中
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
// 是否顺序消费
if (!this.consumeOrderly) { // 并发消费
// 是否超过并发最大偏移量,否则存储到稍后执行拉取请求队列中
if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
log.warn(
"the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
pullRequest, queueMaxSpanFlowControlTimes);
}
return;
}
} else { // 顺序消费
if (processQueue.isLocked()) {
// 顺序拉取消息,没有被锁定
if (!pullRequest.isLockedFirst()) {
// 计算从什么位置拉取
final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
// 计算出的拉取位置 小于 请求下一个偏移量 则代理忙