细读经典——RocketMQ技术NM(2)

二、消息发送

MQ发送普通消息有三种方式:可靠同步、可靠异步、单向,可靠的意思是会收到发送的结果

基于以上认识,我们可以看看一条消息发送的整个过程是怎样的。

2.1 抽象部分:

这里很自然能想到需要定义消息实体Message::

public class Message implements Serializable {
    // 主题        
    private String topic;
    // 保留
    private int flag;
    // 扩展属性
    // 包含消息TAG,消息索引keys,消息延迟级别delayTimeLevel
    private Map<String, String> properties;
    // 消息体
    private byte[] body;
    // 事务id
    private String transactionId;
}

接下来思路就是构建生产者,消费者模型,RocketMQ给出了一个父类MQClientInstance用来标识一个生产者或者消费者实例,MQClientInstance由MQClientManager维护的一个ConcurrentMap进行管理,对于一个clientId,在一个JVM实例中,仅有一个MQClientInstance

public class MQClientManager {
    private final static InternalLogger log = ClientLogger.getLog();
    private static MQClientManager instance = new MQClientManager();
    private AtomicInteger factoryIndexGenerator = new AtomicInteger();
    private ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable =
        new ConcurrentHashMap<String, MQClientInstance>();
}

2.2 实现部分:

对于生产者而言,这里RocketMQ给出了一个默认实现DefaultMQProducer,目前这个方法已经比较臃肿了,直接通过核心方法start()进行跟踪:

 public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;

                this.checkConfig();

                if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                    this.defaultMQProducer.changeInstanceNameToPID();
                }
                // 创建MQClientInstance实例
                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
                // 向生产者组中注册自己,
                boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
                if (!registerOK) {
                    this.serviceState = ServiceState.CREATE_JUST;
                    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                            + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                            null);
                }

                this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

                if (startFactory) {
                    mQClientFactory.start();
                }

                log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                        this.defaultMQProducer.isSendMessageWithVIPChannel());
                this.serviceState = ServiceState.RUNNING;
                break;
            case RUNNING:
            case START_FAILED:
            case SHUTDOWN_ALREADY:
                throw new MQClientException("The producer service state not OK, maybe started once, "
                        + this.serviceState
                        + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                        null);
            default:
                break;
        }

        //向broker广播自己的ClientID
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

        this.timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    RequestFutureTable.scanExpiredRequest();
                } catch (Throwable e) {
                    log.error("scan RequestFutureTable exception", e);
                }
            }
        }, 1000 * 3, 1000);
    }

start()方法的主要功能就是启动一个MQClient的生产者实现,并保存它的信息,方便后续调用。

在拥有了一个生产者实例之后,我们就可以通过tcp连接将消息从生产者发送到MQ的Broker中,MQ源码中提供了简单消息的发送example:

package org.apache.rocketmq.example.simple;

import org.apache.rocketmq.client.QueryResult;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class TestProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建MQclient实例
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        producer.start();

        for (int i = 0; i < 1; i++){
            try {
                Message msg = new Message("TopicTest1",
                        "TagA",
                        "key113",
                        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                // 同步方法,阻塞
                SendResult sendResult = producer.send(msg);
                // 单向
                producer.sendOneway(msg);
                // 异步回调
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.println("发送成功");
                    }

                    @Override
                    public void onException(Throwable e) {
                        System.out.println("发送失败");
                    }
                });
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        producer.shutdown();
    }
}

方法都很好理解,我们看一下返回信息封装SendResult里面的内容:

基于之前的知识,如果要我们自己去写一个返回的结果,应该包括发送状态,消息本身的标识,消息发送到哪个broker上具体什么地方(消费者根据主题去消费,生产者主题在msg里,这里的返回就是最主要的就是标识状态为了排查),基本上SendResult就是这些结果,注意异步和同步方法都有返回SendResult,你可以根据sendResult记录日志。

public class SendResult {
    private SendStatus sendStatus;
    private String msgId;
    private MessageQueue messageQueue;
    private long queueOffset;
    private String transactionId;
    private String offsetMsgId;
    private String regionId;
}

在关注完发送的主题流程之后,我们进入生产者的send方法,看看到底send过程中有什么细节:

send方法底层有一个默认实现SendDefaultImpl,该方法很长,我只提取三个主要的方法:

    private SendResult sendDefaultImpl(
            Message msg,
            final CommunicationMode communicationMode,
            final SendCallback sendCallback,
            final long timeout
    ){
    // 消息验证
    Validators.checkMessage(msg, this.defaultMQProducer);
    // 路由查找
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    // 消息发送
    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
    }

消息发送的基本流程包括:消息验证,路由查找消息发送

消息验证主要是验证消息长度,源码默认长度1024*1024*4为4MB

路由查找则是根据topic找broker和broker下的queue,找到要发送的queue之后,就是向该queue发送msg,这里面选择queue有不同的策略,你应该瞬间有反应策略模式,默认就是轮询的向broker内该topic下的所有queue发送msg。选择queue的源码:

    public MessageQueue selectOneMessageQueue() {
        // 上一次发送的位置自增
        int index = this.sendWhichQueue.getAndIncrement();
        // 取模
        int pos = Math.abs(index) % this.messageQueueList.size();
        // pos小于零就取缓存的queue中的第一个
        if (pos < 0)
            pos = 0;
        return this.messageQueueList.get(pos);
    }

选择完queue之后开始消息发送,根据路由信息组装报文,然后根据不同的CommunicationMode(同步,异步,单向)执行不同的方法,当然底层都是一个tcp长连接,通过Channel去发送数据:底层源码也看看,当然,单向,异步不用返回,直接返回null就行,等待发送完之后的处理。

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() {
                @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);
                    log.warn("send a request command to channel <" + addr + "> failed.");
                }
            });

            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
            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);
        }
    }

2.3 异常情况

发送消息过程中的异常处理其实有很多,这里我们着重看一下broker延迟机制,问题的由来是:

如果一个queue所在的broker宕机了,那么你selectOneMessageQueue方法自增取模,取到的下一个很有可能还是宕机的broker所拥有的queue,说白了这是无用功,怎么避免呢?

源码中提供了selectOneMessageQueue的另一种实现:

    public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            try {
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }

                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }

            return tpInfo.selectOneMessageQueue();
        }

        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

解决的思路就是利用一张CocurrentHashmap存储存在发送问题的broker,让其在一段时间内无法发送,如果过了这段时间,broker和producer的通信结果表明该broker可用,那就再次向该broker尝试发送msg,如果borker可用,就移除该broker的错误标志,有兴趣的可以深入了解一下,我就不展开了。

以上就是发送一个简单消息的全部过程。

2.4 批量消息

当然,RocketMQ也支持批量发送,将msg封装成MessageBatch,其实就是为List<Message>提供了encode方法用于压缩msg的内容:

public class MessageBatch extends Message implements Iterable<Message> {

    //messagebatch实际上就是message的list
    private final List<Message> messages;

    public byte[] encode() {
        //批处理固定格式化
        return MessageDecoder.encodeMessages(messages);
    }

    public Iterator<Message> iterator() {
        return messages.iterator();
    }
}

其中,很优美的实现了一个迭代器模式

一般而言,批量消息发送的策略包括固定格式,固定长度,固定字符结尾等等,RocketMQ采用的是固定格式:

 具体发送过程和发送单条消息没有区别,不在赘述。

生产者端消息发送过程就结束了,架构上没有太多难点,难的反而是高并发编程,这里面用到并发容器很多,有状态服务的维护,netty的网络通信,才是系统实现的难点所在。

今天先到这吧

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值