## 一 : 消息的生产

1. 消息的生产过程

Producer在发送消息时可以将消息写入到指定topic的某Broker中的某Queue中,其经历了如下过程:

  • Producer发送消息之前,会先向NameServer发出获取消息Topic的路由信息的请求

  • NameServer返回该Topic的路由表Broker列表

  • Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息

  • Produer对消息做一些特殊处理,例如,消息本身超过4M,则会对其进行压缩

  • Producer向选择出的Queue所在的Broker发出RPC请求,将消息发送到选择出的Queue

nameServer维护的路由表,实际是一个MapkeyTopic名称,value是一个QueueData实例集合

而一个QueueData则包含一个Broker实例的所有此topic的Queue信息

即一个Broker对应一个QueueData。QueueData中包含brokerName

简单来说,路由表的keyTopic名称,value则为所有涉及该Topic的 BrokerName列表

那根据Topic可以获取到BrokerName,怎么由BrokerName获取到对应的连接地址信息呢

Broker列表:其实际也是一个MapkeybrokerNamevalueBrokerData

一个BrokerData对应一套brokerName名称相同的Master-Slave小集群

BrokerData中包含brokerName及一个map。该map的key为brokerId,value为该

broker对应的地址。brokerId0表示该brokerMaster,非0表示Slave

2. Queue选择算法

对于无序消息,其Queue选择算法,也称为消息投递算法,常见的有两种:

  • 1.轮询算法

默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。

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);
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);
}

该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致

Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。

  • 2.最小投递延迟算法

该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。

如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。

	// 根据sendLatencyFaultEnable 是否打开决定是否使用
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
//基于index和队列数量取余,确定位置
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
} // 从延迟容错broker列表中挑选一个容错性最好的一个 broker
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
// 取余挑选其中一个队列
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
// 取余挑选其中一个队列
return tpInfo.selectOneMessageQueue();
} return tpInfo.selectOneMessageQueue(lastBrokerName);
}

该算法也存在一个问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量

的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆

积。

当然我们也可以自己指定选择算法,通过实现MessageQueueSelector ,自带的有三个实现:

  • SelectMessageQueueByRandom 随机分配策略
  • SelectMessageQueueByHash 基于hash的分配策略
  • SelectMessageQueueByMachineRoom 服务器的就近原则分配策略

其中hash算法就可以用来解决,对于消息的时序性有严格要求,需要保证全局有序的情况,例如一个订单的:创建、付款、推送、完成。

下面是手动实现选择算法的示例:

/**
* Producer,发送顺序消息
*/
public class Producer { public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); String[] tags = new String[]{"TagA", "TagC", "TagD"}; // 订单列表
List<OrderStep> orderList = new Producer().buildOrders(); Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(date);
for (int i = 0; i < 10; i++) {
// 加个时间前缀
String body = dateStr + " Hello RocketMQ " + orderList.get(i);
Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes()); SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long id = (Long) arg; //根据订单id选择发送queue
long index = id % mqs.size();
return mqs.get((int) index);
}
}, orderList.get(i).getOrderId());//订单id System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId(),
body));
} producer.shutdown();
} /**
* 订单的步骤
*/
private static class OrderStep {
private long orderId;
private String desc; public long getOrderId() {
return orderId;
} public void setOrderId(long orderId) {
this.orderId = orderId;
} public String getDesc() {
return desc;
} public void setDesc(String desc) {
this.desc = desc;
} @Override
public String toString() {
return "OrderStep{" +
"orderId=" + orderId +
", desc='" + desc + '\'' +
'}';
}
} /**
* 生成模拟订单数据
*/
private List<OrderStep> buildOrders() {
List<OrderStep> orderList = new ArrayList<OrderStep>(); OrderStep orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("创建");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("创建");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("付款");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("创建");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("付款");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("付款");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("完成");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("推送");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("完成");
orderList.add(orderDemo); orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setDesc("完成");
orderList.add(orderDemo); return orderList;
}
}

二: 消息的存储

RocketMQ中的消息存储在本地文件系统中,这些相关文件默认在当前安装主目录下的store目录中。

  • abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动

    Broker的情况下,发现这个文件是存在的,则说明之前Broker的关闭是非正常关闭。

  • checkpoint:其中存储着commitlog、consumequeue、index文件的最后刷盘时间戳

  • commitlog:存放着commitlog文件(消息就是记录在commitlog文件中的)

  • config:存放着Broker运行期间的一些配置数据

  • consumequeue:存放着consumequeue文件(队列信息就存放在这个目录中)

  • index:其中存放着消息索引文件indexFile

  • lock:运行期间使用到的全局资源锁

1. commitlog文件

介绍:

在很多资料中commitlog目录中的文件简单就称为commitlog文件。但在源码中,该文件被命名为mappedFile

commitlog目录中存放着很多的mappedFile文件,当前Broker中的所有消息都是落盘到这些mappedFile文件中的。mappedFile单个文件大小为1G(小于等于1G),文件名由20位十进制数构成,表示当前文件的第一条消息的起始位移偏移量。(一个字节为一个偏移量)

例如:

第一个文件名一定是200构成的。因为第一个文件的第一条消息的偏移量commitlog offset0 当第一个文件放满时,则会自动生成第二个文件继续存放消息。假设第一个文件大小是 1073741820字节(1G = 1073741824字节),下条消息4个字节无法保存,

则生成第二个文件保存,第二个文件名就是00000000001073741820。

以此类推,第n个文件名应该是前前n-1个文件大小之和。

一个Broker中所有mappedFile文件的commitlog offset在逻辑上是连续的,但是在物理上可能不连续

需要注意的是,一个Broker中仅包含一个commitlog目录,所有的mappedFile文件都是存放在该目录中

的。即无论当前Broker中存放着多少Topic的消息,这些消息都是被顺序写入到了mappedFile文件中

的。也就是说,这些消息在Broker中存放时并没有被按照Topic进行分类存放。

消息单元:

文件名为n(第一条消息的偏移量就为n)的mappedFile 文件内部消息存放结构示意图

如上图所示,每个mappedFile 文件都是由一个一个消息单元构成, 每个消息单元都有其不同的结构

  • MsgLen: 每个消息单元中包含消息总长度
  • physicalOffset: 消息的物理位置
  • Body: 消息体内容
  • BodyLength: 消息体长度
  • Topic: 消息主题Topic
  • TopicLength: Topic长度
  • BornHost: 消息生产者
  • BornTimestamp: 消息发送时间戳
  • QueueId: 消息所在的队列
  • QueueOffset: 消息在Queue中存储的偏移量

等近20余项消息相关属性。

需要注意到,消息单元中是包含Queue相关属性的。所以,我们在后续的学习中,就需要十分留意commitlog与queue间的关系是什么?

2. consumequeue文件

上面说到, 每个broker 的所有消息全都按照顺序写在同一个地方,并且用偏移量标记每个信息的位置,

那么 consumequeue 下的文件,就是用于记录每个Topic下的queue对应每条消息的偏移量地址信息

命名规则:

每个Topic在~/store/consumequeue中创建文件夹,目录名为Topic名称。在该每个Topic目录下,会再为每个该Topic的Queue建立一个目录,目录名为queueId。如下图

每个目录中存放着若干consumequeue文件,consumequeue文件是commitlog的索引文件,可以根据consumequeue定位到具体的消息。

文件格式:

consumequeue文件名也由20位数字构成,表示当前文件的第一个索引条目的起始位移偏移量。与mappedFile文件名不同的是,其后续文件名是固定的。

因为每个consumequeue文件大小是固定不变的。其每个记录单元大小是固定的20字节,如下格式:

每个consumequeue文件可以包含30w个索引条目,每个索引条目包含了三个消息重要属性:

  • 消息在mappedFile文件中的偏移量CommitLog Offset

  • 消息长度、

  • 消息Tag的hashcode值。

这三个属性占20个字节,所以每个文件的大小是固定的30w * 20字节。每个索引条目的起始偏移量,就是该消息在Queue中的 Queue Offset

3. 对文件的读写过程

上面介绍了commitLog 和 consumeQueue文件,下面先看看它们之间的关系

如上图所示, commitlog依次存放了producer发送的五条消息, 而对应的每条消息所属的Queue都记录在其对应的consumequeue文件中,并使用commitlog offset 指向 commitlog,

下面介绍一下每个环节的文件读写流程:

消息写入

一条消息进入到Broker后经历了以下几个过程才最终被持久化。

  1. Broker根据queueId,获取到该消息对应索引条目要在consumequeue目录中的写入偏移量,即QueueOffset

  2. 将queueId、queueOffset等数据,与消息一起封装为消息单元

  3. 将消息单元写入到commitlog

  4. 同时,形成消息索引条目,将消息索引条目分发到相应的consumequeue

消息拉取

当Consumer来拉取消息时会经历以下几个步骤:

  1. Consumer获取到其要消费消息所在Queue的消费偏移量offset,计算出其要消费消息的 消息offset

    消费offset即消费进度,consumer对某个Queue的消费offset,即消费到了该Queue的第几条消息 ,即已经消费的个数

    消息offset = 消费offset + 1

  2. Consumer向Broker发送拉取请求,其中会包含其要拉取消息的Queue、消息offset及消息 Tag。

  3. Broker计算在该consumequeue中的queueOffset。

    queueOffset = 消息offset * 20字节 (每个消息单元20字节)

  4. 从该queueOffset处开始向后查找第一个指定Tag的索引条目。

  5. 解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset

  6. 从对应commitlog文件中根据commitlog offset读取消息单元,并发送给Consumer

性能提升

RocketMQ中,无论是消息本身还是消息索引,都是存储在磁盘上的。其不会影响消息的消费吗?

当然不会。其实RocketMQ的性能在目前的MQ产品中性能是非常高的。因为系统通过一系列相关机制大大 提升了性能。

  1. 首先,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。

  2. 其次,consumequeue中的数据是顺序存放的,使得OS 的PageCache的预读取机制,导致对 consumequeue文件的读取几乎接近于内存读取,即使在有消息堆积情况下也不会影响性能

PageCache机制,页缓存机制,是OS对文件的缓存机制,用于加速对文件的读写操作。一般来 说,程序对文件进行顺序读写的速度几乎接近于内存读写速度,主要原因是由于OS使用 PageCache机制对读写访问操作进行性能优化,将一部分的内存用作PageCache。

写操作:OS会先将数据写入到PageCache中,随后会以异步方式由pdæush(page dirty æush) 内核线程将Cache中的数据刷盘到物理磁盘

读操作:若用户要读取数据,其首先会从PageCache中读取,若没有命中,则OS在从物理磁 盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取。

所以RocketMQ中可能会影响性能的是对commitlog文件的读取。因为对commitlog文件来说,读取消息时 会产生大量的随机访问,而随机访问会严重影响性能。不过,如果选择合适的系统IO调度算法,比如 设置调度算法为Deadline(采用SSD固态硬盘的话),随机读的性能也会有所提升。

4. 与Kafka的简单对比

RocketMQ的很多思想来源于Kafka,其中commitlog与consumequeue就是。

RocketMQ中的commitlog目录与consumequeue的结合就类似于Kafka中的partition分区目录。 mappedFile文件就类似于Kafka中的segment段。

Kafka中的Topic的消息被分割为一个或多个partition。partition是一个物理概念,对应到系统上 就是topic目录下的一个或多个目录。每个partition中包含的文件称为segment,是具体存放消息 的文件。

Kafka中消息存放的目录结构是:topic目录下有partition目录,partition目录下有segment文件

Kafka中没有二级分类标签Tag这个概念

Kafka中无需索引文件。因为生产者是将消息直接写在了partition中的,消费者也是直接从 partition中读取数据的

RocketMQ(2) 消息的生产和存储的更多相关文章

  1. rocketmq总结(消息的顺序、重复、事务、消费模式)

    rocketmq总结(消息的顺序.重复.事务.消费模式) 参考: http://www.cnblogs.com/wxd0108/p/6038543.html https://www.cnblogs.c ...

  2. rocketmq总结(消息的高可用、中间件选型)

    rocketmq总结(消息的高可用.中间件选型) 参考: https://blog.csdn.net/meilong_whpu/article/details/76922456 http://blog ...

  3. RocketMQ的消息是怎么丢失的

    前言 通过之前文章的阅读,有关RocketMQ的底层原理相信小伙伴们已经有了一个比较清晰的认识. 那么接下来王子想跟大家讨论一个话题,如果我们的项目中引入了MQ,势必要面对的一个问题,就是消息丢失问题 ...

  4. RocketMQ 事务消息示例分析

    @ 目录 1 示例模式 2 安装与配置 RocketMQ 3 运行服务 3.1 启动 NameServer 3.2 启动 broker 4 生产者 4.1 事务监听器 4.2 事务消息生产者 5 消费 ...

  5. RocketMQ源码 — 九、 RocketMQ延时消息

    上一节消息重试里面提到了重试的消息可以被延时消费,其实除此之外,用户发送的消息也可以指定延时时间(更准确的说是延时等级),然后在指定延时时间之后投递消息,然后被consumer消费.阿里云的ons还支 ...

  6. 聊一聊顺序消息(RocketMQ顺序消息的实现机制)

    当我们说顺序时,我们在说什么? 日常思维中,顺序大部分情况会和时间关联起来,即时间的先后表示事件的顺序关系. 比如事件A发生在下午3点一刻,而事件B发生在下午4点,那么我们认为事件A发生在事件B之前, ...

  7. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  8. RocketMQ事务消息实现分析

    这周RocketMQ发布了4.3.0版本,New Feature中最受关注的一点就是支持了事务消息: 今天花了点时间看了下具体的实现内容,下面是简单的总结. RocketMQ事务消息概要 通过冯嘉发布 ...

  9. RocketMQ之消息幂等

    幂等(idempotent.idempotence)是一个数学与计算机学概念,常见于抽象代数中. 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同. 首先我们了解一下什么是 ...

  10. RocketMQ 事务消息

    RocketMQ 事务消息在实现上充分利用了 RocketMQ 本身机制,在实现零依赖的基础上,同样实现了高性能.可扩展.全异步等一系列特性. 在具体实现上,RocketMQ 通过使用 Half To ...

随机推荐

  1. Vue中is属性的用法 可以动态切换组件

    is 是组件的一个属性,用来展示组件的名称 is和component联用哈 vue提供了component来展示对应的组件名称 compont是一个占位符,is这个属性,用来展示对应的组件名称 三个子 ...

  2. 使用svn.externals(外链)提升美术多个svn目录的svn up速度

    svn up多个目录耗时大 svn上的美术资源项目,在打包机上对一个很久没有变化的目录进行svn up也是需要消耗不少时间的,特别打包时需要对多个目录进行svn up,比如空跑54个目录的svn up ...

  3. Windows10磁盘占用100%和内存占用高

    前言 公司配备了两台电脑,两台电脑都是安装的win10系统,一台是磁盘占用高,另一台是内存可用低. 具体情况如下: 一台外网机 8g内存,安装win10 专业版,开机一天后经常出现内存不够用,但其实都 ...

  4. Python库【数据处理、机器学习、大数据、文件处理等14个类的所有python库整理】

    吐血整理一个月--终于把所有Python库整理齐了....._小熊猫爱恰饭的博客-CSDN博客 参考链接: 一.数据处理 #python学习资料群:660193417 ##3 Chardet # 字符 ...

  5. tensorflow语法【shape、tf.trainable_variables()、Optimizer.minimize()】

    相关文章: [一]tensorflow安装.常用python镜像源.tensorflow 深度学习强化学习教学 [二]tensorflow调试报错.tensorflow 深度学习强化学习教学 [三]t ...

  6. C/C++ 实现通过FTP上传下载

    实现FTP文件下载: #include <stdio.h> #include <Windows.h> #include <WinInet.h> #pragma co ...

  7. Bootstrap Table 动态修改行的颜色

    Bootstrap Table 官网地址 https://bootstrap-table.com/百度搜了大量资料 还是找不 动态改变行的颜色,一般搜索到的都是 初始化的时候  使用 rowStyle ...

  8. P2572 [SCOI2010] 序列操作 题解

    题解:序列操作 比较综合的 ds 题,综合了线段树常见的几种操作:维护最大子段和.区间翻转.区间求和.区间覆盖 . 维护子段和常见的我们维护三类东西: 前缀最长连续段.后缀最长连续段.当前区间上的最大 ...

  9. 教你用JavaScript实现搜索展开

    欢迎来的我的小院,恭喜你今天又要涨知识了! 案例内容 利用JavaScript实现搜索框的移动展开. 演示 学习 <!DOCTYPE html> <html lang="e ...

  10. Vue3学习(16) - 左侧显示分类菜单

    写在前面 和大家不太一样,我觉得今年的自己更加relax,没有亲戚要走,没有朋友相聚,也没有很好的哥们要去叙旧,更没有无知的相亲,甚至可以这么说没有那些闲得慌的邻居. 也可以说是从今天开始,算是可以进 ...