RocketMQ源码分析之request-reply特性

1.什么是request-reply?

  RocketMQ4.6.0版本中增加了request-reply新特性,该特性允许producer在发送消息后同步或者异步等待consumer消费完消息并返回响应消息,类似rpc调用效果。
2. 使用场景

  • 快速搭建服务总线,实现rpc框架
  • 调用链追踪分析
  • 跨网络区域实现系统间同步调用

3.使用方法

  • producer端

  producer端调用request(final Message msg, final long timeout)方法以同步方式等待consumer端消费完消息并返回响应消息;调用request(final Message msg, final RequestCallback requestCallback, final long timeout)方法以异步方式等待consumer端消费完消息并返回响应消息。

同步方式:

public class RequestProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        String producerGroup = "RequestTopic0218";
        String topic = "RequestTopic";
        long ttl = 300000;

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();

        try {
            Message msg = new Message(topic,
                "",
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            long begin = System.currentTimeMillis();
            Message retMsg = producer.request(msg, ttl);
            long cost = System.currentTimeMillis() - begin;
            System.out.printf("request to <%s> cost: %d replyMessage: %s %n", topic, cost, retMsg);
        } catch (Exception e) {
            e.printStackTrace();
        }
        producer.shutdown();
    }
}

异步方式:

public class AsyncRequestProducer {
    private static final InternalLogger log = ClientLogger.getLog();

    public static void main(String[] args) throws MQClientException, InterruptedException {
        String producerGroup = "please_rename_unique_group_name";
        String topic = "AsynRequestTopic";
        long ttl = 3000;

        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        producer.start();

        try {
            Message msg = new Message(topic,
                "",
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            long begin = System.currentTimeMillis();
            producer.request(msg, new RequestCallback() {
                @Override
                public void onSuccess(Message message) {
                    long cost = System.currentTimeMillis() - begin;
                    System.out.printf("request to <%s> cost: %d replyMessage: %s %n", topic, cost, message);
                }

                @Override
                public void onException(Throwable e) {
                    System.err.printf("request to <%s> fail.", topic);
                }
            }, ttl);
        } catch (Exception e) {
            log.warn("", e);
        }
         /* shutdown after your request callback is finished */
//        producer.shutdown();
    }
}

  • consumer端

  consumer端程序在原来的基础上会增加以下内容:

  (1)创建producer用来发送消息

  (2)在消费完消息后调用RocketMQ提供的MessageUtil.createReplyMessage(final Message requestMessage, final byte[] body)方法来构建响应消息

  (3)调用send方法将响应消息发回给生产者

public class ResponseConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        String producerGroup = "ReplyProducer0218";
        String consumerGroup = "ResponseConsumer0218";
        String topic = "RequestTopic";

        // create a producer to send reply message
        DefaultMQProducer replyProducer = new DefaultMQProducer(producerGroup);
        replyProducer.setNamesrvAddr("127.0.0.1:9876");
        replyProducer.start();

        // create consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        // recommend client configs
        consumer.setPullTimeDelayMillsWhenException(0L);

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                for (MessageExt msg : msgs) {
                    try {
                        System.out.printf("handle message: %s", msg.toString());
                        String replyTo = MessageUtil.getReplyToClient(msg);
                        byte[] replyContent = "reply message contents.".getBytes();
                        // create reply message with given util, do not create reply message by yourself
                        Message replyMessage = MessageUtil.createReplyMessage(msg, replyContent);

                        // send reply message with producer
                        SendResult replyResult = replyProducer.send(replyMessage, 300000);
                        System.out.printf("reply to %s , %s %n", replyTo, replyResult.toString());
                    } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.subscribe(topic, "*");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

  1. 源码分析

在RocketMQ中producer端可以通过调用以下两个方法发送消息并等待consumer端返回响应消息:

  • request(final Message msg, final long timeout)
  • request(final Message msg, final RequestCallback requestCallback, final long timeout)

下面以producer同步等待consumer响应消息为例分析整个request-reply的过程:

public Message request(Message msg,
        long timeout) throws RequestTimeoutException, MQClientException, RemotingException, MQBrokerException, InterruptedException {
        long beginTimestamp = System.currentTimeMillis();
        prepareSendRequest(msg, timeout);
        final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID);

        try {
            final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null);
            RequestFutureTable.getRequestFutureTable().put(correlationId, requestResponseFuture);

            long cost = System.currentTimeMillis() - beginTimestamp;
            this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    requestResponseFuture.setSendRequestOk(true);
                }

                @Override
                public void onException(Throwable e) {
                    requestResponseFuture.setSendRequestOk(false);
                    requestResponseFuture.putResponseMessage(null);
                    requestResponseFuture.setCause(e);
                }
            }, timeout - cost);

            return waitResponse(msg, timeout, requestResponseFuture, cost);
        } finally {
            RequestFutureTable.getRequestFutureTable().remove(correlationId);
        }
    }

(1)获取系统当前时间,方便后续进行超时判断

(2)调用prepareSendRequest(final Message msg, long timeout)函数将待发送给broker的消息进行改造,具体改造如下:

  • 调用CorrelationIdUtil.createCorrelationId()生成该消息的correlationId,并将correlationId添加到消息的扩展属性CORRELATION_ID
  • 获取producer的clientId并将其添加到消息的扩展属性REPLY_TO_CLIENT,该属性的作用在于后续consumer端发送响应消息时broker知道将消息发送给哪个producer端
  • 将超时时间添加到消息的扩展属性TTL

(3)构建RequestResponseFuture对象,这里需要详细解释RequestResponseFuture对象,RequestResponseFuture是实现request-reply特性的关键,producer发送的每条消息都会new一个RequestResponseFuture对象:

  • correlationId是CorrelationIdUtil.createCorrelationId()方法随机生成的UUID字符串,correlationId是用来标识从发送每条消息到conumer端发送响应消息的请求
  • requestMsg是consumer端返回的响应消息
  • countDownLatch在消息发送时会阻塞producer线程(调用了await实现阻塞),等到响应消息返回时激活producer线程,最后返回consumer端响应消息,所以虽然在内部实现上是以异步方式发送消息但是结合countDownLatch达到了同步的效果
  • 由于是同步发送所以requestCallback为null
public class RequestResponseFuture {
    private final String correlationId;
    private final RequestCallback requestCallback;
    private final long beginTimestamp = System.currentTimeMillis();
    private final Message requestMsg = null;
    private long timeoutMillis;
    private CountDownLatch countDownLatch = new CountDownLatch(1);
    private volatile Message responseMsg = null;
    private volatile boolean sendRequestOk = true;
    private volatile Throwable cause = null;

(4)将<correlationId, requestResponseFuture>添加到requestFutureTable,后续consumer向broker发送RequestCode.SEND_REPLY_MESSAGE_V2请求将响应消息发送到broker,broker在处理这个请求时会调用pushReplyMessage方法发送RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT请求给producer,此时producer端会根据响应消息中correlationId在requestFutureTable中获取其对应的requestResponseFuture,并且会将响应消息赋给requestResponseFuture中的responseMsg。

(5)调用sendDefaultImpl方法以异步的方式发送消息,虽然是以异步方式发送消息但是结合RequestResponseFuture中的countDownLatch到达了同步效果。此时producer发送了RequestCode.SEND_MESSAGE请求给broker,broker后续的处理过程与发送普通消息是一样的。

(6)consumer在正常消费完消息后,需要调用MessageUtil.createReplyMessage方法构建响应消息,该方法有两个参数,分别是producer发送消息和响应消息体内容,该方法会从producer发送的消息的扩展属性中获取“CLUSTER”、“REPLY_TO_CLIENT”、“CORRELATION_ID”和“TTL”,并根据这些扩展属性以及响应消息体内容构建响应消息。这里需要注意,新构建的响应消息的topic是由producer发送的消息的扩展属性中的CLUSTER与REPLY_TOPIC拼接起来,即“集群名称_REPLY_TOPIC”,这个是一个系统级别的topic,是由broker自己创建的。

public static Message createReplyMessage(final Message requestMessage, final byte[] body) throws MQClientException {
        if (requestMessage != null) {
            Message replyMessage = new Message();
            String cluster = requestMessage.getProperty(MessageConst.PROPERTY_CLUSTER);
            String replyTo = requestMessage.getProperty(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT);
            String correlationId = requestMessage.getProperty(MessageConst.PROPERTY_CORRELATION_ID);
            String ttl = requestMessage.getProperty(MessageConst.PROPERTY_MESSAGE_TTL);
            replyMessage.setBody(body);
            if (cluster != null) {
                String replyTopic = MixAll.getReplyTopic(cluster);
                replyMessage.setTopic(replyTopic);
                MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_MESSAGE_TYPE, MixAll.REPLY_MESSAGE_FLAG);
                MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_CORRELATION_ID, correlationId);
                MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT, replyTo);
                MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_MESSAGE_TTL, ttl);

                return replyMessage;
            } else {
                throw new MQClientException(ClientErrorCode.CREATE_REPLY_MESSAGE_EXCEPTION, "create reply message fail, requestMessage error, property[" + MessageConst.PROPERTY_CLUSTER + "] is null.");
            }
        }
        throw new MQClientException(ClientErrorCode.CREATE_REPLY_MESSAGE_EXCEPTION, "create reply message fail, requestMessage cannot be null.");
    }

(7)调用send方法发送响应消息到broker,在发送的过程中会判断消息的类型,由于该消息是reply类型的,所以向broker发送的请求类型是RequestCode.SEND_REPLY_MESSAGE_V2

(8)broker处理RequestCode.SEND_REPLY_MESSAGE_V2请求的是ReplyMessageProcessor,具体操作如下:

  • 根据请求中响应消息的topic、queueId、消息体内容、消息标记、消息的扩展属性、消息产生的时间、消息的来源等信息构建MessageExtBrokerInner对象
  • 调用pushReplyMessage方法构建RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT请求,然后根据消息扩展属性REPLY_TO_CLIENT获取broker与producer连接的channel,最后将请求发送给producer。这里有个问题:RocketMQ如何保证请求原路返回?首先producer产生的消息会发送到broker上,此时broker中存储的producer产生的消息的扩展属性中是包含存储的broker的集群名称的,接着consumer消息该消息并根据该消息构造出响应消息,在构造响应消息时,其topic是“集群名称_REPLY_TOPIC”,这样就保证了consumer在发送响应消息到broker是原路返回,即这里的broker是与producer连接的broker。
  • 判断broker端的配置文件中storeReplyMessageEnable配置项的值是否为true,如果为true,则会将响应消息存储在broker端。storeReplyMessageEnable的默认值是true。
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(requestHeader.getTopic());
msgInner.setQueueId(queueIdInt);
msgInner.setBody(body);
msgInner.setFlag(requestHeader.getFlag());
MessageAccessor.setProperties(msgInner, MessageDecoder.string2messageProperties(requestHeader.getProperties()));
msgInner.setPropertiesString(requestHeader.getProperties());
msgInner.setBornTimestamp(requestHeader.getBornTimestamp());
msgInner.setBornHost(ctx.channel().remoteAddress());
msgInner.setStoreHost(this.getStoreHost());
msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes());

PushReplyResult pushReplyResult = this.pushReplyMessage(ctx, requestHeader, msgInner);
this.handlePushReplyResult(pushReplyResult, response, responseHeader, queueIdInt);

if (this.brokerController.getBrokerConfig().isStoreReplyMessageEnable()) {
    PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
    this.handlePutMessageResult(putMessageResult, request, msgInner, responseHeader, sendMessageContext, queueIdInt);
}

(9)producer处理broker发送的RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT请求的是ClientRemotingProcessor,具体如下:

  • 根据请求还原响应消息MessageExt
  • 获取响应消息扩展属性CORRELATION_ID的值correlationId,在producer端的requestFutureTable中根据correlationId获取该消息对应的requestResponseFuture,然后将响应消息放入到requestResponseFuture中的responseMsg并将countDownLatch的值减一,此时producer端调用request方法的线程就激活了
  • 从requestFutureTable中删除key为correlationId的数据项
private void processReplyMessage(MessageExt replyMsg) {
        final String correlationId = replyMsg.getUserProperty(MessageConst.PROPERTY_CORRELATION_ID);
        final RequestResponseFuture requestResponseFuture = RequestFutureTable.getRequestFutureTable().get(correlationId);
        if (requestResponseFuture != null) {
            requestResponseFuture.putResponseMessage(replyMsg);

            RequestFutureTable.getRequestFutureTable().remove(correlationId);

            if (requestResponseFuture.getRequestCallback() != null) {
                requestResponseFuture.getRequestCallback().onSuccess(replyMsg);
            } else {
                requestResponseFuture.putResponseMessage(replyMsg);
            }
        } else {
            String bornHost = replyMsg.getBornHostString();
            log.warn(String.format("receive reply message, but not matched any request, CorrelationId: %s , reply from host: %s",
                correlationId, bornHost));
        }
    }

(10)producer端调用request方法线程激活后会调用waitResponse方法返回requestResponseFuture中的responseMsg,这里最终调用的waitResponseMessage方法中带有一个参数:超时时间,如果到了超时时间后consumer端的响应消息没有被producer端收到,线程也会被激活,这样的设置也是防止producer线程一直被阻塞。

参考资料: 官方视频链接.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值