【三】客户端设计原理分析

168 阅读7分钟

生产者启动分析

DefaultMQProducerDefaultMQProducerImpl

  • DefaultMQProducer 门面 ==》 是生产者发送消息的入口。
  • DefaultMQProducerImpl 实际干活的

官方示例

public static void main(String[] args) throws MQClientException, InterruptedException {

    DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
    producer.start();

    for (int i = 0; i < 128; i++)
        try {
            {
                Message msg = new Message("TopicTest",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.send(msg);
                System.out.printf("%s%n", sendResult);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    producer.shutdown();
}

继承关系

public class DefaultMQProducer extends ClientConfig implements MQProducer {

  • ClientConfig 客户端相关的配置信息(生产/消费)
  • MQProducer 生产者抽象接口

public class DefaultMQProducerImpl implements MQProducerInner {

重点属性


// DefaultMQProducer
  
    // 获取路由信息的间隔时间周期, 30s
    private int pollNameServerInterval = 1000 * 30;
    // 客户端与broker之间心跳周期 30s
    private int heartbeatBrokerInterval = 1000 * 30;
    // 消费者持久化消费进度的周期, 5秒
    private int persistConsumerOffsetInterval = 1000 * 5;
    private long pullTimeDelayMillsWhenException = 1000;

    // 默认broker每个主题  创建的队列数量
    private volatile int defaultTopicQueueNums = 4;

    // 发送消息超时限制 默认 3s
    private int sendMsgTimeout = 3000;

    // 压缩阈值, 当msg body 超过4k后,选择使用压缩算法
    private int compressMsgBodyOverHowmuch = 1024 * 4;

    // 同步发送失败后,重试发送次数 , 2。 再加上第一次发送。 共3次
    private int retryTimesWhenSendFailed = 2;

    // 异步发送失败后,重试发送次数, 2
    private int retryTimesWhenSendAsyncFailed = 2;

    // 消息未存储成功, 是否选择其他broker节点进行消息重试,一般需要设置为true
    private boolean retryAnotherBrokerWhenNotStoreOK = false;

    // 消息体最大限制,默认值 4mb
    private int maxMessageSize = 1024 * 1024 * 4; // 4M
    
 // DefaultMQProducerImpl
 
    // 主题发布信息映射表
    // 主题:主题的发布信息
    private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable =
        new ConcurrentHashMap<String, TopicPublishInfo>();  

init(构造函数)

  • DefaultMQProducer : 创建defaultMQProducerImpl对象。

  • DefaultMQProducerImpl

    • 创建异步消息线程池队列,长度五万
    • 创建默认的 异步消息任务线程池。
    public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) {
        this.defaultMQProducer = defaultMQProducer;
        this.rpcHook = rpcHook;
    
        // 创建异步消息线程池任务队列,长度5w
        this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<Runnable>(50000);
    
        //创建缺省的 异步消息任务线程池
    
        /*
         * @param corePoolSize      线程池核心数,平台核心数
         * @param maximumPoolSize   线程池最大核心数,平台核心数
         * @param keepAliveTime     空闲线程的存活时间,60s
         * @param unit              时间单位
         * @param workQueue         异步消息线程池任务队列
         * @param threadFactory     线程工厂,创建出来的线程 AsyncSenderExecutor_ 前缀
         */
        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());
                    }
                });
    }
    

start()

  • DefaultMQProducer : 调用defaultMQProducerImpl对象start()方法。
  • DefaultMQProducerImpl
    1. 自己维护了启动状态serviceState,避免重复调用
    2. 一顿检查
    3. 拿到或者创建MQClientInstance。 这里叫 mQClientFactory
    4. 将自己(this)注册到mQClientFactory。 由 mQClientFactory 统一管理。
    5. 默认一个主题信息("TBW102")
    6. 启动mQClientFactory 如果 参数 startFactory为true的情况下。(一般为true)
    7. 强制 rocketMq 客户端实例 向已知的broker节点发送一次心跳,(讲 客户端定时任务时,再聊)
    8. 定时任务,处理回执消息 =》 太慢的话,从列表中删掉。

回执消息

// request 发送的消息 需要 消费者 回执一条消息。
// 生产者 msg 加了一些信息, 关联ID,客户端ID , 发送到broker之后
// 消费者从 broker 拿到这条消息,检查msg类型, 发现是一个 需要回执的消息。
// 处理完消息之后,根据msg 关联ID 和 客户端ID 生产一条消息,(封装响应给 生产者的结果) 发送到broker
// broker 拿到这个消息后,它知道这是一条 回执消息,根据客户端id,找到channel, 将消息推送给 生产者。
// 生产者这边拿到 回执消息之后,读取出来 关联ID,找到对应的RequestFuture, 将阻塞的线程唤醒。
// 类似于 生产者和消费者 之间进行了一个RPC通信,只不过中间由 broker代理完成的。
//

MqClientInstance

客户端实例,一个JVM进程只有一个该实例。(好像4.9以后的版本改了)

重点属性

   // 生产者,消费者 映射表。 组名 : 生产者,或者消费者
    private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<String, MQProducerInner>();
    private final ConcurrentMap<String/* group */, MQConsumerInner> consumerTable = new ConcurrentHashMap<String, MQConsumerInner>();
    private final ConcurrentMap<String/* group */, MQAdminExtInner> adminExtTable = new ConcurrentHashMap<String, MQAdminExtInner>();

    // 客户端本地路由数据
    // 主题名称: 主题路由数据
    private final ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable = new ConcurrentHashMap<String, TopicRouteData>();
    
    
    

    // broker 物理节点映射表
    // key : brokerName  逻辑层面的东西。
    // val : HashMap<Long/* brokerId */, String/* address */>
    //                      brokerId 0是master节点 其他是slave
    //                      address 地址 ip:端口
    private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable =
            new ConcurrentHashMap<String, HashMap<Long, String>>();

    // broker 物理节点版本映射表
    // key : brokerName  逻辑层面的东西。
    // val : HashMap<String/* address */, Integer>
    //                        address 物理节点地址
    //                                版本号
    private final ConcurrentMap<String/* Broker Name */, HashMap<String/* address */, Integer>> brokerVersionTable =
            new ConcurrentHashMap<String, HashMap<String, Integer>>();

    

init(构造函数)

  1. 给属性赋值
      /**
      * @param clientConfig  客户端配置
      * @param instanceIndex index 一般是0
      * @param clientId      客户端id
      * @param rpcHook       rpc hook
      */
    
  2. 创建客户端协议处理器
  3. 创建API实现对象 mQClientAPIImpl。>>> 核心的一个API实现,它几乎包含了所有服务端 的API, 它的作用就是将MQ业务层的数据 转换为 网络层的 RemotingCommand 对象
      /**
      * @param nettyClientConfig       客户端网络配置
      * @param clientRemotingProcessor 客户端协议处理器,要注册到 客户端网络层、
      * @param rpcHook                 rpc hook  要注册到网络层
      * @param clientConfig            客户端配置
      */
     this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig);
    
    
  4. 创建内部生产者实例,消息回退时使用。

start()

  1. 启动客户端网络层 this.mQClientAPIImpl.start();
  2. 启动定时任务 this.startScheduledTask()
  3. 还有一些消费者相关的内容,先跳过

MQClientAPIImpl

重点属性

    /**
     * 客户端网络层对象, 管理客户端 与 服务器之间 连接 NioSocketChannel 对象
     * 通过它提供的 invoke 系列方法, 客户端可以与服务端进行远程调用。
     * 服务器 也可以直接调用客户端
     */
    private final RemotingClient remotingClient;

init(构造函数)

  1. 为属性赋值
  2. 创建网络层对象
        // 创建网络层对象
        /**
         * 网络层对象
         * @param nettyClientConfig 客户端网络层配置
         * @param channelEventListener null , 客户端并不关心 channel event
         */
        this.remotingClient = new NettyRemotingClient(nettyClientConfig, null);
    
  3. 注册rpc hook
  4. 注册业务逻辑

start()

  • 调用客户端网络层启动 this.remotingClient.start();
  • 启动定时任务(细说)
  • 消费者服务启动
  • 负载均衡服务启动
  • 内部生产者启动
定时任务
  • 定时任务1: 从nameserver 更新客户端本地的路由数据。周期30秒
  • 定时任务2: 清理下线的broker节点。向在线的broker节点发送心跳数据。周期30秒、
  • 定时任务3: 消费者持久化 消费进度。周期5秒。(后面细说)
  • 定时任务4: 动态调整消费者线程池。周期60秒。(后面细说)

NettyRemotingClient

客户端网络层封装, 在nameserver时,有一个NettyRetimgServer的类,是类似的

重点属性

 // Channel 映射表 , key : 服务器地址  value : 客户端-服务端 channel 封装对象
    private final ConcurrentMap<String /* addr */, ChannelWrapper> channelTables = new ConcurrentHashMap<String, ChannelWrapper>();

init(构造函数)

  • 创建一些线程池,用来处理网络请求

start()

  • 配置 netty 客户端启动类对象
  • 定时扫描 ResposeFutureTable 中超时的ResponseFuture 避免 客户端线程 长时间 阻塞。

生产者发送消息分析

同步发送 DefaultMQProducer.send(Message msg)

异步发送 DefaultMQProducer.send(Message msg, SendCallback sendCallback)

同步和异步都是调用DefaultMQProducerImpl#sendDefaultImpl()方法进行发送的。只是异步操作采用了线程池,我们直接看sendDefaultImpl方法就好。


   /**
     * @param msg     消息
     * @param timeout 发送超时时间 默认3秒
     * @return
     * @throws MQClientException
     * @throws RemotingException
     * @throws MQBrokerException
     * @throws InterruptedException
     */
    public SendResult send(Message msg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        //    * @param msg 消息
        //    * @param communicationMode 模式 (同步)
        //    * @param sendCallback 回调,同步发送时,不需要提供这个参数,只有异步才需要
        //    * @param timeout 超时
        return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
    }
    
        /**
     * It will be removed at 4.4.0 cause for exception handling and the wrong Semantics of timeout. A new one will be
     * provided in next version
     *
     * @param msg
     * @param sendCallback
     * @param timeout      the <code>sendCallback</code> will be invoked at most time
     * @throws RejectedExecutionException
     */
    @Deprecated
    public void send(final Message msg, final SendCallback sendCallback, final long timeout)
            throws MQClientException, RemotingException, InterruptedException {
        final long beginStartTime = System.currentTimeMillis();
        ExecutorService executor = this.getAsyncSenderExecutor();
        try {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    long costTime = System.currentTimeMillis() - beginStartTime;
                    if (timeout > costTime) {
                        try {
                            sendDefaultImpl(msg, CommunicationMode.ASYNC, sendCallback, timeout - costTime);
                        } catch (Exception e) {
                            sendCallback.onException(e);
                        }
                    } else {
                        sendCallback.onException(
                                new RemotingTooMuchRequestException("DEFAULT ASYNC send call timeout"));
                    }
                }

            });
        } catch (RejectedExecutionException e) {
            throw new MQClientException("executor rejected ", e);
        }

    }
    
    

sendDefaultImpl

  1. 检查生产者的状态
  2. 判断消息规格(Message对象的字段)
  3. 生成一个调用id,打日志用
  4. 获取当前消息 主题的发布信息, 需要依赖它里面的 MessageQueues 消息,选择一个队列, 后面去发送消息使用。
  5. 发送。有重试机制 // 发送总尝试次数, 同步模式 1+2 =3 。 异步模式 1。
    1. 选择一个MessageQueue。
    2. 调用核心方法sendKernelImpl
      // 核心方法
      /*
       * @param msg 消息
       * @param mq mq, 选择的队列
       * @param communicationMode 发送模式 (同步,异步,单向)
       * @param sendCallback 异步发送时,需要传递一个回调处理对象。同步,单向时 为null
       * @param topicPublishInfo 主题发布信息
       * @param timeout 超时限制
       */
     sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
    
    1. 根据发送模式不同,进行处理
      • 异步或者单向发送
        // 异步或者单向, 直接 返回null
        // 异步 返回值由sendCallback 和回调线程处理。
        // 单向 服务器不返回任何数据
        
      • 同步发送: 检查是否发送成功,失败的话尝试换一个broker节点发送。(是否切换,是个配置项。)

sendKernelImpl

核心发送,组装数据,调用网络端

image.png