本文章基于 RocketMQ 4.9.3
1. 前言
上一篇文章从生产者发送源码方法开始讲述了同步、异步、单向发送的流程,这篇文章来看下异步回调是如何处理的。
2. InvokeCallback#operationComplete
上一篇文章我们说过,异步发送在 sendMessageAsync 中会通过 InvokeCallback 来进行响应回调,下面就来看下这个 InvokeCallback 的 operationComplete 方法。
@Override
public void operationComplete(ResponseFuture responseFuture) {
// 消耗的时间
long cost = System.currentTimeMillis() - beginStartTime;
// 响应结果
RemotingCommand response = responseFuture.getResponseCommand();
// 如果 sendCallback 为空并且 response 不为空,如果是异步发送那么就必须要设置 sendCallback,所以这里就不会是异步的回调
if (null == sendCallback && response != null) {
try {
// 这里就是根据 response 构建发送结果
SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response, addr);
if (context != null && sendResult != null) {
// 将发送结果设置到上下文
context.setSendResult(sendResult);
// 同时回调 SendMessageHook 的 sendMessageAfter 方法
context.getProducer().executeSendMessageHookAfter(context);
}
} catch (Throwable e) {
}
// 更新延迟故障容错集合
producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);
return;
}
if (response != null) {
try {
// 异步回调会走这里,同样和上面差不多,根据 response 构建发送结果
SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response, addr);
assert sendResult != null;
if (context != null) {
// 给上下文设置发送结果
context.setSendResult(sendResult);
// 同时回调 SendMessageHook 的 sendMessageAfter 方法
context.getProducer().executeSendMessageHookAfter(context);
}
try {
// 回调 sendCallback#onSuccess 方法
sendCallback.onSuccess(sendResult);
} catch (Throwable e) {
}
// 更新延迟故障容错集合
producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);
} catch (Exception e) {
// 异常的情况下也会更新延迟故障容错集合
producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
// 再次重试
onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
retryTimesWhenSendFailed, times, e, context, false, producer);
}
} else {
// 这里就是出问题了,返回结果为空,更新延迟故障容错集合
producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
if (!responseFuture.isSendRequestOK()) {
// 走异常重试逻辑
MQClientException ex = new MQClientException("send request failed", responseFuture.getCause());
onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
retryTimesWhenSendFailed, times, ex, context, true, producer);
} else if (responseFuture.isTimeout()) {
// 走异常重试逻辑
MQClientException ex = new MQClientException("wait response timeout " + responseFuture.getTimeoutMillis() + "ms",
responseFuture.getCause());
onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
retryTimesWhenSendFailed, times, ex, context, true, producer);
} else {
// 走异常重试逻辑
MQClientException ex = new MQClientException("unknow reseaon", responseFuture.getCause());
onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
retryTimesWhenSendFailed, times, ex, context, true, producer);
}
}
}
回调方法分为了几种情况,首先如果用户没有设置 sendCallback,sendCallback 是用户设置的异步发送回调,在调用 send 方法时就会设置进去,也就是说如果要用异步发送就得设置下这个回调函数。
如果没有设置回调函数,那么回调的结果就只是添加到发送上下文,然后回调 SendMessageHook 的 sendMessageAfter 方法,最后更新下延迟故障容错集合,就直接返回了。
如果设置了回调函数,就会走到 if (response != null)
这个分支,同样和上面流程差不多,根据 response 构建发送结果设置到上下文中,最重要的是会回调 sendCallback#onSuccess 方法,也就是我们自己的业务逻辑,最后再更新延迟故障容错集合。
那如果用户设置了 sendCallback,但是返回结果是空的,这种情况下有三种情况,超时、发送失败、其他异常,这三种情况都会走到 onExceptionImpl 异常重试逻辑。不过要注意下,如果是上面设置了回调函数但是在 sendCallback#onSuccess 中发生了异常,这种情况下是不会走到 onExceptionImpl 逻辑的,也就是说如果用户业务有异常的话就不管,自己处理。
3. onExceptionImpl 异常处理
/**
* 异常处理的核心逻辑
* @param brokerName broker 名称
* @param msg 要发送的消息
* @param timeoutMillis 超时时间
* @param request 发送的消息请求
* @param sendCallback 发送回调
* @param topicPublishInfo 发送的 topic 信息
* @param instance 客户端实例
* @param timesTotal 发送失败之后可以重新发送多少次,默认是 2,就算是异步也可以重试 2 次,但是重试意思是往同
* 一个 broker 发送,而不是发送到不同的 broker 上面
* @param curTimes 发送失败的次数
* @param context 消息发送上下文,可以用来回调一些钩子方法
* @param producer 生产者
* @throws InterruptedException
* @throws RemotingException
*/
private void onExceptionImpl(final String brokerName,
final Message msg,
final long timeoutMillis,
final RemotingCommand request,
final SendCallback sendCallback,
final TopicPublishInfo topicPublishInfo,
final MQClientInstance instance,
final int timesTotal,
final AtomicInteger curTimes,
final Exception e,
final SendMessageContext context,
final boolean needRetry,
final DefaultMQProducerImpl producer
) {
// 出现异常的次数 + 1
int tmp = curTimes.incrementAndGet();
// 如果可以重试,并且当前重试次数小于总次数,注意重试和重传不一样,我们平时说同步发送可以重传 3 次意思是同步发送失败的时候可以往其他
// broker 进行重传,异步是不允许重传的,但是异步可以重试,重试是发送失败的时候往一个 broker 重发,所以记住一句话:同步有重传、异步
// 有重试
if (needRetry && tmp <= timesTotal) {
// 重试默认是发送给相同的 broker
String retryBrokerName = brokerName;//by default, it will send to the same broker
// 因为上面的 broker 发送失败了,所以下面如果传入的 topic 信息不为空,下面还是会从 topic 的队列中挑选一个可用的来发送
if (topicPublishInfo != null) { //select one message queue accordingly, in order to determine which broker to send
// 重新挑选这个 topic 下面一个可用的 broker 队列
MessageQueue mqChosen = producer.selectOneMessageQueue(topicPublishInfo, brokerName);
// 然后将发送的 brokerName 设置为这个队列的 brokerName,因为 topic 下面的队列是可以存放到不同的 broker 集群的
retryBrokerName = mqChosen.getBrokerName();
}
// 然后找出这个 broker 的 地址
String addr = instance.findBrokerAddressInPublish(retryBrokerName);
log.warn(String.format("async send msg by retry {} times. topic={}, brokerAddr={}, brokerName={}", tmp, msg.getTopic(), addr,
retryBrokerName), e);
try {
// 重新设置请求 ID
request.setOpaque(RemotingCommand.createNewRequestId());
// 发送异步消息
sendMessageAsync(addr, retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance,
timesTotal, curTimes, context, producer);
} catch (InterruptedException e1) {
// 异常处理
onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
context, false, producer);
} catch (RemotingTooMuchRequestException e1) {
// 异常处理
onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
context, false, producer);
} catch (RemotingException e1) {
// RemotingException 异常,这种情况就会记录故障容错集合
producer.updateFaultItem(brokerName, 3000, true);
// 再次进行异常处理
onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
context, true, producer);
}
} else {
// 这里就是到达重试的上限或者不允许重试
if (context != null) {
// 设置异常
context.setException(e);
// 调用 SendMessageHook 的 sendMessageAfter 方法
context.getProducer().executeSendMessageHookAfter(context);
}
try {
// 异常回调
sendCallback.onException(e);
} catch (Exception ignored) {
}
}
}
首先 curTimes 表示当前发送失败的次数,这里会和 timesTotal
对比,这个 timesTotal 是从 DefaultMQProducer 中获取的,默认是 2,也就是下面的值。
/**
* 异步发送失败重试次数,异步重试默认不会选择其他 broker,仅在同一个 broker 上做重试,不保证消息不丢
*/
private int retryTimesWhenSendAsyncFailed = 2;
上一篇文章发送的时候我们也看到异步发送重传次数为 1,但是异步发送次数是 3 次,根据下面的代码,也就是 String retryBrokerName = brokerName
就可以看出重试的 topic 发送的 broker 默认值不变,也就是默认还是往上一个 broker 发送。
但是如果 topic 配置不为空,就会调用 selectOneMessageQueue
方法重新选择一个队列,如果开启了故障容错,就会根据 broker 的不可用时间选择一个可用的 broker,如果没有开启,就根据传入的 brokerName 选择一个队列,这个队列的 brokerName 和传入的 brokerName 不同,也就是说避开这个 broker,如果这个 topic 只是存到了一个 broker 集群,那么最后兜底就是选择这个 topic 下面的一个队列。
异步方法传入的 topic 配置就是不为空的,所以这里会重新选择一个队列来发送,然后重新设置一个请求 ID,再次调用 sendMessageAsync 发送异步消息。
如果重试次数已经到达上限了,直接调用 sendCallback.onException 方法。
4. 定时任务扫描 responseTable
生产者或者消费者启动时会启动一个 timer 定时器,初始化之后 3s 执行, 之后 1s 执行一次,扫描 responseTable, 将超时的 ResponseFuture 删掉, 然后执行回调逻辑。
public void scanResponseTable() {
final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
while (it.hasNext()) {
Entry<Integer, ResponseFuture> next = it.next();
ResponseFuture rep = next.getValue();
// 这里就是超时了
if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) {
rep.release();
it.remove();
// 添加到超时集合中
rfList.add(rep);
log.warn("remove timeout request, " + rep);
}
}
for (ResponseFuture rf : rfList) {
try {
// 统一执行 executeInvokeCallback 回调函数
executeInvokeCallback(rf);
} catch (Throwable e) {
log.warn("scanResponseTable, operationComplete Exception", e);
}
}
}
ResponseFuture 的超时时间是发送的超时时间减去发送消息的耗时,如果超时了,就添加到 rfList 中,然后统一遍历调用 executeInvokeCallback 回调方法。
private void executeInvokeCallback(final ResponseFuture responseFuture) {
boolean runInThisThread = false;
ExecutorService executor = this.getCallbackExecutor();
if (executor != null) {
try {
// 使用线程池来执行请求的回调
executor.submit(new Runnable() {
@Override
public void run() {
try {
responseFuture.executeInvokeCallback();
} catch (Throwable e) {
log.warn("execute callback in executor exception, and callback throw", e);
} finally {
responseFuture.release();
}
}
});
} catch (Exception e) {
runInThisThread = true;
log.warn("execute callback in executor exception, maybe executor busy", e);
}
} else {
runInThisThread = true;
}
if (runInThisThread) {
try {
// 这个就是当前线程来回调, 不用线程池
responseFuture.executeInvokeCallback();
} catch (Throwable e) {
log.warn("executeInvokeCallback Exception", e);
} finally {
responseFuture.release();
}
}
}
一般来说就是用线程池来回调,在 executeInvokeCallback 中首先 CAS 设置下 executeCallbackOnlyOnce 为 true,用来确保只有一个线程回调,注意这种情况下回调 operationComplete 时候 response 是空,所以就会走到 else if (responseFuture.isTimeout())
这个分支,然后到 onExceptionImpl 里面去重试。
public void executeInvokeCallback() {
if (invokeCallback != null) {
if (this.executeCallbackOnlyOnce.compareAndSet(false, true)) {
invokeCallback.operationComplete(this);
}
}
}
5. 小结
好了,这篇文章就到这里,我们把消息发送剩下的一点补充完成,现在可以做一个小结了:
- 首先就是异步发送没有重传,只有重试,默认一共最高可以发送 3 次,重试会默认选择上一次发送失败的 broker 进行重试,但是异步发送会从 topic 下面的队列尝试去选择一个和上一次不同的 broker 的队列去发送。
- 然后就是同步发送,同步发送没有重试,有重传,重传的意思就是默认会选择一个不同的 broker 去发送,如果同步发送抛出异常也不会去重试,同步重传的次数也是 3 次,本质上和异步是一样的。
RocketMQ 会启动一个定时任务,每隔 3s 去扫描 responseTable 集合,扫描出里面的过期请求,大家也可以看到对于同步、异步任务,发送请求会存储到 responseTable 集合,但是对于单向发送,由于不关心返回结果,所以会生成 ResponseFuture 存储到 responseTable,自然也不会有超时这些了。
最后还有一点没有说的,就是当生产者接受到 broker 的响应结果时,会调用 processResponseCommand 方法,将响应结果存储到 ResponseFuture#responseCommand 属性中,再把这个请求从 responseTable 集合中删掉,然后使用 executeInvokeCallback 方法回调异步请求,或者如果不是异步请求,就直接通过 putResponse 唤醒正在阻塞等待的同步线程。
public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
// 请求 id
final int opaque = cmd.getOpaque();
// 获取响应结果
final ResponseFuture responseFuture = responseTable.get(opaque);
if (responseFuture != null) {
// 设置响应结果
responseFuture.setResponseCommand(cmd);
// 从集合中删掉
responseTable.remove(opaque);
if (responseFuture.getInvokeCallback() != null) {
// 异步发送回调
executeInvokeCallback(responseFuture);
} else {
// 同步发送唤醒等待的线程
responseFuture.putResponse(cmd);
responseFuture.release();
}
} else {
log.warn("receive response, but not matched any request, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
log.warn(cmd.toString());
}
}
public void putResponse(final RemotingCommand responseCommand) {
this.responseCommand = responseCommand;
// 唤醒等待的线程
this.countDownLatch.countDown();
}
如有错误,欢迎指出!!!!