RocketMQ源码(四):RocketMQ生产者发送消息流程
RocketMQ通过Producer发送消息,以同步方式发送普通消息为例,分析发送消息的整体流程。Producer的示例代码如下:
1 import org.apache.rocketmq.client.producer.DefaultMQProducer; 2 import org.apache.rocketmq.client.producer.SendResult; 3 import org.apache.rocketmq.common.message.Message; 4 import org.apache.rocketmq.remoting.common.RemotingHelper; 5 // 同步发送 6 public class SyncProducer { 7 public static void main(String[] args) throws Exception{ 8 // 实例化消息生产者Producer 9 DefaultMQProducer producer = new DefaultMQProducer("group_test"); 10 try{ 11 // 设置NameServer的地址 12 producer.setNamesrvAddr("127.0.0.1:9876"); 13 // 启动Producer实例 14 producer.start(); 15 for (int i = 0; i < 2; i++) { 16 // 创建消息,并指定Topic,Tag和消息体 17 Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); 18 // 发送消息到一个Broker 19 SendResult sendResult = producer.send(msg); 20 System.out.printf("%s%n", sendResult); 21 } 22 }finally { 23 //如果不再发送消息,关闭Producer实例。 24 producer.shutdown(); 25 } 26 } 27 }
根据上述代码,生产者发送消息主要完成以下几件事:
1、创建生产者对象DefaultMQProducer并设置NameServer地址
2、启动生产者
3、发送消息
一、创建生产者
DefaultMQProducer构造函数详情如下:
1 public DefaultMQProducer(final String producerGroup) { 2 this(null, producerGroup, null); 3 } 4 5 public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { 6 this.namespace = namespace; 7 this.producerGroup = producerGroup; 8 defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); 9 }
DefaultMQProducer中的持有关键属性DefaultMQProducerImpl。DefaultMQProducerImpl是实际干活的,DefaultMQProducer的启动与消息发送都依赖于DefaultMQProducerImpl中方法,利用装饰者模式。
二、启动生产者
1 // 启动生产者 2 public void start(final boolean startFactory) throws MQClientException { 3 switch (this.serviceState) { 4 // 刚创建未启动 5 case CREATE_JUST: 6 this.serviceState = ServiceState.START_FAILED; 7 // 检查配置 8 this.checkConfig(); 9 // 更改当前instanceName为进程ID 10 if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) { 11 this.defaultMQProducer.changeInstanceNameToPID(); 12 } 13 14 // 获取MQ客户端实例 15 this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook); 16 17 // 注册Producer到MQClientInstance客户端实例 18 boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this); 19 // 未注册成功,抛出异常 20 if (!registerOK) { 21 this.serviceState = ServiceState.CREATE_JUST; 22 throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup() 23 + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), 24 null); 25 } 26 27 // 将Topic发布信息添加到topicPublishInfoTable属性中 28 this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo()); 29 30 // 启动MQ客户端实例 31 if (startFactory) { 32 // todo 最终还是调用MQClientInstance 33 mQClientFactory.start(); 34 } 35 36 log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(), 37 this.defaultMQProducer.isSendMessageWithVIPChannel()); 38 this.serviceState = ServiceState.RUNNING; 39 break; 40 case RUNNING: 41 case START_FAILED: 42 case SHUTDOWN_ALREADY: 43 throw new MQClientException("The producer service state not OK, maybe started once, " 44 + this.serviceState 45 + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), 46 null); 47 default: 48 break; 49 } 50 51 // 向所有的broker发送心跳 52 this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); 53 54 // 扫描并移除超时请求,并执行回调方法onException 55 this.timer.scheduleAtFixedRate(new TimerTask() { 56 @Override 57 public void run() { 58 try { 59 RequestFutureTable.scanExpiredRequest(); 60 } catch (Throwable e) { 61 log.error("scan RequestFutureTable exception", e); 62 } 63 } 64 }, 1000 * 3, 1000); 65 }
1、检查配置
DefaultMQProducerImpl#checkConfig() 核心代码:
1 private void checkConfig() throws MQClientException { 2 // 组名称校验 3 Validators.checkGroup(this.defaultMQProducer.getProducerGroup()); 4 // 组名称为null,抛出异常 5 if (null == this.defaultMQProducer.getProducerGroup()) { 6 throw new MQClientException("producerGroup is null", null); 7 } 8 //不能和系统的分组名冲突(DEFAULT_PRODUCER) 9 if (this.defaultMQProducer.getProducerGroup().equals(MixAll.DEFAULT_PRODUCER_GROUP)) { 10 throw new MQClientException("producerGroup can not equal " + MixAll.DEFAULT_PRODUCER_GROUP + ", please specify another one.", 11 null); 12 } 13 }
2、获取客户端实例
DefaultMQProducerImpl#start() 获取客户端实例的代码段:
1 // 获取MQ客户端实例 2 this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook); 3 // 注册Producer到MQClientInstance客户端实例 4 boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
2.1、获取MQClientInstance实例
在MQClientManager中维护了一个clinetId与MQClientInstance映射的缓存表。获取MQ客户端实例MQClientManager#getOrCreateMQClientInstance() 核心代码:
1 public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) { 2 // 获取客户端ID 3 String clientId = clientConfig.buildMQClientId(); 4 // 根据客户端id缓存中获取MQClientInstance 5 MQClientInstance instance = this.factoryTable.get(clientId); 6 // 若为空,则创建实例并添加到实例表中 7 if (null == instance) { 8 // 创建MQClientInstance 9 instance = 10 new MQClientInstance(clientConfig.cloneClientConfig(), 11 this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook); 12 // 添加进factoryTable缓存 13 MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance); 14 if (prev != null) { 15 instance = prev; 16 log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId); 17 } else { 18 log.info("Created new MQClientInstance for clientId:[{}]", clientId); 19 } 20 } 21 return instance; 22 }
一个clientId只会创建一个MQClientInstance,添加进MQClientManager的缓存表。clientId的生成规则IP@instanceName@unitName。
1 // IP@instanceName@unitName相同,复用同一个MQClientInstance 2 public String buildMQClientId() { 3 StringBuilder sb = new StringBuilder(); 4 // IP 5 sb.append(this.getClientIP()); 6 sb.append("@"); 7 // 实例名称 8 sb.append(this.getInstanceName()); 9 // unitName不为null 10 if (!UtilAll.isBlank(this.unitName)) { 11 sb.append("@"); 12 sb.append(this.unitName); 13 } 14 return sb.toString(); 15 }
对于RocketMQ而言,消息发送者、消息消费者都属于客户端,每一个客户端就是一个MQClientInstance,每一个ClientConfig对应一个实例。
MQClientInstance通过ClientConfig属性,既关联生产者又关联消费者。
在创建MQClientInstance对象时,会初始化远程客户端MQClientAPIImpl,用于网络处理API,是消息生产者和消息消费者与NameServer、Broker打交道的网络通道。
2.2、注册组名与生产者实例映射关系
1 // 生产者缓存表 2 private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<String, MQProducerInner>(); 3 4 public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) { 5 if (null == group || null == producer) { 6 return false; 7 } 8 // 将生产者组名称与生产者实例映射关系 添加进MQClientInstance中 9 MQProducerInner prev = this.producerTable.putIfAbsent(group, producer); 10 // 若MQClientInstanc已存在生产者实例,注册失败 11 if (prev != null) { 12 log.warn("the producer group[{}] exist already.", group); 13 return false; 14 } 15 return true; 16 }
将生产者实例注册到客户端实例MQClientInstance缓存表中。
3、启动客户端实例
1 public void start() throws MQClientException { 2 synchronized (this) { 3 switch (this.serviceState) { 4 // 刚创建未启动 5 case CREATE_JUST: 6 this.serviceState = ServiceState.START_FAILED; 7 // 未指定NameServer地址,通过http的方式远程调用NameServer服务,获取NameServer地址 8 if (null == this.clientConfig.getNamesrvAddr()) { 9 this.mQClientAPIImpl.fetchNameServerAddr(); 10 } 11 // 开启网络通信 NRC 12 this.mQClientAPIImpl.start(); 13 // 开启定时执行任务 14 this.startScheduledTask(); 15 // 开启拉取消息服务 16 this.pullMessageService.start(); 17 // 开启负载均衡服务 18 this.rebalanceService.start(); 19 // 开启消息推送服务 20 this.defaultMQProducer.getDefaultMQProducerImpl().start(false); 21 // 设置当前服务状态为运行中 22 this.serviceState = ServiceState.RUNNING; 23 break; 24 case START_FAILED: 25 throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null); 26 default: 27 break; 28 } 29 } 30 }
1、开启网络通信
2、开启定时任务
MQClientInstance#startScheduledTask() 核心代码:
1 /** 2 * 开启各种定时任务 3 * 生产者或者消费者不是实时感知Broker的状态,而是会有一定的偏差, 4 * 所以如果服务器出现宕机,生产者或者消费要自行处理故障 5 */ 6 private void startScheduledTask() { 7 // 2min同步NameServer地址服务 8 if (null == this.clientConfig.getNamesrvAddr()) { 9 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 10 11 @Override 12 public void run() { 13 try { 14 MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr(); 15 } catch (Exception e) { 16 log.error("ScheduledTask fetchNameServerAddr exception", e); 17 } 18 } 19 }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS); 20 } 21 22 // 30s同步NameServer中的Topic路由信息 23 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 24 25 @Override 26 public void run() { 27 try { 28 MQClientInstance.this.updateTopicRouteInfoFromNameServer(); 29 } catch (Exception e) { 30 log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e); 31 } 32 } 33 }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS); 34 35 // 30s剔除下线的broker、发送心跳检测到broker 36 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 37 @Override 38 public void run() { 39 try { 40 MQClientInstance.this.cleanOfflineBroker(); 41 MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); 42 } catch (Exception e) { 43 log.error("ScheduledTask sendHeartbeatToAllBroker exception", e); 44 } 45 } 46 }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS); 47 48 // 5s 持久化消费进度 49 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 50 51 @Override 52 public void run() { 53 try { 54 MQClientInstance.this.persistAllConsumerOffset(); 55 } catch (Exception e) { 56 log.error("ScheduledTask persistAllConsumerOffset exception", e); 57 } 58 } 59 }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS); 60 61 // 调整线程池异步任务 62 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 63 @Override 64 public void run() { 65 try { 66 MQClientInstance.this.adjustThreadPool(); 67 } catch (Exception e) { 68 log.error("ScheduledTask adjustThreadPool exception", e); 69 } 70 } 71 }, 1, 1, TimeUnit.MINUTES); 72 }
同步NameServer地址服务定时任务、同步NameServer中的Topic路由信息、持久化消费进度、发送心跳检测到broker
3、开启消息拉取服务、负载均衡服务
开启消息拉取服务、负载均衡服务,ServiceThread#start() 核心代码:
1 public void start() { 2 log.info("Try to start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread); 3 if (!started.compareAndSet(false, true)) { 4 return; 5 } 6 stopped = false; 7 this.thread = new Thread(this, getServiceName()); 8 this.thread.setDaemon(isDaemon); 9 this.thread.start(); 10 }
此处只是开启了线程,具体执行逻辑在PullMessageService、RebalanceService的重写方法run()中。
三、发送消息
根据DefaultMQProducer的send方法,最终调到DefaultMQProducerImpl的sendDefaultImpl方法,核心代码段如下。
1、传递数据合法性校验
DefaultMQProducerImpl#sendDefaultImpl()方法,代码段:
校验传送的消息与Topic,Validators#checkMessage() 核心代码:
1 // 校验消息核心代码 2 public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer) 3 throws MQClientException { 4 if (null == msg) { 5 throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null"); 6 } 7 // topic规范检查 8 Validators.checkTopic(msg.getTopic()); 9 // topic是否能写入消息 10 Validators.isNotAllowedSendTopic(msg.getTopic()); 11 // 消息非空检查 12 if (null == msg.getBody()) { 13 throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null"); 14 } 15 // 消息长度检查 16 if (0 == msg.getBody().length) { 17 throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero"); 18 } 19 if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) { 20 throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, 21 "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize()); 22 } 23 }
2、查找路由
DefaultMQProducerImpl#sendDefaultImpl()方法,代码段:
查找路由信息,DefaultMQProducerImpl#tryToFindTopicPublishInfo() 核心代码:
1 // topic与topic路由信息映射缓存表 2 private final ConcurrentMap<String, TopicPublishInfo> topicPublishInfoTable = 3 new ConcurrentHashMap<String, TopicPublishInfo>(); 4 // MQ客户端实例 5 private MQClientInstance mQClientFactory; 6 7 // 查找路由信息 8 private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) { 9 // 获取缓存表中的topic路由信息 10 TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic); 11 // 缓存表中路由信息为空,则从NameServer获取topic路由信息 12 if (null == topicPublishInfo || !topicPublishInfo.ok()) { 13 this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo()); 14 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); 15 topicPublishInfo = this.topicPublishInfoTable.get(topic); 16 } 17 // 缓存表中存在或Nameserver中存在topic的路由信息,直接返回 18 if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) { 19 return topicPublishInfo; 20 // 若未找到当前topic的路由信息,则用默认主题继续查找 21 } else { 22 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer); 23 topicPublishInfo = this.topicPublishInfoTable.get(topic); 24 return topicPublishInfo; 25 } 26 }
优先获取缓存表中获取topic的路由信息TopicPublishInfo,若缓存表中存在,返回路由信息。
若缓存表中不存在,根据topic从nameserver中获取路由信息,若获取到路由信息,设置进缓存表并返回路由信息。
若nameserver中未查找到topic路由信息,使用默认的主题继续从nameserver中查找路由信息。
3、选择发送消息队列
DefaultMQProducerImpl#sendDefaultImpl()方法,代码段:
DefaultMQProducerImpl#selectOneMessageQueue() 核心代码:
1 // 故障延迟标识开关,默认关闭 2 private boolean sendLatencyFaultEnable = false; 3 4 // 选择消息队列 5 public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) { 6 // Broker故障延迟机制,默认关闭 7 if (this.sendLatencyFaultEnable) { 8 try { 9 int index = tpInfo.getSendWhichQueue().getAndIncrement(); 10 // 1、对消息队列轮询获取一个队列 11 for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) { 12 // 基于index和队列数量取余,确定位置 13 int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size(); 14 if (pos < 0) 15 pos = 0; 16 // 2、获取消息队列 17 MessageQueue mq = tpInfo.getMessageQueueList().get(pos); 18 // 3、消息队列所在的broker可用,返回消息队列;不可用,获取下一个消息队列 19 if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) 20 return mq; 21 } 22 23 // 4、若预测的所有broker都不可用,则随机选择一个broker,随机选择该Broker下一个队列进行发送 24 final String notBestBroker = latencyFaultTolerance.pickOneAtLeast(); 25 // 获得Broker的写队列集合 26 int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker); 27 if (writeQueueNums > 0) { 28 final MessageQueue mq = tpInfo.selectOneMessageQueue(); 29 if (notBestBroker != null) { 30 // 获得一个队列,指定broker和队列ID并返回 31 mq.setBrokerName(notBestBroker); 32 mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums); 33 } 34 return mq; 35 } else { 36 latencyFaultTolerance.remove(notBestBroker); 37 } 38 } catch (Exception e) { 39 log.error("Error occurred when selecting message queue", e); 40 } 41 42 return tpInfo.selectOneMessageQueue(); 43 } 44 45 // 默认执行 46 return tpInfo.selectOneMessageQueue(lastBrokerName); 47 }
故障延迟处理标识开关sendLatencyFaultEnable默认关闭,开关关闭,RocketMQ选择消息队列采用轮询的方式,轮询算法能保证每一个Queue队列的消息投递数量尽可能均匀。
1 // 默认使用轮询方式选择消息队列 2 public MessageQueue selectOneMessageQueue() { 3 // 使用 ThreadLocal 进行 sendWhichQueue 的自增 4 int index = this.sendWhichQueue.getAndIncrement(); 5 // 对队列大小取模 6 int pos = Math.abs(index) % this.messageQueueList.size(); 7 if (pos < 0) 8 pos = 0; 9 // 返回对应的队列 10 return this.messageQueueList.get(pos); 11 }
开关打开,RocketMQ就开启了故障延迟功能,每次向Broker成功或者异常的发送,RocketMQ都会计算出该Borker的可用时间(发送结束时间-发送开始时间,失败的按照30S计算),并且保存,方便下次发送时做broekr不可用时长的判断。
故障规避延时机制,MQFaultStrategy#updateFaultItem() 核心代码:
1 // 发送延时 2 private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L}; 3 // 故障规避 4 private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L}; 5 6 public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) { 7 if (this.sendLatencyFaultEnable) { 8 //获取不可用持续时长,在这个时间内,Broker将被规避 9 long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency); 10 this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration); 11 } 12 } 13 // 根据发送延时来定义故障规避的时间 14 private long computeNotAvailableDuration(final long currentLatency) { 15 for (int i = latencyMax.length - 1; i >= 0; i--) { 16 if (currentLatency >= latencyMax[i]) 17 return this.notAvailableDuration[i]; 18 } 19 return 0; 20 }
如果消息时长在550ms之内,不可用时长为0;达到550ms,不可用时长为30S;达到1000ms,不可用时长为60S;达到2000ms,不可用时长为120S;达到3000ms,不可用时长为180S;达到15000ms,不可用时长为600S。
故障延迟机制策略处理:遍历消息队列列表,获取列表中broker可用的消息队列;若列表中没有broker可用的消息队列,随机选择一个broker中的某个队列进行消息发送。
从RocketMQ的策略上可以看到,默认队列选择是轮训策略,而故障延迟选择队列则是优先考虑消息的发送时长短的队列。
当一个Topic创建在不同的Broker上时,通讯网络较好,推荐默认的轮询策略;通讯网络较差,推荐故障延迟机制,可避免不断向宕机的broker发送消息,实现消息发送的高可用。
4、消息发送
DefaultMQProducerImpl#sendDefaultImpl()方法,代码段:
DefaultMQProducerImpl#sendKernelImpl()方法,代码段:
MQClientAPIImpl#sendMessage()方法,代码段:
根据通讯模式,执行远程的调用:
MQClientAPIImpl#sendMessageSync()方法,代码段:
NettyRemotingClient#invokeSync(),代码段:
在RocketMQ中,客户端与Broker的连接是在发送消息时建立的,即只有在需要与对端进行数据交互时建立的网络通信连接,连接建立之后,发送同步消息。
NettyRemotingAbstract#invokeSync(),代码段:
通过netty发送消息到broker中,并监听响应结果。
四、生产者消息发送核心流程图