RocketMQ 消息发送
三种发送方式
- 同步
生产者发送消息,需要等到消息服务器返回结果
- 异步
生产者发送消息, 不需要等到消息服务器返回结果,生产者线程不阻塞,只需注册监听回调函数即可。
- 单向
生产者只管发,不管成功与否。
消息结构
org.apache.rocketmq.common.message.Message
private String topic;
private int flag;
// 属性
private Map<String, String> properties;
// 消息体
private byte[] body;
// 事务 ID
private String transactionId;
properties 存放的扩展属性主要有:
- tags
用于过滤消息
- keys
Message 索引键,多个用空格隔开,RocketMQ 可根据这些 key 快速检索消息
- waitStoreMsgOk
消息发送时,是否等到消息存储完,再返回
- delayTimeLevel
消息延迟级别,用于定时消息或消息重试
生产者
DefaultMQProducer 核心属性
// 生成者组,消息服务器在回查事务状态时,会随机选择该组中的任何一个生成者发起事务回查请求
private String producerGroup;
// 默认的 topic key
private String createTopicKey = MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC; // TBW102
// 默认的主题队列数量
private volatile int defaultTopicQueueNums = 4;
// 默认发送超时时间
private int sendMsgTimeout = 3000;
// 消息体超过 4kb 时,压缩消息
private int compressMsgBodyOverHowmuch = 1024 * 4;
// 同步发送失败,最大尝试次数
private int retryTimesWhenSendFailed = 2;
// 异步发送失败,最大尝试次数
private int retryTimesWhenSendAsyncFailed = 2;
// 消息重试时选择另外一个 broker 时,是否不等待存储结果就返回。默认 false
private boolean retryAnotherBrokerWhenNotStoreOK = false;
// 允许发送的最大消息长度
private int maxMessageSize = 1024 * 1024 * 4; // 4M
生产者启动
流程代码位于:DefaultMQProducerImpl#start
- 检查 productGroup,默认为 CLIENT_INNER_PRODUCER。
- 将 instanceName 修改为 PID。即 clientId 格式为 ip@pid
- 创建 MQClientInstance 实例,给交给 MQClientManager 管理
- 创建默认的 Topic(TBW102),并放到 topicPublishInfoTable 中。
消息发送流程
流程代码位于:DefaultMQProducerImpl#sendDefaultImpl
验证消息
查找路由
- Producer 优先从缓存中查找,存在并且正常直接返回
- 如果没有,则向 namesrv 发起请求, namesrv 返回 TopicRouteData。
- 判断返回的 TopicRouteData 与旧的是否被改变过;如果是,则TopicPublishInfo 更新 queueDatas,brokerDatas 信息。
重要的类:TopicPublishInfo
// 是否是顺序消息
private boolean orderTopic = false;
private boolean haveTopicRouterInfo = false;
// 主题的消息队列
private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
// 用于选择消息队列。每次选择一个队列,会自增1,如果等于 Integer.MAX_VALUE,重置为0
private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
private TopicRouteData topicRouteData;
// TopicRouteData
private String orderTopicConf;
private List<QueueData> queueDatas;
private List<BrokerData> brokerDatas;
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
代码位于:DefaultMQProducerImpl#tryToFindTopicPublishInfo(final String topic)
选择消息队列
不启用故障延迟机制
sendLatencyFaultEnable = false 默认不启用 broker 故障延迟机制。
RocketMQ 会避开上一次发送失败的 broker
代码入口:TopicPublishInfo#selectOneMessageQueue(final String lastBrokerName)
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
if (lastBrokerName == null) {
return selectOneMessageQueue();
} else {
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);
// 避开上一次发送失败的 broker
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
return selectOneMessageQueue();
}
}
启用故障延迟机制
代码入口:MQFaultStrategy#selectOneMessageQueue
消息发送
代码入口:DefaultMQProducerImpl#sendKernelImpl
- 找到所要发送的 broker 地址
- 为消息分配全局唯一ID,消息体超过4kb,则采用ZIP压缩。
- 如果注册了消息发送钩子函数,则执行消息发送之前的增强逻辑。通过DefaultMQProducerImpl#registerSendMessageHook() 注册钩子处理类,且可以注册多个
- 构造消息发送请求包
- 根据消息发送方式,进行发送(同步、异步、单向)
- 如果注册了消息发送钩子函数,执行after 逻辑
如果发送失败,则继续重复 选择消息队列,然后发送
同步发送
- 检查消息发送是否合理
- 消息重试次数超过最大重试次数,消息将进入 DLD 延迟队列。主题为:%DLD%
- 消息存储
异步方式
重试的调用入口是在收到服务端响应包时进行的。因此网络异常、网络超时,将不会触发重试。
单向发送
异步发送,不需要回调,没有重试机制
上篇疑问解答
Q: 路由发现与删除机制存在一个问题,Namesrv 需要等 Broker 失效 至少 120s 才能将 Broker 从路由表中删除。那么如果,Producer 获取到的 broker 是已经宕机的信息,那么是否会造成消息发送失败
单机环境肯定没救了,集群环境下没有影响的。
rocketMQ 每次发送消息时,会选择消息队列。第一次发送失败后,会进行失败的尝试。此时会将上一次发送失败的 broker 排除。重新选择一个 broker。
Q: namesrv 之间数据不共享,那么会造成消息的发送失败吗?
如果是当前保持连接的 namesrv 保存的 broker 信息,都挂掉了,那么一定是会失败的。(如果是这个原因,那么我觉得,所有的 broker 应该挂掉了)
如果是此 namesrv 没有 topic 对应的 broker ,那么 RocketMQ 会选择其他的 namesrv 保持连接,所以,不会发送失败。
相关代码:NettyRemotingClient#getAndCreateNameserverChannel()
如果只是 namesrv 中的某个 broker 挂掉了,但是因为 Producer 有消息重试机制,会选择其他的 broker。因此,消息不会发送失败。除非所有的 broker 都失败了。
结论:除非所有 broker 挂掉,不然我觉得不会造成消息发送失败。
Q: 集群中的 namesrv 挂掉了一个,是否会造成连接它的 broker 发送消息失败?
答案是不会。
原因是,每次再向 namesrv 发送消息时,需要判断与 namesrv 的 channel 是否有效。如果无效,则会尝试从剩下的 namesrv 查找一个有效的,并与之保持连接。
相关代码位于: NettyRemotingClient#getAndCreateNameserverChannel()
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。