「日拱一卒」带你探索RocketMQ消息发送(三)

103 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

前言

通过上一篇文章「日拱一卒」带你探索RocketMQ消息发送(二),我们主要学习了 RocketMQ 的消息发送重试机制、故障规避机制来保障消息发送的高可用;再简单看了下消息实体类 Message。今天,我们来承接上一篇文章,来开始学习下 RocketMQ 消息发送的源码吧!

RocketMQ 消息结构与构造方法

RocketMQ 消息实体类是 Message,它位于 org.apache.rocketmq.common.message 包之下,主要具备的属性有:

  • String topic : MQ 消息所属的主题
  • int flag:消息标记
  • Map properties:扩展属性
  • byte[] body: 消息体
public class Message implements Serializable {
    private static final long serialVersionUID = 8445773977080406428L;

    private String topic;
    private int flag;
    private Map<String, String> properties;
    private byte[] body;
    private String transactionId;
}

根据使用场景的不同,它有五个构造方法

public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK) {
    this.topic = topic;
    this.flag = flag;
    this.body = body;

    if (tags != null && tags.length() > 0) {
        this.setTags(tags);
    }

    if (keys != null && keys.length() > 0) {
        this.setKeys(keys);
    }

    this.setWaitStoreMsgOK(waitStoreMsgOK);
}    

除了无参构造方法外,其他的三个方法都是复用了全属性构造方法。其中,有几个参数需要来解释一下:

  • tags:消息的Tag,主要用于消息的过滤
  • keys:消息索引键,RocketMQ 主要根据这些Key来快速检索消息
  • waitStoreMsgOK:RocketMQ 将消息发送出去后,是否要等待消息存储完成后再返回;从源码可以看出来,这个值是建议使用 true 的,这样可以使消息不容易丢失。

RocketMQ 生产者大揭秘

终于要到了本篇文章的重头戏,RocketMQ 生产者能够发送消息,究竟背后运行的原理是什么?

RocketMQ 生产者的代码实现都在 client 模块中,要具体看它的源码,大家可以看看这个包下的内容:

image.png

RocketMQ 发送消息 Demo

在 RocketMQ 提供的源码中,我们可以看到有对应的生产者发送消息的 Demo,大家有条件地话可以试着在本地跑一下,我这里截取了一些需要重点去理解的源码,如下:

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        producer.start();
        for (int i = 0; i < 1000; i++) {
            try {
                Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                );
                SendResult sendResult = producer.send(msg);
                producer.sendOneway(msg);
                 
                SendResult sendResult = producer.send(msg);
                System.out.printf("%s%n", sendResult);
               
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        // do something
                    }
                 
                    @Override
                      public void onException(Throwable e) {
                         // do something
                     }
                });
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        producer.shutdown();
    }
}

根据这个 Demo,我们知道,要发送一个消息的步骤如下:

  • 首先需要初始化一个 DefaultMQProducer 对象,调用该对象的 start 方法启动生产者
  • 然后初始化一个 Message 对象
  • 再调用 DefaultMQProducer 对象的 send 方法,将 Message 对象作为入参,将消息发送出去。根据 send 的不同重载,可以分为同步发送和异步发送,异步发送时需要实现它的回调方法;此外,还有 sendOneway 这个单向发送的方式
  • 完成消息发送后,为了及时地释放资源,不占用过多的内存,需要调用 DefaultMQProducer 对象的 shutdown 方法来关闭生产者

Demo 还是比较好理解的,接下来我们着重了解下 DefaultMQProducer 对象。

认识 DefaultMQProducer

public class DefaultMQProducer extends ClientConfig implements MQProducer {}

public interface MQProducer extends MQAdmin{}

public interface MQAdmin {}

DefaultMQProducer 类继承自 ClientConfig 类,实现了 MQProducer 接口,而 MQProducer 接口又继承自 MQAdmin 接口。

DefaultMQProducer 类的全属性构造方法为:

public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook,
    boolean enableMsgTrace, final String customizedTraceTopic) {
    this.namespace = namespace;
    this.producerGroup = producerGroup;
    defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    //if client open the message trace feature
    if (enableMsgTrace) {
        try {
            AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, customizedTraceTopic, rpcHook);
            dispatcher.setHostProducer(this.defaultMQProducerImpl);
            traceDispatcher = dispatcher;
            this.defaultMQProducerImpl.registerSendMessageHook(
                new SendMessageTraceHookImpl(traceDispatcher));
            this.defaultMQProducerImpl.registerEndTransactionHook(
                new EndTransactionTraceHookImpl(traceDispatcher));
        } catch (Throwable e) {
            log.error("system mqtrace hook init failed ,maybe can't send msg trace data");
        }
    }
}

根据这个构造方法,真正干活的类其实是 DefaultMQProducerImpl ,细心看代码的同学会发现,DefaultMQProducer 类下的方法,底层都是调用的 this.defaultMQProducerImpl 的方法,也就是 DefaultMQProducerImpl 类中的方法。构造方法首先是将命名空间、生产者所属组、RPCHook 钩子以及是否开启消息追踪等赋值到它的属性上。

DefaultMQProducer 的其他方法就不一一介绍了,我们知道怎么用就好了。

RocketMQ 生产者的启动流程

从上面的 Demo 我们知道,生产者在启动时,会调用 DefaultMQProducer 类的 start() 方法,而它的底层又是 DefaultMQProducerImpl 类的start() 方法。

一开始,主要做一些前置校验和一些赋值操作:

  • 首先,会 DefaultMQProducerImpl 默认的 serviceState = CREATE_JUST,switch 分支会走到创建的流程中
  • 然后,会检查 ProducerGroup 是否符合要求
  • 将 Producer 的 instanceName 即实例名称改为进程ID

完成后,会创建 MQClientInstance 实例:

  • 整个 JVM 实例中只存在一个的 MQClientManager 实例
  • MQClientManager 维护一个 ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable,clientID 由 客户端IP + instanceName + unitName(可选)生成
  • 创建 MQClientInstance 时,主要会初始化各种 Netty 网络操作对象,方便后续调用网络请求、心跳检测等
  • 最后启动 MQClientInstance

小结

这篇文章涉及比较多的代码解析,涉及的类名比较多,大家慢慢消化,多看几遍源码加深印象就容易掌握了。今天我们主要学了 RocketMQ 发送消息的 Demo、简要了解了 DefaultMQProducer 的属性和方法以及在源码的基础上分析了 RocketMQ 生产者的启动流程。明天,将会更加深入地讲解 RocketMQ 消息发送的基本流程和关键链路,大家多多关注和支持~