RocketMQ之 Producer

producer 发送流程

producer 在消息发送时,肯定要先知道 topic 的信息,以及 topic 在哪个 broker 上,那么 producer 是如何做的呢?

还记得在 01 开篇说的吗? namesrv 提供一个通过 topic 获取路由信息的接口(RouteInfoManager#pickupTopicRouteData). producer 就是根据该接口返回的 TopicRouteData, 知道要将 topic 发送至哪个 broker.

TopicRouteData

private String orderTopicConf;
private List<QueueData> queueDatas;
private List<BrokerData> brokerDatas;
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

从返回的结构来看,知道了 broker 地址,还有 topic 下有哪些队列,接下来就可以发消息了。

producer 获取了 topic 的信息,会将这些信息封装为另外一个数据结构

class TopicPublishInfo {
    ...
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
    ... 
}

class MessageQueue {
    private String topic;
    private String brokerName;
    private int queueId;
}

messageQueueList 大概长这样子

[
    {
        "brokerName":"broker-a",
        "queueId":0,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-a",
        "queueId":1,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-a",
        "queueId":2,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-a",
        "queueId":3,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-b",
        "queueId":0,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-b",
        "queueId":1,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-b",
        "queueId":2,
        "topic":"TBW102%zouLeTopic"
    },
    {
        "brokerName":"broker-b",
        "queueId":3,
        "topic":"TBW102%zouLeTopic"
    }
]

可以看到,上面的 topic TBW102%zouLeTopicbroker-b, broker-a 都存在

我们知道了 topic 可以有很多队列,那么 producer 在发消息时,如何选择哪个队列发送呢?

答案是 从 messageQueueList 中轮询选择一个队列进行发送(每个线程,都有自己的计数器)。

TopicPublishInfo#selectOneMessageQueue(final String lastBrokerName)

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    if (lastBrokerName == null) {
        // 大部分情况走这个函数
        return selectOneMessageQueue();
    } else {
        // 如果 lastBrokerName 不为 null,那么会选择一个与 lastBrokerName 名称不一样的 broker。
        int index = this.sendWhichQueue.getAndIncrement();
        for (int i = 0; i < this.messageQueueList.size(); i++) {
            int pos = Math.abs(index++) % this.messageQueueList.size();
            if (pos < 0)
                pos = 0;
            MessageQueue mq = this.messageQueueList.get(pos);
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }
        // 如果没有找到,兜底返回
        return selectOneMessageQueue();
    }
}

public MessageQueue selectOneMessageQueue() {
    int index = this.sendWhichQueue.getAndIncrement();
    int pos = Math.abs(index) % this.messageQueueList.size();
    if (pos < 0)
        pos = 0;
    return this.messageQueueList.get(pos);
}
假如 producer 在发送消息时,如果发送失败了,会怎么处理?

TopicPublishInfo#selectOneMessageQueue(final String lastBrokerName) 方法的代码。可以看到,如果 lastBrokerName 不为 null 的时候,会选择一个与 lastBrokerName 名称不一样的 broker 返回。
那么 lastBrokerName 什么时候会不为 null 呢? 在消息发送失败,进行重试的时候,就会不为null.(默认至多重试2次

producer 端的发送延迟故障转移

实际上,在 producer 端,还有一个参数 sendLatencyFaultEnable 控制着,是否开启 发送延迟故障转移。但是,该参数默认为 false。
当该参数开启时,就不走上述的逻辑。

逻辑实现在该类下:MQFaultStrategy
该类主要做的事情就是:根据发送消息到 broker 花费的时间,判断 producer 应该在多久之内不选择此 broker 进行消息的发送。

producer 已经知道要发往哪个 broker 了,那么 rocketMQ 提供哪几种发送方式?各自区别又是什么?

producer 端提供了三种方式方式:同步、异步、单向

同步:
producer 会等待 broker
可以触发 producer 的重试机制

异步:
producer 会在线程池中,执行消息发送的逻辑
可以触发 producer 的重试机制

实际上,异步发送时,只能对 broker 端出现的错误进行重试。如果因为网络问题的话,是无法进行重试的。因为重试是在回调里面进行的

单向:
单向发送的意思时,只管发送,不管消息是否发送成功与否。
无法触发 producer 的重试机制

异步发送重试逻辑
MQClientAPIImpl#sendMessageAsync

private void sendMessageAsync(
        final String addr,
        final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final MQClientInstance instance,
        final int retryTimesWhenSendFailed,
        final AtomicInteger times,
        final SendMessageContext context,
        final DefaultMQProducerImpl producer
    ) throws InterruptedException, RemotingException {
    this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
        @Override
        public void operationComplete(ResponseFuture responseFuture) {
            RemotingCommand response = responseFuture.getResponseCommand();

            // todo 没有设置回调
            if (null == sendCallback && response != null) {

                try {
                    SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response);
                    if (context != null && sendResult != null) {
                        context.setSendResult(sendResult);
                        context.getProducer().executeSendMessageHookAfter(context);
                    }
                } catch (Throwable e) {
                }

                producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);
                return;
            }

            if (response != null) {
                try {
                   ... 业务错里
                } catch (Exception e) {
                    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);

                    // todo 出现异常,该方法会再次调用消息发送
                    onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                        retryTimesWhenSendFailed, times, e, context, false, producer);
                }
            } else {
                //...
                if (!responseFuture.isSendRequestOK()) {
                    // .. 
                     // todo 出现异常,该方法会再次调用消息发送
                    onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                        retryTimesWhenSendFailed, times, ex, context, true, producer);
                } else if (responseFuture.isTimeout()) {
                    // ...
                    // todo 出现异常,该方法会再次调用消息发送
                    onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                        retryTimesWhenSendFailed, times, ex, context, true, producer);
                } else {
                     // ...
                     // todo 出现异常,该方法会再次调用消息发送
                    onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                        retryTimesWhenSendFailed, times, ex, context, true, producer);
                }
            }
        }
    });
}

producer,最后会在 onExceptionImpl 方法里面,进行重试(默认至多重试2次

总结
消息发送流程
  1. 获取 topic 所在的 broker
  2. 轮询选择队列
  3. 如果发送失败(同步、异步),会进行重试(默认至多2次
其他
  1. 异步发送时,如果出现网络原因,是无法进行重试的
  2. producer 端有参数 sendLatencyFaultEnable 可开启 发送延迟故障转移机制
  3. producer 消息发送失败时,默认的策略时,会避免上一次发送失败的 broker(如果是双主结构)

rocketMQ 消息协议的设计

聊完了,producer 的发送流程。那么 producer 与 broker 在通信时,肯定要遵循一定的协议,在 rocketMQ 中,消息的协议是如何设计的呢?

说这个事情之前,先来思考一下,如何设计协议?需要有哪些要素?

一个协议,正常会包含以下信息

  • 魔数(非必须),用来在第一时间判定是否是无效数据包
  • 版本号,可以支持协议的升级
  • 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
  • 指令类型,业务相关。如获取路由信息,注册 broker 信息
  • 请求序号(非必须),为了双工通信,提供异步能力
  • 消息长度
  • 消息正文
rocketMQ 消息协议设计

NettyEncoder#encode()
在这里插入图片描述
RemotingCommand rocketMQ 消息协议类

public class RemotingCommand {
    
    // 指令类型
    private int code;
    private LanguageCode language = LanguageCode.JAVA;
     
    // 版本号 
    private int version = 0;
    
    
    private int opaque = requestId.getAndIncrement();
    private int flag = 0;
    private String remark;
    private HashMap<String, String> extFields;
    private transient CommandCustomHeader customHeader;

    private SerializeType serializeTypeCurrentRPC = serializeTypeConfigInThisServer;
    
    // 消息正文
    private transient byte[] body;
    
    // netty 会调用该方法,对消息进行编码
    public ByteBuffer encodeHeader(final int bodyLength) {
        // 1> header length size
        int length = 4;

        // 2> header data length
        byte[] headerData;
        headerData = this.headerEncode();

        length += headerData.length;

        // 3> body data length
        length += bodyLength;

        ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength);

        // length
        result.putInt(length);

        // header length
        result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC));

        // header data
        result.put(headerData);

        result.flip();

        return result;
    }
}

客户端 netty 的线程模型

NettyRemotingClient

Netty Bootstrap 构建

this.bootstrap
.group(this.eventLoopGroupWorker)
.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // ...
        pipeline.addLast(
            defaultEventExecutorGroup,
            new NettyEncoder(),
            new NettyDecoder(),
           // ...
    }
});

work EventLoopGroup线程数

// 写死的 1 ,不可变
this.eventLoopGroupWorker = new NioEventLoopGroup(1, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, String.format("NettyClientSelector_%d", this.threadIndex.incrementAndGet()));
            }
        });

IO 线程数

this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
            // 写死的 4 ,可配置
            nettyClientConfig.getClientWorkerThreads(),
            new ThreadFactory() {

                private AtomicInteger threadIndex = new AtomicInteger(0);

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

代码鉴赏

ThreadLocalIndex
public class ThreadLocalIndex {
    private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();
    private final Random random = new Random();

    public int getAndIncrement() {
        Integer index = this.threadLocalIndex.get();
        if (null == index) {
            index = Math.abs(random.nextInt());
            if (index < 0)
                index = 0;
            this.threadLocalIndex.set(index);
        }
            
        // todo 细节
        index = Math.abs(index + 1);
        if (index < 0)
            index = 0;

        this.threadLocalIndex.set(index);
        return index;
    }

    @Override
    public String toString() {
        return "ThreadLocalIndex{" +
            "threadLocalIndex=" + threadLocalIndex.get() +
            '}';
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值