RocketMQ 支持同步、异步、Oneway 三种消息发送方式。

  • 同步:客户端发起一次消息发送后会同步等待服务器的响应结果。
  • 异步:客户端发起一下消息发起请求后不等待服务器响应结果而是立即返回,这样不会阻塞客户端子线程,当客户端收到服务端(Broker)的响应结果后会自动调用回调函数。
  • Oneway:客户端发起消息发送请求后并不会等待服务器的响应结果,也不会调用回调函数,即不关心消息的最终发送结果。

这里重点介绍下异步与同步。

异步消息

  1. 每一个消息发送者实例(DefaultMQProducer)内部会创建一个异步消息发送线程池,默认线程数量为 CPU 核数,线程池内部持有一个有界队列,默认长度为 5W,并且会控制异步调用的最大并发度,默认为 65536,其可以通过参数 clientAsyncSemaphoreValue 来配置。
  2. 客户端使线程池将消息发送到服务端,服务端处理完成后,返回结构并根据是否发生异常调用 SendCallback 回调函数

上面是发送异步消息的过程,下面再从源码上分析下。

public void start() throws MQClientException {
        this.defaultMQProducerImpl.start();
        if (null != traceDispatcher) {
            try {
                traceDispatcher.start(this.getNamesrvAddr());
            } catch (MQClientException e) {
                log.warn("trace dispatcher start failed ", e);
            }
        }
    }

这个是Producer服务的启动入口。接着看DefaultMQProducerImpl类:

public void start(final boolean startFactory) throws MQClientException {
    ...
    if (startFactory) {
        //启动MQClientInstance
        mQClientFactory.start();
    }
    ...
}
public void start(final boolean startFactory) throws MQClientException {
        ...
        this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
        ...
    }

在getAndCreateMQClientInstance方法里会创建MQClientInstance实例,接着在MQClientInstance创建过程上又会创建DefaultMQProducerImpl对象,这时会创建一个异步消息发送线程池。

        this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<Runnable>(50000);
        this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(
            Runtime.getRuntime().availableProcessors(),
            Runtime.getRuntime().availableProcessors(),
            1000 * 60,
            TimeUnit.MILLISECONDS,
            this.asyncSenderThreadPoolQueue,
            new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "AsyncSenderExecutor_" + this.threadIndex.incrementAndGet());
                }
            });

下面看下异步回调的地方,涉及MQClientAPIImpl#sendMessageAsync方法:

this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
            @Override
            public void operationComplete(ResponseFuture responseFuture) {
                RemotingCommand response = responseFuture.getResponseCommand();
                ...
                if (response != null) {
                    try {
                        SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response);
                        assert sendResult != null;
                        if (context != null) {
                            context.setSendResult(sendResult);
                            context.getProducer().executeSendMessageHookAfter(context);
                        }

                        try {
                            sendCallback.onSuccess(sendResult);
                        } catch (Throwable e) {
                        }
            ...
            }
           }
        });

其中的sendCallback.onSuccess(sendResult)就是broker处理完请求后在进行回调。上面提到的限制65535并发是通过NettyRemotingAbstract#invokeAsyncImpl()里设置的Semaphore实现的,它默认是65535且可通过clientAsyncSemaphoreValue调整。

异步发送是否还能保证消息的有序性呢?如果做到下面几点是可以保证有序性的:

  1. 需要单线程异步发送
  2. 发送端需要记录每条消息的流水号,假设有1,2,3,4,5这5条消息,如果在3发送失败,则需要从3再发一遍,也就是4,5会发送两次
  3. 消费端需要对重复消息进行处理,比如对于第2步,如果没有收到消息3却收到了之后的4,5那消息4,5也不能处理,只有收到3之后才能向后处理。

参考:消息生产的实现过程(答疑)

同步发送
因为RocketMQ是借助Netty进行IO读写,而Netty是多主从Ractor模型,所以同步调用其实也是异步,只不过RocketMQ使用了一点技巧将异步转成了同步。我们来看下代码:

public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
        final long timeoutMillis)
        throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
        final int opaque = request.getOpaque();

        try {
            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
            this.responseTable.put(opaque, responseFuture);
            final SocketAddress addr = channel.remoteAddress();
            //有响应后进行回调,这就是异步
            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {//相关点1
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    if (f.isSuccess()) {
                        responseFuture.setSendRequestOK(true);
                        return;
                    } else {
                        responseFuture.setSendRequestOK(false);
                    }

                    responseTable.remove(opaque);
                    responseFuture.setCause(f.cause());
                    responseFuture.putResponse(null);//相关点2
                    log.warn("send a request command to channel <" + addr + "> failed.");
                }
            });

            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);//相关点3
            if (null == responseCommand) {
                if (responseFuture.isSendRequestOK()) {
                    throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
                        responseFuture.getCause());
                } else {
                    throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
                }
            }

            return responseCommand;
        } finally {
            this.responseTable.remove(opaque);
        }
    }

在‘相关点1’里面的回调就是broker处理完后使用另一个线程进行了回调。让主线程等待就是‘相关点2’外的代码

RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis)

里面使用了countDownLatch这个工具,那是在哪里进行的countDown呢?答案就是在回调里的‘相关点2’处的responseFuture.putResponse(null)。这个就是异步转同步的方法。

Oneway的方式

Oneway 方式通常用于发送一些不太重要的消息,例如操作日志,偶然出现消息丢失对业务无影响,这里就不过多的提了。

总结

本文主要提到了RocketMQ三种消息发送方式,重点介绍了异步发送逻辑与同步方式里如何将异步转成同步。
根据笔者在网上查到的一些资料来看使用异步发送的方式并不是特别多,如果想提高消息发送效率,一般是可以从刷盘策略和复制策略入手进行优化,使用同步发送方式基本上是可以满足需求的,当然一切也得从实际的业务场景出发。
最后还要提一点就是失败重试,在三种发送方式里如果SendStatus不是SEND_OK,只有同步的方式才会进行重试,也就是说在补偿机制、容错机制上,如果是异步或Oneway也是我们在使用时需要考虑的问题。


步履不停
38 声望13 粉丝

好走的都是下坡路