RocketMQ学习笔记

rocketMQ

一.简介及使用

1.1 简介

1.1.1 使用场景

  • 应用解耦

    系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。

  • 流量削峰

    应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。

    举例:业务系统正常时段的QPS如果是1000,流量最高峰是10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰

  • 数据分发

    通过消息队列可以让数据在多个系统之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可

1.1.2 RocketMQ的角色介绍

  • Broker

    暂存和传输消息;部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载,其他的slave不能参与读负载。

  • NameServer

    管理Broker;是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。

  • Topic

    区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息

  • Message Queue

    相当于是Topic的分区;用于并行发送和接收消息

  • Producer

    消息的发送者;与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。

  • Consumer

    消息接收者;与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

RocketMQ启动执行流程

  • 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。

  • Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。

  • 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。

  • Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。

  • C onsumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息

1.1.3 特性

  • 订阅与发布

    消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息。

  • 消息顺序

    消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ可以严格的保证消息有序

  • 消息过滤

    RocketMQ的消费者可以根据Tag进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker端实现的,优点是减少了对于Consumer无用消息的网络传输,缺点是增加了Broker的负担、而且实现相对复杂。

  • 消息可靠性

    RocketMQ支持消息的高可靠,影响消息可靠性的几种情况

    • 1.Broker非正常关闭
    • 2.Broker异常Crash
    • 3.OS Crash
    • 4.机器掉电,但是能立即恢复供电情况
    • 5.机器无法开机(可能是cpu、主板、内存等关键设备损坏)
    • 6.磁盘设备损坏

    1、2、3、4 四种情况都属于硬件资源可立即恢复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。

    5、6属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。

    RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。

    通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合。

  • 至少一次

    至少一次(At least Once)指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性。

  • 回溯消费

    回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。

  • 事务消息

    RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。

    RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致性。

  • 定时消息

    定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。

    broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。

    messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:

    msg.setDelayLevel(level)。level有以下三种情况:

    • level == 0,消息为非延迟消息

    • 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s

    • level > maxLevel,则level== maxLevel,例如level==20,延迟2h

    定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。

    需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高

  • 消息重试

    Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以认为有以下几种情况:

    • 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过10秒后再重试。
    • 由于依赖的下游应用服务不可用,例如db连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s,再消费下一条消息,这样可以减轻Broker重试消息的压力。
  • 消息重投

    生产者在发送消息时:

    • 同步消息失败会重投
    • 异步消息有重试
    • oneway没有任何保证

    消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。

    如下方法可以设置消息重试策略

    • retryTimesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,尝试向其他broker发送,最大程度保证消息不丢失。超过重投次数,抛异常,由客户端保证消息不丢失。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。

    • retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。

    • retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。

  • 流量控制

    生产者流控,因为broker处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。

    • 生产者流控:

      • commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,发生流控。

      • 如果开启transientStorePoolEnable = true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,发生流控。

      • broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,发生流控。

      • broker通过拒绝send 请求方式实现流量控制。

    • 消费者流控:

      • 消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。
      • 消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB。
      • 消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。
      • 消费者流控的结果是降低拉取频率。

    注意,生产者流控,不会尝试消息重投

  • 死信队列

    死信队列用于处理无法被正常消费的消息。

    当一条消息初次消费失败,消息队列会自动进行消息重试;

    达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。

    RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),

    将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

    在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。

1.1.4 消费模式Push or Pull

RocketMQ消息订阅有两种模式,一种是Push模式(MQPushConsumer),即MQServer主动向消费端推送;另外一种是Pull模式(MQPullConsumer),即消费端在需要时,主动到MQ Server拉取。但在具体实现时,Push和Pull模式本质都是采用消费端主动拉取的方式,即consumer轮询从broker拉取消息。

  • Push模式

    好处就是实时性高。不好处在于消费端的处理能力有限,当瞬间推送很多消息给消费端时,容易造成消费端的消息积压,严重时会压垮客户端

  • Pull模式

    好处就是主动权掌握在消费端自己手中,根据自己的处理能力量力而行。缺点就是如何控制Pull的频率。定时间隔太久担心影响时效性,间隔太短担心做太多“无用功”浪费资源。比较折中的办法就是长轮询

  • Push模式与Pull模式的区别

    Push方式里,consumer把长轮询的动作封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。

    Pull方式里,取消息的过程需要用户自己主动调用,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。

    RocketMQ使用长轮询机制来模拟Push效果,算是兼顾了二者的优点。

1.1.5 角色及相关术语

  • 消息模型(Message Model)

    RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker负责存储消息。Broker 在实际部署过程中对应一台服务器,每个Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。

  • Producer

    消息生产者,负责产生消息,一般由业务系统负责产生消息。

  • Consumer

    消息消费者,负责消费消息,一般是后台系统负责异步消费

    • PushConsumer

      Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端。应用通常向Consumer对象注册一个Listener接口,一旦收到消息,Consumer对象立刻回调Listener接口方法。该消费模式一般实时性较高。

    • PullConsumer

      Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。

  • ProducerGroup

    同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。

  • ConsumerGroup

    同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)

  • Broker

    消息中转角色,负责存储消息,转发消息,一般也称为 Server。在 JMS 规范中称为Provider

  • 广播消费

    一条消息被多个 Consumer 消费,即使这些 Consumer 属于同一个 Consumer Group,消息也会被 Consumer Group 中的每个 Consumer 都消费一次,广播消费中的 Consumer Group 概念可以认为在消息划分方面无意义。

    在 CORBA Notification 规范中,消费方式都属于广播消费。

    在 JMS 规范中,相当于 JMS Topic( publish/subscribe )模型

  • 集群消费

    一个 Consumer Group 中的 Consumer 实例平均分摊消费消息。例如某个 Topic 有 9 条消息,其中一个 Consumer Group 有 3 个实例(可能是 3 个进程,或者3台机器),那每个实例只消费其中的 3条消息。一个消费队列只能对应一个消费者,一个消费者可以消费多个消费队列,一个消费者可以有多个消费线程

  • 顺序消息

    消费消息的顺序要同发送消息的顺序一致,在RocketMQ 中主要指的是局部顺序,即一类消息为满足顺序性,必须Producer单线程顺序发送,且发送到同一个队列,这样Consumer 就可以按照Producer发送的顺序去消费消息

    • 普通顺序消息

      顺序消息的一种,正常情况下可以保证完全的顺序消息,但是一旦发生通信异常,Broker 重启,由于队列总数发生发化,哈希取模后定位的队列会发化,产生短暂的消息顺序不一致;比如,刚开始有3个Broker,一个Broker有两个写队列,一共是6个写队列,正常情况下某个生产者哈希之后值是6 那取模后就是0,但是当某一个Broker不可用时,队列总数变成了4,6%4就变成了2,生产者选择的队列就发生了变化

      如果业务能容忍在集群异常情况(如某个Broker 宕机或者重启)下,消息短暂的乱序,使用普通顺序方式比较合适

    • 严格顺序消息

      顺序消息的一种,无论正常异常情况都能保证顺序,但是牺牲了分布式 Failover特性,即Broker集群中只要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。 如果服务器部署为同步双写模式,此缺陷可通过备机自动切换为主避免,不过仍然会存在几分钟的服务不可用。(依赖同步双写,主备自动切换,自动切换功能目前还未实现)

      目前已知的应用只有数据库 binlog 同步强依赖严格顺序消息,其他应用绝大部分都可以容忍短暂乱序,推荐使用普通的顺序消息

  • Message Queue

    在 RocketMQ 中,所有消息队列都是持久化的,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用Offset来访问,offset 为 java long 类型,64 位,理论上在 100 年内不会溢出,所以认为为是长度无限,另外队列中只保存最近几天的数据,之前的数据会按照过期时间来删除。也可以认为Message Queue是一个长度无限的数组,offset 就是下标。

  • 标签(Tag)

    为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

1.2 使用

1.2.1 API使用

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.5.1</version>
</dependency>
public class Constant {
    public static final String NAME_ADDR="my_name_addr";
    public static final String PRODUCER_GROUP_01="my_producer_group";
    public static final String TOPIC_01="my_topic";
    public static final String CONSUMER_GROUP_01="my_consumer_group";
    public static final String CONSUMER_GROUP_02="my_consumer_group";
}
1.2.1.1 发送消息
public class MyProducer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer(Constant.PRODUCER_GROUP_01);
        producer.setNamesrvAddr(Constant.NAME_ADDR);
        producer.start();
        for (int i=0;i<100;i++){
            Message message = new Message(Constant.TOPIC_01,("这个是一条测试消息_" + LocalDateTime.now()+"_"+i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult result = producer.send(message);
            System.out.println(result);
        }

        producer.shutdown();
    }
}
1.2.1.2 拉消息
public class MyPullConsumer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(Constant.CONSUMER_GROUP_01);
        consumer.setNamesrvAddr(Constant.NAME_ADDR);
        consumer.start();
        Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues(Constant.TOPIC_01);
        for (MessageQueue messageQueue : messageQueues) {
            System.out.println("messageQueue:" + messageQueue);
            PullResult result = consumer.pull(messageQueue, "*", 0, 10);
            List<MessageExt> msgFoundList = result.getMsgFoundList();
            if (msgFoundList == null) continue;
            for (MessageExt messageExt : msgFoundList) {
                System.out.println(messageQueue.getQueueId() + "的消息:" + new String(messageExt.getBody(), "utf-8"));
            }
        }

        consumer.shutdown();
    }
}
1.2.1.3 推消息
public class MyPushConsumer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(null, Constant.CONSUMER_GROUP_02);
				// 指定nameserver的地址
        consumer.setNamesrvAddr(Constant.NAME_ADDR);
      	//订阅这个主题的全部消息
        consumer.subscribe(Constant.TOPIC_01, "*");
        consumer.setConsumeThreadMin(2);
        consumer.setConsumeThreadMax(2);
        consumer.setPullBatchSize(1);
        consumer.setConsumeMessageBatchMaxSize(1);
        AtomicBoolean atomicBoolean = new AtomicBoolean(true);
        consumer.registerMessageListener(
                new MessageListenerConcurrently() {
                    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                        MessageQueue messageQueue = context.getMessageQueue();
                        for (MessageExt msg : msgs) {
                            try {
                                String s = new String(msg.getBody(), "utf-8");
                                System.out.println(Thread.currentThread().getName() + "_" + messageQueue + "的消息:" + s);


                            } catch (UnsupportedEncodingException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;


                    }
                });
        consumer.start();
    }
}

1.2.2 spring boot中使用

<properties>
    <rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot-starter-version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>${rocketmq-spring-boot-starter-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
spring.application.name=springboot-rocketmq
rocketmq.name-server=my_name_addr
rocketmq.producer.group=my_producer_group
1.2.2.1 发送消息
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootDemo02ApplicationTests {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Test
    public void test() {
        rocketMQTemplate.convertAndSend(Constant.TOPIC_01, "这个是springboot的消息");
    }

}
1.2.2.2 推消费
@Slf4j
@Component
@RocketMQMessageListener(topic = Constant.TOPIC_01, consumerGroup = Constant.CONSUMER_GROUP_02)
public class ConsumerTest implements RocketMQListener<String> {

    @Override
    public void onMessage(String s) {
        log.info("这个是RocketMQ的消息:{}", s);
    }
}

二.高级特性及原理

2.1 发送消息

生产者向消息队列里写入消息,不同的业务场景需要生产者采用不同的写入策略。比如同步发送、异步发送、Oneway发送、延迟发送、发送事务消息等。 默认使用的是DefaultMQProducer类,发送消息要经过五个步骤:

  1. 设置Producer的GroupName。
  2. 设置InstanceName,当一个Jvm需要启动多个Producer的时候,通过设置不同的InstanceName来区分,不设置的话系统使用默认名称“DEFAULT”。
  3. 设置发送失败重试次数,当网络出现异常的时候,这个次数影响消息的重复投递次数。想保证不丢消息,可以设置多重试几次。
  4. 设置NameServer地址
  5. 组装消息并发送。

消息发生返回状态(SendResult#SendStatus)有四种,不同状态在不同的刷盘策略和同步策略的配置下含义是不同的:

  • FLUSH_DISK_TIMEOUT:表示没有在规定时间内完成刷盘(需要Broker的刷盘策略被设置成SYNC_FLUSH才会报这个错误)。

  • FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker被设置成SYNC_MASTER方式,没有在设定时间内完成主从同步。

  • SLAVE_NOT_AVAILABLE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示在主备方式下,并且Broker被设置成SYNC_MASTER,但是没有找到被配置成Slave的Broker。

  • SEND_OK:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被同步到了Slave上?消息在Slave上是否被写入磁盘?需要结合所配置的刷盘策略、主从策略来定。这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是SEND_OK。

发送一条消息出去要经过三步

  1. 客户端发送请求到服务器。
  2. 服务器处理该请求。
  3. 服务器向客户端返回应答

一次消息的发送耗时是上述三个步骤的总和。

在一些对速度要求高,但是可靠性要求不高的场景下,比如日志收集类应用, 可以采用Oneway方式发送,Oneway方式只发送请求不等待应答,即将数据写入客户端的Socket缓冲区就返回,不等待对方返回结果。用这种方式发送消息的耗时可以缩短到微秒级

另一种提高发送速度的方法是增加Producer的并发量,使用多个Producer同时发送,不用担心多Producer同时写会降低消息写磁盘的效率,RocketMQ引入了一个并发窗口,在窗口内消息可以并发地写入DirectMem中,然后异步地将连续一段无空洞的数据刷入文件系统当中。

顺序写CommitLog可让RocketMQ无论在HDD还是SSD磁盘情况下都能保持较高的写入性能

目前在阿里内部经过调优的服务器上,写入性能达到90万+的TPS,在Linux操作系统层级进行调优,推荐使用EXT4文件系统,IO调度算法使用deadline算法。

2.2 消费消息

简单总结消费的几个要点:

  • 消息消费方式(Pull和Push)
  • 消息消费的模式(广播模式和集群模式)
  • 流量控制(可以结合sentinel来实现)
  • 并发线程数设置
  • 消息的过滤(Tag、Key) TagA||TagB||TagC null :

当Consumer的处理速度跟不上消息的产生速度,会造成越来越多的消息积压,这个时候首先查看消费逻辑本身有没有优化空间,除此之外还有三种方法可以提高Consumer的处理能力。

  1. 提高消费并行度

    在同一个ConsumerGroup下(Clustering方式),可以通过增加Consumer实例的数量来提高并行度。

    通过加机器,或者在已有机器中启动多个Consumer进程都可以增加Consumer实例数。

    注意:总的Consumer数量不要超过Topic下Read Queue数量,超过的Consumer实例接收不到消息

    此外,通过提高单个Consumer实例中的并行处理的线程数,可以在同一个Consumer内增加并行度来提高吞吐量(设置方法是修改consumeThreadMin和consumeThreadMax)。

  2. 以批量方式进行消费

    某些业务场景下,多条消息同时处理的时间会大大小于逐个处理的时间总和,比如消费消息中涉及update某个数据库,一次update10条的时间会大大小于十次update1条数据的时间。

    可以通过批量方式消费来提高消费的吞吐量。实现方法是设置Consumer的consumeMessageBatchMaxSize这个参数,默认是1,如果设置为N,在消息多的时候每次收到的是个长度为N的消息链表。

  3. 检测延时情况,跳过非重要消息

    Consumer在消费的过程中,如果发现由于某种原因发生严重的消息堆积,短时间无法消除堆积,这个时候可以选择丢弃不重要的消息,使Consumer尽快追上Producer的进度。

2.3 消息存储

目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题

目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍!RocketMQ的消息用顺序写,保证了消息存储的速度

RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • CommitLog

消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;

  • ConsumeQueue

消息消费队列,引入的目的主要是提高消息消费的性能,RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行,如果要遍历commitlog文件根据topic检索消息是非常低效。

Consumer即可根据ConsumeQueue来查找待消费的消息。其中ConsumeQueue(逻辑消费队列)作为消费消息的索引保存了以下数据

  • 保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset
  • 消息大小size
  • 消息Tag的HashCode值。

consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式是topic/queue/file三层组织结构

具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。

consumequeue文件采取定长设计,每个条目共20个字节,分别为:

  • 8字节的commitlog物理偏移量

  • 4字节的消息长度

  • 8字节tag hashcode

单个文件由30W个条目组成,可以像数组一样随机访问每一个条目

每个consumequeue文件大小约5.72M

  • IndexFile

    IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。

    • Index文件的存储位置是:$HOME/store/index/{fileName}。

    • 文件名fileName是以创建时的时间戳命名的

    • 固定的单个IndexFile文件大小约为400M

    • 一个IndexFile可以保存 2000W个索引

    • IndexFile的底层存储设计为在文件系统中实现HashMap结构故rocketmq的索引文件其底层实现为hash索引

2.4 消息过滤

RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。

RocketMQ这么做是在于其Producer端写入消息和Consumer端订阅消息采用分离存储的机制来实现的,Consumer端订阅消息是需要通过ConsumeQueue这个消息消费的逻辑队列拿到一个索引,然后再从CommitLog里面读取真正的消息实体内容,所以说到底也是还绕不开其存储结构。

其ConsumeQueue的存储结构如下,可以看到其中有8个字节存储的Message Tag的哈希值,基于Tag的消息过滤正式基于这个字段值的。

主要支持如下3种的过滤方式

  • Tag过滤方式

    Consumer端在订阅消息时除了指定Topic还可以指定TAG,如果一个消息有多个TAG,可以用||分隔。

    • Consumer端会将这个订阅请求构建成一个 SubscriptionData,发送一个Pull消息的请求给Broker端。
    • Broker端从RocketMQ的文件存储层—Store读取数据之前,会用这些数据先构建一个MessageFilter,然后传给Store。
    • Store从 ConsumeQueue读取到一条记录后,会用它记录的消息tag hash值去做过滤。
    • 在服务端只是根据hashcode进行判断,无法精确对tag原始字符串进行过滤,在消息消费端拉取到消息后,还需要对消息的原始tag字符串进行比对,如果不同,则丢弃该消息,不进行消息消费。
  • SQL92的过滤方式

    仅对push的消费者起作用。

    Tag方式虽然效率高,但是支持的过滤逻辑比较简单。SQL表达式可以更加灵活的支持复杂过滤逻辑,这种方式的大致做法和上面的Tag过滤方式一样,只是在Store层的具体过滤过程不太一样,真正的 SQL expression 的构建和执行由rocketmq-filter模块负责的。

    每次过滤都去执行SQL表达式会影响效率,所以RocketMQ使用了BloomFilter避免了每次都去执行。

    SQL92的表达式上下文为消息的属性。

    如果要使用 SQL92 过滤,需要设置 Broker 的配置项 enablePropertyFilter=true,这个配置默认为 false

    enablePropertyFilter=true
    

    如果要开启布隆过滤器进行双层过滤,需要设置如下配置。

    enableCalcFilterBitMap=true        # 设置在构造消费队列时,用布隆过滤器计算匹配过滤条件的消费组,构造成二进制数组
    enableConsumeQueueExt=true        # 启用消费队列扩展存储,二进制数组会存到扩展存储中
    

    然后重启生效

    SQL92 的过滤语法规则如下:

    语法说明示例
    IS NULL判断属性不存在。a IS NULL :属性a不存在。
    IS NOT NULL判断属性存在。a IS NOT NULL:属性a存在。
    > >= < <=用于比较数字,不能用于比较字符串,否则消费者客户端启动时会报错。 说明 可转化为数字的字符串也被认为是数字。a IS NOT NULL AND a > 100:属性a存在且属性a的值大于100。 a IS NOT NULL AND a > 'abc':错误示例,abc为字符串,不能用于比较大小。
    BETWEEN xxx AND xxx用于比较数字,不能用于比较字符串,否则消费者客户端启动时会报错。等价于>= xxx AND <= xxx。表示属性值在两个数字之间。a IS NOT NULL AND (a BETWEEN 10 AND 100):属性a存在且属性a的值大于等于10且小于等于100。
    NOT BETWEEN xxx AND xxx用于比较数字,不能用于比较字符串,否则消费者客户端启动会报错。等价于< xxx OR > xxx,表示属性值在两个值的区间之外。a IS NOT NULL AND (a NOT BETWEEN 10 AND 100):属性a存在且属性a的值小于10或大于100。
    IN (xxx, xxx)表示属性的值在某个集合内。集合的元素只能是字符串。a IS NOT NULL AND (a IN ('abc', 'def')):属性a存在且属性a的值为abc或def。
    = <>等于和不等于。可用于比较数字和字符串。a IS NOT NULL AND (a = 'abc' OR a<>'def'):属性a存在且属性a的值为abc或a的值不为def。
    AND OR逻辑与、逻辑或。可用于组合任意简单的逻辑判断,需要将每个逻辑判断内容放入括号内。a IS NOT NULL AND (a > 100) OR (b IS NULL):属性a存在且属性a的值大于100或属性b不存在。
  • Filter Server

    这是一种比SQL表达式更灵活的过滤方式,允许用户自定义Java函数,根据Java函数的逻辑对消息进行过滤。

    要使用Filter Server,首先要在启动Broker前在配置文件里加上 filterServer-Nums=3 这样的配置,Broker在启动的时候,就会在本机启动3个Filter Server进程。Filter Server类似一个RocketMQ的Consumer进程,它从本机Broker获取消息,然后根据用户上传过来的Java函数进行过滤,过滤后的消息再传给远端的Consumer。

    这种方式会占用很多Broker机器的CPU资源,要根据实际情况谨慎使用。上传的java代码也要经过检查,不能有申请大内存、创建线程等这样的操作,否则容易造成Broker服务器宕机

    
    consumer.subscribe("SqlFilterTest",
        MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
            "and (a is not null and a between 0 and 3)"));
    

2.5 零拷贝相关概念解析

2.5.1 pageCache

  • 由内存中的物理page组成,其内容对应磁盘上的block。
  • page cache的大小是动态变化的。
  • backing store: cache缓存的存储设备
  • 一个page通常包含多个block, 而block不一定是连续的。

读Cache

  • 当内核发起一个读请求时, 先会检查请求的数据是否缓存到了page cache中。

    • 如果有,那么直接从内存中读取,不需要访问磁盘, 此即 cache hit(缓存命中)
    • 如果没有, 就必须从磁盘中读取数据, 然后内核将读取的数据再缓存到cache中, 如此后续的读请求就可以命中缓存了。
  • page可以只缓存一个文件的部分内容, 而不需要把整个文件都缓存进来。

写Cache

  • 当内核发起一个写请求时, 也是直接往cache中写入, 后备存储中的内容不会直接更新。

  • 内核会将被写入的page标记为dirty, 并将其加入到dirty list中。

  • 内核会周期性地将dirty list中的page写回到磁盘上, 从而使磁盘上的数据和内存中缓存的数据一致。

cache回收

  • Page cache的另一个重要工作是释放page, 从而释放内存空间。

  • cache回收的任务是选择合适的page释放

  • 如果page是dirty的, 需要将page写回到磁盘中再释放。

2.5.2 cache和buffer的区别

  • Cache

    缓存区,是高速缓存,是位于CPU和主内存之间的容量较小但速度很快的存储器,因为CPU的速度远远高于主内存的速度,CPU从内存中读取数据需等待很长的时间,而 Cache保存着CPU刚用过的数据或循环使用的部分数据,这时从Cache中读取数据会更快,减少了CPU等待的时间,提高了系统的性能。

    Cache并不是缓存文件的,而是缓存块的(块是I/O读写最小的单元);Cache一般会用在I/O请求上,如果多个进程要访问某个文件,可以把此文件读入Cache中,这样下一个进程获取CPU控制权并访问此文件直接从Cache读取,提高系统性能。

    • 作用

      缓存用于存储已经计算过或获取过的数据,以便在后续访问时能够更快地获取数据,从而提高系统的响应速度。

    • 工作原理

      缓存会将经常访问的数据复制到更快的存储介质中,如内存,以便在后续访问时无需再从原始数据源获取。这样能够减少数据访问时间,提高性能。

    • 数据处理

      缓存中的数据通常可以根据需要进行处理,以满足特定的访问要求。例如,可以将数据库查询结果存储在缓存中,以减少数据库访问频率

  • Buffer

    缓冲区,用于存储速度不同步的设备或优先级不同的设备之间传输数据;通过buffer可以减少进程间通信需要等待的时间,当存储速度快的设备与存储速度慢的设备进行通信时,存储慢的数据先把数据存放到buffer,达到一定程度存储快的设备再读取buffer的数据,在此期间存储快的设备CPU可以干其他的事情。

    • 作用

      缓冲通常用于临时存储数据,以平衡不同速度的数据传输过程之间的差异。它可以用来解决数据传输速度不匹配的问题。

    • 工作原理

      缓冲区是一个存储区域,用于暂时保存数据,待数据传输速度对齐后再将数据发送出去。在数据传输过程中,如果数据接收速度较快,数据会被存储在缓冲区中,直到接收方准备好接收为止。

    • 数据处理

      缓冲区通常不会对数据进行处理或修改,它只是临时存储数据的容器

2.5.3 HeapByteBuffer和DirectByteBuffer

  • HeapByteBuffer

    是在jvm堆上面一个buffer,底层的本质是一个数组,用类封装维护了很多的索引(limit/position/capacity等)。

    优点:内容维护在jvm里,把内容写进buffer里速度快;更容易回收

  • DirectByteBuffer

    底层的数据是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向数据,进而操作数据。

    跟外设(IO设备)打交道时会快很多,因为外设读取jvm堆里的数据时,不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的,如果使用DirectByteBuffer,则可以省去这一步,实现zero copy(零拷贝)

外设之所以要把jvm堆里的数据copy出来再操作,不是因为操作系统不能直接操作jvm内存,而是因为jvm在进行gc(垃圾回收)时,会对数据进行移动,一旦出现这种问题,外设就会出现数据错乱的情况。

截屏2023-11-15 11.44.07

所有的通过allocate方法创建的buffer都是HeapByteBuffer.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

堆外内存实现零拷贝

  1. 前者分配在JVM堆上(ByteBuffer.allocate()),后者分配在操作系统物理内存上(ByteBuffer.allocateDirect(),JVM使用C库中的malloc()方法分配堆外内存);
  2. DirectByteBuffer可以减少JVM GC压力,当然,堆中依然保存对象引用,fullgc发生时也会回收直接内存,也可以通过system.gc主动通知JVM回收,或者通过 cleaner.clean主动清理。Cleaner.create()方法需要传入一个DirectByteBuffer对象和一个Deallocator(一个堆外内存回收线程)。GC发生时发现堆中的DirectByteBuffer对象没有强引用了,则调用Deallocator的run()方法回收直接内存,并释放堆中DirectByteBuffer的对象引用;
  3. 底层I/O操作需要连续的内存(JVM堆内存容易发生GC和对象移动),所以在执行write操作时需要将HeapByteBuffer数据拷贝到一个临时的(操作系统用户态)内存空间中,会多一次额外拷贝。而DirectByteBuffer则可以省去这个拷贝动作,这是Java层面的 “零拷贝” 技术,在netty中广泛使用;
  4. MappedByteBuffer底层使用了操作系统的mmap机制,FileChannel#map()方法就会返回MappedByteBuffer。DirectByteBuffer虽然实现了MappedByteBuffer,不过DirectByteBuffer默认并没有直接使用mmap机制。

2.5.4 缓存IO和直接IO

  • 缓存IO

    缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间

    • 读操作

      操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。

    • 写操作

      将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。

    缓存I/O的优点:

    • 在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;

    • 可以减少读盘的次数,从而提高性能

    缓存I/O的缺点:

    • 在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输。数据在传输过程中就需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
  • 直接IO

    直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。

    直接IO的缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓慢。通常直接IO与异步IO结合使用,会得到比较好的性能。

    下图是写场景下的DirectIO和BufferIO:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.5.5 内存映射文件(Mmap)

在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

映射关系可以分为两种

  1. 文件映射 磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。
  2. 匿名映射 初始化全为0的内存空间。

而对于映射关系是否共享又分为

  1. 私有映射(MAP_PRIVATE) 多进程间数据共享,第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进程看不见,不影响数据源,修改不反应到磁盘实际文件,是一个copy-on-write(写时复制)的映射方式。
  2. 共享映射(MAP_SHARED) 多进程间数据共享,修改反应到磁盘实际文件中。

因此总结起来有4种组合

  1. 私有文件映射

    多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中

  2. 私有匿名映射

    mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存(malloc分配大内存会调用mmap)。 例如开辟新进程时,会为每个进程分配虚拟的地址空间,这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的时候会copy-on-write。

  3. 共享文件映射

    多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件的修改会反应到实际物理文件中,他也是进程间通信(IPC)的一种机制。

  4. 共享匿名映射

    这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC).

mmap只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存。

在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多。

2.5.6 直接内存读取并发送文件的过程

截屏2023-11-21 13.48.48

2.5.7 Mmap读取并发送文件的过程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.5.8 Sendfile零拷贝读取并发送文件的过程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

零拷贝(zero copy)小结

  1. 虽然叫零拷贝,实际上sendfile有2次数据拷贝的。第1次是从磁盘拷贝到内核缓冲区,第二次是从内核缓冲区拷贝到网卡(协议引擎)。如果网卡支持 SG-DMA(The Scatter-GatherDirect Memory Access)技术,就无需从PageCache拷贝至 Socket 缓冲区;
  2. 之所以叫零拷贝,是从内存角度来看的,数据在内存中没有发生过拷贝,只是在内存和I/O设备之间传输。很多时候我们认为sendfile才是零拷贝,mmap严格来说不算;
  3. Linux中的API为sendfile、mmap,Java中的API为FileChanel.transferTo()、FileChannel.map()等;
  4. Netty、Kafka(sendfile)、Rocketmq(mmap)、Nginx等高性能中间件中,都有大量利用操作系统零拷贝特性。

2.6 同步复制和异步复制

如果一个Broker组有Master和Slave,消息需要从Master复制到Slave 上,有同步和异步两种复制方式

同步复制

同步复制方式是等Master和Slave均写成功后才反馈给客户端写成功状态;

在同步复制方式下,如果Master出故障,Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。

异步复制

异步复制方式是只要Master写成功即可反馈给客户端写成功状态。

在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写 入Slave,有可能会丢失;

总结

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式, 尤其是SYNC_FLUSH方式,由于频繁地触发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Save配置成ASYNC_FLUSH的刷盘方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出故障,仍然能保证数据不丢,是个不错的选择。

2.7 高可用机制

RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。

Master和Slave的区别:

  1. 在Broker的配置文件中,参数brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是Slave,
  2. brokerRole参数也说明这个Broker是Master还是Slave。(SYNC_MASTER/ASYNC_MASTER/SALVE)
  3. Master角色的Broker支持读和写,Slave角色中brokerId=1的Broker仅支持读,其他的Slave不能支持读写
  4. Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息

截屏2023-11-21 15.00.50

消息消费高可用

在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave 读。

有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。这就达到了消费端的高可用性。

消息发送高可用

在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同Broker名称,不同brokerId的机器组成一个Broker组),这样既可以在性能方面具有扩展性,也可以降低主节点故障对整体上带来的影响,而且当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息的。

RocketMQ目前还不支持把Slave自动转成Master,如果机器资源不足,需要把Slave转成Master。

  1. 手动停止Slave角色的Broker。
  2. 更改配置文件。
  3. 用新的配置文件启动Broker。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这种早期方式在大多数场景下都可以很好的工作,但也面临一些问题。

比如,在需要保证消息严格顺序的场景下,由于在主题层面无法保证严格顺序,所以必须指定队列来发送消息,对于任何一个队列,它一定是落在一组特定的主从节点上,如果这个主节点宕机,其他的主节点是无法替代这个主节点的,否则就无法保证严格顺序。

在这种复制模式下,严格顺序和高可用只能选择一个

RocketMQ 2018 年底迎来了一次重大的更新,引入 Dledger,增加了一种全新的复制方式。

RocketMQ 引入 Dledger,使用新的复制方式,可以很好地解决这个问题。

Dledger 在写入消息的时候,要求至少消息复制到半数以上的节点之后,才给客户端返回写入成功,并且它是支持通过选举来动态切换主节点的。

举例:

假如有3个节点,当主节点宕机的时候,2 个从节点会通过投票选出一个新的主节点来继续提供服务,相比主从的复制模式,解决了可用性的问题。

由于消息要至少复制到 2 个节点上才会返回写入成功,即使主节点宕机了,也至少有一个节点上的消息是和主节点一样的。

Dledger在选举时,总会把数据和主节点一样的从节点选为新的主节点,这样就保证了数据的一致性,既不会丢消息,还可以保证严格顺序。

存在问题:

当然,Dledger的复制方式也不是完美的,依然存在一些不足:

  1. 比如,选举过程中不能提供服务。
  2. 最少需要 3 个节点才能保证数据一致性,3 节点时,只能保证 1 个节点宕机时可用,如果 2个节点同时宕机,即使还有 1 个节点存活也无法提供服务,资源的利用率比较低。
  3. 另外,由于至少要复制到半数以上的节点才返回写入成功,性能上也不如主从异步复制的方式快。

2.8 刷盘机制

RocketMQ 的所有消息都是持久化的,先写入系统 PageCache,然后刷盘,可以保证内存与磁盘都有一份数据, 访问时,直接从内存读取。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,分别是同步刷盘和异步刷盘

  • 同步刷盘

    同步刷盘与异步刷盘的唯一区别是异步刷盘写完 PageCache直接返回,而同步刷盘需要等待刷盘完成才返回, 同步刷盘流程如下:

    • 写入 PageCache后,线程等待,通知刷盘线程刷盘。
    • 刷盘线程刷盘后,唤醒前端等待线程,可能是一批线程。
    • 前端等待线程向用户返回成功

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 异步刷盘

    线上的网卡一般都为千兆网卡,写磁盘速度一般是明显快于数据网络入口速度

    • 由于磁盘速度大于网卡速度,那么刷盘的进度肯定可以跟上消息的写入速度。

    • 万一由于此时系统压力过大,可能堆积消息,除了写入 IO,还有读取 IO,万一出现磁盘读取落后情况, 会不会导致系统内存溢出,答案是否定的,原因如下:

      • 写入消息到 PageCache时,如果内存不足,则尝试丢弃干净的 PAGE,腾出内存供新消息使用,策略是LRU 方式。
      • 如果干净页不足,此时写入 PageCache会被阻塞,系统尝试刷盘部分数据,大约每次尝试 32个 PAGE , 来找出更多干净 PAGE。

    综上,内存溢出的情况不会出现。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.9 负载均衡

RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。

2.9.1 product端负载均衡

product在发送消息时,比如有5 个队列,这些队列可以部署在一台机器上,也可以分别部署在 5 台不同的机器上,发送消息通过轮询队列的方式 发送,每个队列接收平均的消息量。通过增加机器,可以水平扩展队列容量。 另外也可以自定义方式选择发往哪个队列。

截屏2023-11-21 16.20.14

2.9.2 consume端负载均衡

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图所示,如果有 5 个队列,2 个consumer,那么第一个Consumer 消费 3 个队列,第二consumer 消费 2 个队列。 这样即可达到平均消费的目的,可以水平扩展 Consumer 来提高消费能力。但是 Consumer 数量要小于等于队列数 量,如果 Consumer 超过队列数量,那么多余的Consumer 将不能消费消息 。

在RocketMQ中,Consumer端的两种消费模式(Push/Pull)底层都是基于拉模式来获取消息的,而在Push模式只是对pull模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又“马不停蹄”的继续向服务器再次尝试拉取消息。

如果未拉取到消息,则延迟一下又继续拉取。

在两种基于拉模式的消费方式(Push/Pull)中,均需要Consumer端在知道从Broker端的哪一个消息队列中去获取消息。

因此,有必要在Consumer端来做负载均衡,即Broker端中多个MessageQueue分配给同一个ConsumerGroup中的哪些Consumer消费。

要做负载均衡,必须知道一些全局信息,也就是一个ConsumerGroup里到底有多少个Consumer。

知道了全局信息,才可以根据某种算法来分配,比如简单地平均分到各个Consumer。

在RocketMQ中,负载均衡或者消息分配是在Consumer端代码中完成的,Consumer从Broker处获得全局信息,然后自己做负载均衡,只处理分给自己的那部分消息。

Pull Consumer可以看到所有的Message Queue,而且从哪个Message Queue读取消息,读消息时的Offset都由使用者控制,使用者可以实现任何特殊方式的负载均衡。

DefaultMQPullConsumer有两个辅助方法可以帮助实现负载均衡,一个是registerMessageQueueListener函数,一个是MQPullConsumerScheduleService(使用这个Class类似使用DefaultMQPushConsumer,但是它把Pull消息的主动性留给了使用者)

DefaultMQPushConsumer的负载均衡过程不需要使用者操心,客户端程序会自动处理,每个DefaultMQPushConsumer启动后,会马上会触发一个doRebalance动作;而且在同一个ConsumerGroup里加入新的DefaultMQPushConsumer时,各个Consumer都会被触发doRebalance动作(Broker会通知ConsumerGroup下的消费者)。

负载均衡的分配粒度只到Message Queue,把Topic下的所有Message Queue分配到不同的Consumer中

负载均衡算法有几种,默认用的是AllocateMessageQueueAveragely

以AllocateMessageQueueAveragely策略为例,如果创建Topic的时候,把Message Queue数设为3,当Consumer数量为2的时候,有一个Consumer需要处理Topic三分之二的消息,另一个处理三分之一的消息;当Consumer数量为4的时候,有一个Consumer无法收到消息,其他3个Consumer各处理Topic三分之一的消息

可见Message Queue数量设置过小不利于做负载均衡,通常情况下,应把一个Topic的MessageQueue数设置为16。

消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列。

2.10 消息消费重试

  • 顺序消息的重试

    对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒)队列中后续的消息不会被消费,这时应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。

    顺序消息使用MessageListenerOrderly监听

  • 无序消息的重试

    对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果,队列中后续的消息可以正常消费。

    无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。

    • 重试次数

      消息队列 RocketMQ 默认允许每条消息最多重试 16 次

      第几次重试与上次重试的间隔时间
      110 秒
      230 秒
      31 分钟
      42 分钟
      53 分钟
      64 分钟
      75 分钟
      86 分钟
      97 分钟
      108 分钟
      119 分钟
      1210 分钟
      1320 分钟
      1430 分钟
      151 小时
      162 小时

      如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。

      注意: 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变

    • 配置方式

      集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):

      • 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER; (推荐)
      • 返回 Null
      • 抛出异常
      public class MyConcurrentlyMessageListener implements
      MessageListenerConcurrently {
      @Override
      public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
      ConsumeConcurrentlyContext context) {
      //处理消息
      doConsumeMessage(msgs);
      //方式1:返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,消息将重试
      return ConsumeConcurrentlyStatus.RECONSUME_LATER;
      //方式2:返回 null,消息将重试
      return null;
      //方式3:直接抛出异常, 消息将重试
      throw new RuntimeException("Consumer Message exceotion");
      //方式4:消息消费成功,不重试
      return ConsumeConcurrentlyStatus.CONSUME_SUCCESS
      }
      }
      

      自定义消息最大重试次数

      消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:

      • 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。

      • 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时

      • 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效

      • 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。

      • 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置

      DefaultMQPushConsumer consumer = new
      DefaultMQPushConsumer("consumer_grp_test");
      // 设置重新消费的次数
      // 共16个级别,大于16的一律按照2小时重试
      consumer.setMaxReconsumeTimes(20);
      

      获取消息重试次数

      消费者收到消息后,可按照如下方式获取消息的重试次数:

      public class MyConcurrentlyMessageListener implements
      MessageListenerConcurrently {
      @Override
      public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
      ConsumeConcurrentlyContext context) {
      for (MessageExt msg : msgs) {
      System.out.println(msg.getReconsumeTimes());
      }
      doConsumeMessage(msgs);
      return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
      }
      }
      

2.11 死信队列

RocketMQ中消息重试超过一定次数后(默认16次)就会被放到死信队列中,在消息队列RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。可以在控制台(rocketmq-console)Topic列表中看到“DLQ”相关的Topic,默认命名是:

  • %RETRY%消费组名称(重试Topic)

  • %DLQ%消费组名称(死信Topic)

死信队列特性

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
  • 死信队列也可以被订阅和消费,并且也会过期

死信消息特性

  • 不会再被消费者正常消费。
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3天内及时处理。

一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。

2.12 延迟消息

定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。可以配置自定义messageDelayLevel。注意,messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。

发送消息时通过设置Message的DelayTimeLevel属性可以来设置消息的延迟登记

level有以下三种情况:

  • level == 0,消息为非延迟消息
  • 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
  • level > maxLevel,则level== maxLevel,例如level==20,延迟2h

定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高

2.13 顺序消息

顺序消息是指消息的消费顺序和产生顺序相同,在有些业务逻辑下,必须保证顺序。比如订单的生成、付款、发货,这3个消息必须按顺序处理才行。

顺序消息分为全局顺序消息和部分顺序消息:

  • 全局顺序消息指某个Topic下的所有消息都要保证顺序;

  • 部分顺序消息只要保证每一组消息被顺序消费即可,比如订单消息,只要保证同一个订单ID的三个消息能按顺序消费即可。

RocketMQ在默认情况下不保证顺序,比如创建一个Topic,默认八个写队列,八个读队列。这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个Consumer,每个Consumer也可能启动多个线程并行处理,所以消息被哪个Consumer消费,被消费的顺序和写入的顺序是否一致是不确定的。

要保证全局顺序消息,需要先把Topic的读写队列数设置为一,然后Producer和Consumer的并发设置也要是一。简单来说,为了保证整个Topic的全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要保证部分消息有序,需要发送端和消费端配合处理。在发送端,要做到把同一业务ID的消息发送到同一个Message Queue;在消费过程中,要做到从同一个Message Queue读取的消息不被并发处理,这样才能达到部分有序。消费端通过使用MessageListenerOrderly类来解决单Message Queue的消息被并发处理的问题。

Consumer使用MessageListenerOrderly的时候,下面四个Consumer的设置依旧可以使用:

  • setConsumeThreadMin
  • setConsumeThreadMax
  • setPullBatchSize
  • setConsumeMessageBatchMaxSize。

前两个参数设置Consumer的线程数;

PullBatchSize指的是一次从Broker的一个Message Queue获取消息的最大数量,默认值是32;

ConsumeMessageBatchMaxSize指的是这个Consumer的Executor(也就是调用MessageListener处理的地方)一次传入的消息数(Listmsgs这个链表的最大长度),默认值是1。

上述四个参数可以使用,说明MessageListenerOrderly并不是简单地禁止并发处理。在MessageListenerOrderly的实现中,为每个Consumer Queue加个锁,消费每个消息前,需要先获得这个消息对应的Consumer Queue所对应的锁,这样保证了同一时间,同一个Consumer Queue的消息不被并发消费,但不同Consumer Queue的消息可以并发处理

2.14 事务消息

RocketMQ的事务消息,是指发送消息事件和其他事件需要同时成功或同时失败。比如银行转账,A银行的某账户要转一万元到B银行的某账户。A银行发送“B银行账户增加一万元”这个消息,要和“从A银行账户扣除一万元”这个操作同时成功或者同时失败。

RocketMQ采用两阶段提交的方式实现事务消息,TransactionMQProducer处理上面情况的流程是,先发一个“准备从B银行账户增加一万元”的消息,发送成功后做从A银行账户扣除一万元的操作,根据操作结果是否成功,确定之前的“准备从B银行账户增加一万元”的消息是做commit还是rollback,具体流程如下:

  1. 发送方向RocketMQ发送“待确认”消息。

  2. RocketMQ将收到的“待确认”消息持久化成功后,向发送方回复消息已经发送成功,此时第一阶段消息发送完成。

  3. 发送方开始执行本地事件逻辑。

  4. 发送方根据本地事件执行结果向RocketMQ发送二次确认(Commit或是Rollback)消息,RocketMQ收到Commit状态则将第一阶段消息标记为可投递,订阅方将能够收到该消息;收到Rollback状态则删除第一阶段的消息,订阅方接收不到该消息。

  5. 如果出现异常情况,步骤4)提交的二次确认最终未到达RocketMQ,服务器在经过固定时间段后将对“待确认”消息发起回查请求。

  6. 发送方收到消息回查请求后(如果发送一阶段消息的Producer不能工作,回查请求将被发送到和Producer在同一个Group里的其他Producer),通过检查对应消息的本地事件执行结果返回Commit或Roolback状态。

  7. RocketMQ收到回查请求后,按照步骤4)的逻辑处理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面的逻辑似乎很好地实现了事务消息功能,它也是RocketMQ之前的版本实现事务消息的逻辑。但是因为RocketMQ依赖将数据顺序写到磁盘这个特征来提高性能,步骤4却需要更改第一阶段消息的状态,这样会造成磁盘Catch的脏页过多,降低系统的性能。所以RocketMQ在4.x的版本中将这部分功能去除。系统中的一些上层Class都还在,用户可以根据实际需求实现自己的事务功能。

客户端有三个类来支持用户实现事务消息,第一个类是LocalTransaction-Executer,用来实例化步骤3和步骤4的逻辑,根据情况返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE状态。第二个类是TransactionMQProducer,它的用法和DefaultMQProducer类似,要通过它启动一个Producer并发送消息,但是比DefaultMQProducer多设置本地事务处理函数和回查状态函数。第三个类是TransactionCheckListener,实现步骤5中MQ服务器的回查请求,返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE

截屏2023-11-22 14.44.12

2.15.1 RocketMQ事务消息流程概要

事务消息的大致方案分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

  • 事务消息发送及提交

    发送消息(half消息)。

    服务端响应消息写入结果。

    根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。

    根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

  • 补偿流程

    对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”

    Producer收到回查消息,检查回查消息对应的本地事务的状态

    根据本地事务状态,重新Commit或者Rollback

    补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

2.15.2 RocketMQ事务消息设计

  • 事务消息在一阶段对用户不可见

    在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。写入的如果事务消息,对消息的Topic和Queue等属性进行替换改变主题为RMQ_SYS_TRANS_HALF_TOPIC,同时将原来的Topic和Queue信息存储到消息的属性中。由于消费组未订阅该主题,故消费端无法消费half类型的消息。然后二阶段会显示执行提交或者回滚half消息(逻辑删除)。当然,为了防止二阶段操作失败,RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。

    在RocketMQ中,消息在服务端的存储结构如下,每条消息都会有对应的索引信息,Consumer通过ConsumeQueue这个二级索引来读取消息实体内容,其流程如下:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Commit和Rollback操作以及Op消息的引入

    在完成一阶段写入一条对用户不可见的消息后,二阶段如果是Commit操作,则需要让消息对用户可见;如果是Rollback则需要撤销一阶段的消息。先说Rollback的情况。对于Rollback,本身一阶段的消息对用户是不可见的,其实不需要真正撤销消息(实际上RocketMQ也无法去真正的删除一条消息,因为是顺序写文件的)。但是区别于这条消息没有确定状态(Pending状态,事务悬而未决),需要一个操作来标识这条消息的最终状态。

    RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。

  • Op消息的存储和对应关系

    RocketMQ将Op消息写入到全局一个特定的Topic中不会被用户消费。Op消息的内容为对应的Half消息的存储的Offset,这样通过Op消息能索引到Half消息进行后续的回查操作

  • Half消息的索引构建

    在执行二阶段Commit操作时,需要构建出Half消息的索引。一阶段的Half消息由于是写到一个特殊的Topic,所以二阶段构建索引时需要读取出Half消息,并将Topic和Queue替换成真正的目标的Topic和Queue,之后通过一次普通消息的写入操作来生成一条对用户可见的消息。所以RocketMQ事务消息二阶段其实是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程。

  • 如何处理二阶段失败的消息

    如果在RocketMQ事务消息的二阶段过程中失败了,例如在做Commit操作时,出现网络问题导致Commit失败,那么需要通过一定的策略使这条消息最终被Commit。RocketMQ采用了一种补偿机制,称为“回查”。Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback。Broker端通过对比Half消息和Op消息进行事务消息的回查并且推进CheckPoint(记录那些事务消息的状态是确定的)。

    rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,rocketmq默认回滚该消息。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.15.3 事务消息代码示例

生产者:

TransactionListener listener = new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                // 当发送事务消息prepare(half)(一阶段)成功后,调用该方法执行本地事务
                System.out.println("执行本地事务,参数为:" + arg);
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // return LocalTransactionState.ROLLBACK_MESSAGE;
                return LocalTransactionState.COMMIT_MESSAGE;
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                // 如果没有收到生产者发送的Half Message的响应,broker发送请求到生产者回查生产者本地事务的状态
                // 该方法用于获取本地事务执行的状态。
                System.out.println("检查本地事务的状态:" + msg);
                return LocalTransactionState.COMMIT_MESSAGE;
                // return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        };
        TransactionMQProducer producer = new TransactionMQProducer("tx_producer_grp_test");
        producer.setTransactionListener(listener);
        producer.setNamesrvAddr("namesrvAddr");
        producer.start();
        Message message = null;
        message = new Message("tp_demo_test", "hello- tx".getBytes());
        producer.sendMessageInTransaction(message, "{\"name\":\"zhangsan\"}");

消费者和正常消费没有区别

2.15 消息查询

  • 按照MessageId查询消息

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    MsgId 总共 16 字节,包含消息存储主机地址(ip/port),消息 Commit Log offset。从 MsgId 中解析出 Broker 的地址和 Commit Log 的偏移地址,然后按照存储格式所在位置将消息 buffer 解析成一个完整的消息。

    在RocketMQ中具体做法是:Client端从MessageId中解析出Broker的地址(IP地址和端口)和Commit Log的偏移地址后封装成一个RPC请求后,通过Remoting通信层发送(业务请求码:VIEW_MESSAGE_BY_ID)。Broker使用QueryMessageProcessor,使用请求中的 commitLog offset和 size 去 commitLog 中找到真正的记录并解析成一个完整的消息返回

  • 按照Message Key查询消息

    “按照Message Key查询消息”,主要是基于RocketMQ的IndexFile索引文件来实现的。RocketMQ的索引文件逻辑结构,类似JDK中HashMap的实现。索引文件的具体结构如下

    截屏2023-11-22 16.04.31

    查询步骤

    • 根据查询的 key 的 hashcode%slotNum 得到具体的槽的位置(slotNum 是一个索引文件里面包含的最大槽的数目, 例如图中所示slotNum=5000000)。

    • 根据 slotValue(slot 位置对应的值)查找到索引项列表的最后一项(倒序排列,slotValue 总是指向最新的一个索引项)。

    • 遍历索引项列表返回查询时间范围内的结果集(默认一次最大返回的 32 条记录)

    Hash 冲突

    • key 的 hash 值不同但模数相同,此时查询的时候会再比较一次 key 的 hash 值(每个索引项保存了 key 的 hash 值),过滤掉 hash 值不相等的项。
    • hash 值相等但 key 不等, 出于性能的考虑冲突的检测放到客户端处理(key 的原始值是存储在消息文件中的,避免对数据文件的解析), 客户端比较一次消息体的 key 是否相同

    为了节省空间索引项中存储的时间是时间差值(存储时间-开始时间,开始时间存储在索引文件头中), 整个索引文件是定长的,结构也是固定的

2.16 消息优先级

有些场景,需要应用程序处理几种类型的消息,不同消息的优先级不同。RocketMQ是个先入先出的队列,不支持消息级别或者Topic级别的优先级。业务中简单的优先级需求,可以通过间接的方式解决

  • 流量大的消息设置单独的 Topic

    多个不同的消息类型使用同一个topic时,由于某一个种消息流量非常大,导致其他类型的消息无法及时消费,造成不公平,所以把流量大的类型消息在一个单独的 Topic,其他类型消息在另外一个Topic,应用程序创建两个 Consumer,分别订阅不同的 Topic,这样就可以了。

  • 设置多个MessageQueue根据业务选择MessageQueue

    设置 Topic 时将 MessageQueue 数量设置的多一些,根据不同的业务选择不同的MessageQueue,这样某个业务消息大量增加不影响其他业务。因为DefaultMQPushConsumer默认是采用循环的方式逐个读取一个 Topic 的所有 MessageQueue,这样如果某业务流量大增,这个业务对应的 MessageQueue 消息数增多,等待时间增长,但不会造成其他业务的消息等待时间增长。

  • 强制优先级

    TypeA、 TypeB、 TypeC 三类消息 。 TypeA 处于第一优先级,要确保只要有TypeA消息,必须优先处理; TypeB处于第二优先级; TypeC 处于第三优先级 。 对这种要求,或者逻辑更复杂的要求,就要用 户自己编码实现优先级控制,如果上述的三类消息在一个Topic 里,可以使 用 PullConsumer,自主控制 MessageQueue 的遍历,以及消息的读取;如果上述三类消息在三个 Topic下,需要启动三个Consumer, 实现逻辑控制三个 Consumer 的消费 。

三.RocketMQ高级实战

3.1 product

  • tag的使用

    一个应用尽可能用一个Topic,而消息子类型则可以用tags来标识。tags可以由应用自由设置,只有生产者在发送消息设置了tags,消费方在订阅消息时才可以利用tags通过broker做消息过滤:message.setTags(“TagA”)。

  • keys的使用

    每个消息在业务层面的唯一标识码要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic、key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。

    // 订单Id
    
    String orderId = "20034568923546";
    message.setKeys(orderId);
    
  • 日志打印

    消息发送成功或者失败要打印消息日志,务必要打印SendResult和key字段。send消息方法只要不抛异常,就代表发送成功。发送成功会有多个状态,在sendResult里定义。

    • SEND_OK

      消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。

    • FLUSH_DISK_TIMEOUT

      消息发送成功但是服务器刷盘超时。此时消息已经进入服务器队列(内存),只有服务器宕机,消息才会丢失。消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度。

      如果Broker服务器设置了刷盘方式为同步刷盘,即FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当Broker服务器未在同步刷盘时间内(默认为5s)完成刷盘,则将返回该状态—刷盘超时。

    • FLUSH_SLAVE_TIMEOUT

      消息发送成功,但是服务器同步到Slave时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。

      如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master即ASYNC_MASTER),并且从Broker服务器未在同步刷盘时间(默认为5秒)内完成与主服务器的同步,则将返回该状态—数据同步到Slave服务器超时。

    • SLAVE_NOT_AVAILABLE

      消息发送成功,但是此时Slave不可用。

      如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master服务器即ASYNC_MASTER),但没有配置slave Broker服务器,则将返回该状态—无Slave服务器可用。

  • 消息发送失败处理方式

    Producer的send方法本身支持内部重试,重试逻辑如下:

    • 至多重试2次(同步发送为2次,异步发送为0次)。

    • 如果发送失败,则轮转到下一个Broker。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。

    • 如果本身向broker发送消息产生超时异常,就不会再重试。

    以上策略也是在一定程度上保证了消息可以发送成功。如果业务对消息可靠性要求比较高,建议应用增加相应的重试逻辑:比如调用send同步方法发送失败时,则尝试将消息存储到db,然后由后台线程定时重试,确保消息一定到达Broker。

    上述db重试方式为什么没有集成到MQ客户端内部做,而是要求应用自己去完成,主要基于以下几点考虑:

    • MQ的客户端设计为无状态模式,方便任意的水平扩展,且对机器资源的消耗仅仅是cpu、内存、网络。

    • 如果MQ客户端内部集成一个KV存储模块,那么数据只有同步落盘才能较可靠,而同步落盘本身性能开销较大,所以通常会采用异步落盘,又由于应用关闭过程不受MQ运维人员控制,可能经常会发生 kill -9 这样暴力方式关闭,造成数据没有及时落盘而丢失。

    • 重试过程应交由应用来控制。

  • oneway形式发送

    通常消息的发送是这样一个过程:

    • 客户端发送请求到服务器

    • 服务器处理请求

    • 服务器向客户端返回应答

    所以,一次消息发送的耗时时间是上述三个步骤的总和,而某些场景要求耗时非常短,但是对可靠性要求并不高,例如日志收集类应用,此类应用可以采用oneway形式调用,oneway形式只发送请求不等待应答,而发送请求在客户端实现层面仅仅是一个操作系统系统调用的开销,即将数据写入客户端的socket缓冲区,此过程耗时通常在微秒级

3.2 consume

  • 消费过程幂等

    RocketMQ无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。

    可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。

    在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)

    实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。

  • 消费速度慢的处理方式

    • 提高消费并行度

      绝大部分消息消费行为都属于 IO 密集型,即可能是操作数据库,或者调用 RPC,这类消费行为的消费速度在于后端数据库或者外系统的吞吐量。

      通过增加消费并行度,可以提高总的消费吞吐量,但是并行度增加到一定程度,反而会下降

      所以,应用必须要设置合理的并行度。 如下有几种修改消费并行度的方法:

      • 同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度(不能超过消费者队列数量,不然多余的 Consumer 实例无效)。可以通过加机器,或者在已有机器启动多个进程的方式。

      • 提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax实现。

    • 批量方式消费

      某些业务流程如果支持批量方式消费,则可以很大程度上提高消费吞吐量。

      例如订单扣款类应用,一次处理一个订单耗时 1 s,一次处理 10 个订单可能也只耗时 2 s,这样即可大幅度提高消费的吞吐量,通过设置 consumer的 consumeMessageBatchMaxSize 返个参数,默认是 1,即一次只消费一条消息,例如设置为 N,那么每次消费的消息数小于等于 N。

    • 跳过非重要消息

      发生消息堆积时,如果消费速度一直追不上发送速度,如果业务对数据要求不高的话,可以选择丢弃不重要的消息

    • 优化每条消息消费过程

    某条消息的消费过程如下:

    • 根据消息从 DB 查询【数据 1】

    • 根据消息从 DB 查询【数据 2】

    • 复杂的业务计算

    • 向 DB 插入【数据 3】

    • 向 DB 插入【数据 4】

    这条消息的消费过程中有4次与 DB的 交互,如果按照每次 5ms 计算,那么总共耗时 20ms,假设业务计算耗时 5ms,那么总过耗时 25ms,所以如果能把 4 次 DB 交互优化为 2 次,那么总耗时就可以优化到 15ms,即总体性能提高了 40%。所以应用如果对时延敏感的话,可以把DB部署在SSD硬盘,相比于SCSI磁盘,前者的RT会小很多。

    • 打印日志

      如果消息量较少,建议在消费入口方法打印消息,消费耗时等,方便后续排查问

  • 其他消费建议

  • 关于消费者和订阅

    不同的消费组可以独立的消费 topic,每个消费组都有自己的消费偏移量。

    确保同一组内的每个消费者订阅信息保持一致

  • 关于有序消息

    消费者将锁定每个消息队列,以确保他们被逐个消费,会导致性能下降,但是当关心消息顺序的时候会很有用

    不建议抛出异常,可以返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 作为替代。

  • 关于并发消费

    消费者将并发消费消息,可以使用它来获得良好性能,不建议抛出异常,可以返回ConsumeConcurrentlyStatus.RECONSUME_LATER 作为替代。

  • 关于消费状态Consume Status

    对于并发的消费监听器,可以返回 RECONSUME_LATER 来通知消费者现在不能消费这条消息,并且希望可以稍后重新消费它。

    对于有序的消息监听器,因为关心顺序,所以不能跳过消息,但是可以返回SUSPEND_CURRENT_QUEUE_A_MOMENT 告诉消费者等待一段时间。

  • 关于Blocking

    不建议阻塞监听器,因为它会阻塞线程池,并最终可能会终止消费进程

  • 关于线程数设置

    消费者使用 ThreadPoolExecutor 在内部对消息进行消费,可以通过设置setConsumeThreadMin 或 setConsumeThreadMax 来设置线程数量

  • 关于消费位点

    当建立一个新的消费组时,可以通过设置consumer.setConsumeFromWhere()决定是否需要消费已经存在于 Broker 中的历史消息

    CONSUME_FROM_LAST_OFFSET 将会忽略历史消息,并消费之后生成的任何消息(默认)。

    CONSUME_FROM_FIRST_OFFSET 将会从头消费每个存在于 Broker 中的信息。

    CONSUME_FROM_TIMESTAMP 来消费在指定时间戳后产生的消息,需要同时指定consumer的consumeTimestamp

3.3 Broker

  • Broker

    Broker 角色分为 ASYNC_MASTER(异步主机)、SYNC_MASTER(同步主机)以及SLAVE(从机)。如果对消息的可靠性要求比较严格,可以采用 SYNC_MASTER加SLAVE的部署方式。如果对消息可靠性要求不高,可以采用ASYNC_MASTER加SLAVE的部署方式。如果只是测试方便,则可以选择仅ASYNC_MASTER或仅SYNC_MASTER的部署方式。

  • FlushDiskType

    SYNC_FLUSH(同步刷新)相比于ASYNC_FLUSH(异步处理)会损失很多性能,但是也更可靠,所以需要根据实际的业务场景做好权衡。

  • Broker配置

    /opt/rocket/conf/broker.conf 文件:Broker的配置文件

    参数名默认值说明
    listenPort10911接受客户端连接的监听端口
    namesrvAddrnullnameServer 地址
    brokerIP1网卡的 InetAddress当前 broker 监听的 IP
    brokerIP2跟 brokerIP1 一样存在主从 broker 时,如果在 broker 主节点上配置了 brokerIP2 属性,broker 从节点会连接主节点配置的 brokerIP2 进行同步
    brokerNamenullbroker 的名称
    brokerClusterNameDefaultCluster本 broker 所属的 Cluser 名称
    brokerId0broker id, 0 表示 master, 其他的正整数表示 slave
    storePathCommitLog$HOME/store/commitlog/存储 commit log 的路径
    storePathConsumerQueue$HOME/store/consumequeue/存储 consume queue 的路径
    mappedFileSizeCommitLog1024 * 1024 * 1024(1G)commit log 的映射文件大小
    deleteWhen04在每天的什么时间删除已经超过文件保留时间的commit log
    fileReservedTime72以小时计算的文件保留时间
    brokerRoleASYNC_MASTERSYNC_MASTER/ASYNC_MASTER/SLAVE
    flushDiskTypeASYNC_FLUSHSYNC_FLUSH/ASYNC_FLUSH SYNC_FLUSH 模式下的 broker 保证在收到确认生产者之前将消息刷盘。ASYNC_FLUSH 模式下的 broker 则利用刷盘一组消息的模式,可以取得更好的性能。

3.4 nameServer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • NameServer互相独立,彼此没有通信关系,单台NameServer挂掉,不影响其他NameServer。

  • NameServer不去连接别的机器,不主动推消息

  • 单个Broker(Master、Slave)与所有NameServer进行定时注册,以便告知NameServer自己还活着。

    • Broker每隔30秒向所有NameServer发送心跳,心跳包含了自身的topic配置信息。
    • NameServer每隔10秒,扫描所有还存活的broker连接,如果某个连接的最后更新时间与当前时间差值超过2分钟,则断开此连接。同时更新topic与队列的对应关系,但不通知生产者和消费者。
    • Broker slave 同步或者异步从Broker master 上拷贝数据。
  • Consumer随机与一个NameServer建立长连接,如果该NameServer断开,则从NameServer列表中查找下一个进行连接。

    • Consumer主要从NameServer中根据Topic查询Broker的地址,查到就会缓存到客户端,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。
    • 如果Broker宕机,则NameServer会将其剔除,而Consumer端的定时任务MQClientInstance.this.updateTopicRouteInfoFromNameServer 每30秒执行一次,将Topic对应的Broker地址拉取下来,此地址只有Slave地址了,此时Consumer从Slave上消费。
    • 消费者与Master和Slave都建有连接,在不同场景有不同的消费规则。
  • Producer随机与一个NameServer建立长连接,每隔30秒(此处时间可配置)从NameServer获取Topic的最新队列情况,如果某个Broker Master宕机,Producer最多30秒才能感知在这个期间,发往该broker master的消息失败。Producer向提供Topic服务的Master建立长连接,且定时向Master发送心跳。

    • 生产者与所有的master连接,但不能向slave写入。
    • 客户端是先从NameServer寻址的,得到可用Broker的IP和端口信息,然后据此信息连接broker。

NameServer在RocketMQ中的作用:

  1. NameServer 用来保存活跃的 broker 列表,包括 Master 和 Slave 。
  2. NameServer 用来保存所有 topic 和该 topic 所有队列的列表。
  3. NameServer 用来保存所有 broker 的 Filter 列表。
  4. 提供最新的路由信息。

RocketMQ为什么不使用ZooKeeper作为注册中心?

服务发现领域,ZooKeeper 根本就不能算是最佳的选择。

  1. 注册中心是CP还是AP系统?

    在分布式系统中,即使是对等部署的服务,因为请求到达的时间,硬件的状态,操作系统的调度,虚拟机的GC等,任何一个时间点,这些对等部署的节点状态也不可能完全一致,而流量不一致的情况下,只要注册中心在承诺的时间内(例如1s内)将数据收敛到一致状态(即满足最终一致),流量将很快趋于统计学意义上的一致,所以注册中心以最终一致的模型设计在生产实践中完全可以接受。

    分区容忍及可用性需求分析实践中,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性,这是注册中心设计应该遵循的在CAP的权衡中,注册中心的可用性比数据强一致性更宝贵,所以整体设计更应该偏向AP,而非CP,数据不一致在可接受范围

  2. 服务规模、容量、服务联通性

当数据中心服务规模超过一定数量,作为注册中心的ZooKeeper性能堪忧,因为ZooKeeper的写并不是可扩展的,不可以通过加节点解决水平扩展性问题。
在服务发现和健康监测场景下,随着服务规模的增大,无论是应用频繁发布时的服务注册带来的写请求,还是刷毫秒级的服务健康状态带来的写请求,还是恨不能整个数据中心的机器或者容器皆与注册中心有长连接带来的连接压力上,ZooKeeper很快就会力不从心,

  1. 注册中心持久存储和事务日志

    在服务发现场景中,其最核心的数据—实时的健康的服务的地址列表,不需要数据持久化,因为在服务发现中,服务调用发起方更关注的是其要调用的服务的实时的地址列表和实时健康状态,每次发起调用时,并不关心要调用的服务的历史服务地址列表、过去的健康状态。

    但是一个完整的生产可用的注册中心,除了服务的实时地址列表以及实时的健康状态之外,还会存储一些服务的元数据信息,例如服务的版本,分组,所在的数据中心,权重,鉴权策略信息,服务标签等元数据,这些数据需要持久化存储,并且注册中心应该提供对这些元数据的检索的能力。

  2. 服务健康检查

    使用ZooKeeper作为服务注册中心时,服务的健康检测绑定在了ZooKeeper对于Session的健康监测上,或者说绑定在TCP长链接活性探测上了。

    ZK与服务提供者机器之间的TCP长链接活性探测正常的时候,该服务不一定就是健康的,注册中心应该提供更丰富的健康监测方案,服务的健康与否的逻辑应该开放给服务提供方自己定义,而不是一刀切搞成了TCP活性检测!

    健康检测的一大基本设计原则就是尽可能真实的反馈服务本身的真实健康状态,否则一个不敢被服务调用者相信的健康状态判定结果还不如没有健康检测。

  3. 注册中心的容灾考虑

    如果注册中心(Registry)本身完全宕机了,服务调用链路不应该受到影响

    服务调用(请求响应流)链路应该是弱依赖注册中心,必须仅在服务发布,机器上下线,服务扩缩容等必要时才依赖注册中心。

    这需要注册中心仔细的设计自己提供的客户端,客户端中应该有针对注册中心服务完全不可用时做容灾的手段,例如设计客户端缓存数据机制就是行之有效的手段。另外,注册中心的健康检查机制也要仔细设计以便在这种情况不会出现诸如推空等情况的出现。

    ZooKeeper的原生客户端并没有这种能力,所以利用ZooKeeper实现注册中心的时候需要注意的是如果把ZooKeeper所有节点全干掉,生产上的所有服务调用链路是否能不受任何影响

  4. 场景

    ZK在粗粒度分布式锁,分布式选主,主备高可用切换等不需要高TPS支持的场景下有不可替代的作用,而这些需求往往多集中在大数据、离线任务等相关的业务领域,因为大数据领域,讲究分割数据集,并且大部分时间分任务多进程/线程并行处理这些数据集,但是总是有一些点上需要将这些任务和进程统一协调,这时候就是ZooKeeper发挥巨大作用的用武之地。

    但是在交易场景交易链路上,在主业务数据存取,大规模服务发现、大规模健康监测等方面有天然的短板,应该竭力避免在这些场景下引入ZooKeeper,

    对于ZooKeeper,大数据使用,服务发现不用。

3.5 clinet配置

相对于RocketMQ的Broker集群,生产者和消费者都是客户端。

DefaultMQProducer、TransactionMQProducer、DefaultMQPushConsumer、DefaultMQPullConsumer都继承于ClientConfig类,ClientConfig为客户端的公共配置类。

客户端的配置都是get、set形式,每个参数都可以用spring来配置,也可以在代码中配置。

3.5.1 clinet寻址

在RocketMQ中客户端先找到NameServer, 然后通过NameServer再找到Broker。NameServer有多种配置方式,优先级由高到低,高优先级会覆盖低优先级。

  • 代码中指定NameServer地址,多个NameServer地址之间用分号分割

    //生产者
    producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");
    //消费者
    consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");
    
  • Java启动参数中指定NameServer地址

    -Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876
    
  • 环境变量指定NameServer地址

    export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876

  • HTTP静态服务器寻址(默认)

    该静态地址,客户端第一次会10s后调用,然后每个2分钟调用一次。

    客户端启动后,会定时访问一个静态HTTP服务器,地址如下:http://jmenv.tbsite.net:8080/rocketmq/nsaddr,这个URL的返回内容如下:

    192.168.0.1:9876;192.168.0.2:9876

    这个地址是在源码中写死的,也可以通过配置修改,rocketmq.namesrv.domain可以配置域名,rocketmq.namesrv.domain.subgroup可以配置nsaddr

    推荐使用HTTP静态服务器寻址方式,NameServer集群可以热升级。NameServer地址发生改变时只需要修改域名解析,客户端不需要重启。

3.5.2 clinet公共配置

参数名默认值说明
namesrvAddrNameServer地址列表,多个NameServer 地址用分号隔开
clientIP本机IP客户端本机IP地址,某些机器会发生无法识别客户端IP地址情况,需要应用在代码中强制指定
instanceNameDEFAULT客户端实例名称,客户端创建的多个Producer、Consumer实际是共用一个内部实例(这个实例包含网络连接、线程资源等)
namespace客户端命名空间
clientCallbackExecutorThreadsRuntime.getRuntime()通信层异步回调线程数
pollNameServerInteval30000轮询NameServer间隔时间,单位毫秒
heartbeatBrokerInterval30000向Broker发送心跳间隔时间,单位毫秒
persistConsumerOffsetInterval5000作用于Consumer,持久化Consumer消费进度间隔时间,单位毫秒
accessChannelLOCAL设置访问通道
pullTimeDelayMillsWhenException1000拉取消息出现异常的延迟时间设置
unitName单位名称
unitModefalse客单位模式
vipChannelEnabledtrue是否启用vip netty通道以发送消息
useTLSfalse是否使用安全传输。
mqClientApiTimeout3000mq客户端api超时设置
languageLanguageCode.JAVA客户端实现语言

3.5.3 product配置

参数名默认值说明
producerGroupDEFAULT_PRODUCERProducer组名,多个Producer如果属于一个应用,发送同样的消息,则应该将它们归为同一组
createTopicKeyTBW102在发送消息时,自动创建服务器不存在的topic,需要指定Key,该Key可用于配置发送消息所在topic的默认路由
defaultTopicQueueNums4在发送消息,自动创建服务器不存在的topic时,默认创建的队列数
sendMsgTimeout10000发送消息超时时间,单位毫秒
compressMsgBodyOverHowmuch4096消息Body超过多大开始压缩(Consumer收到消息会自动解压缩),单位字节
retryAnotherBrokerWhenNotStoreOKFALSE如果发送消息返回sendResult,但是sendStatus!=SEND_OK,是否重试发送
retryTimesWhenSendFailed2如果消息发送失败,最大重试次数,该参数只对同步发送模式起作用
maxMessageSize400M客户端限制的消息大小,超过报错,同时服务端也会限制,所以需要跟服务端配合使用
transactionCheckListener事务消息回查监听器,如果发送事务消息,必须设置
checkThreadPoolMinSize1Broker回查Producer事务状态时,线程池最小线程数
checkThreadPoolMaxSize1Broker回查Producer事务状态时,线程池最大线程数
checkRequestHoldMax2000Broker回查Producer事务状态时,Producer本地缓冲请求队列大小
RPCHooknull该参数是在Producer创建时传入的,包含消息发送前的预处理和消息响应后的处理两个接口,用户可以在第一个接口中做一些安全控制或者其他操作

3.5.4 PushConsumer配置

参数名默认值说明
consumerGroupDEFAULT_CONSUMERConsumer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
messageModelCLUSTERING消费模型支持集群消费和广播消费两种
consumeFromWhereCONSUME_FROM_LAST_OFFSETConsumer启动后,默认从上次消费的位置开始消费,这包含两种情况:一种是上次消费的位置未过期,则消费从上次中止的位置进行;一种是上次消费位置已经过期,则从当前队列第一条消息开始消费
consumeTimestamp消费的起始时间只有当consumeFromWhere值为CONSUME_FROM_TIMESTAMP时才起作用。
allocateMessageQueueStrategyAllocateMessageQueueAveragelyRebalance算法实现策略,负载均衡
subscription订阅关系
messageListener消息监听器
offsetStore消费进度存储
consumeThreadMin10消费线程池最小线程数
consumeThreadMax20消费线程池最大线程数
consumeConcurrentlyMaxSpan2000单队列并行消费允许的最大跨度
pullThresholdForQueue1000拉消息本地队列缓存消息最大数
pullInterval0拉消息间隔,由于是长轮询,所以为0,但是如果应用为了流控,也可以设置大于0的值,单位毫秒
consumeMessageBatchMaxSize1批量消费,一次消费多少条消息
pullBatchSize32批量拉消息,一次最多拉多少条

3.5.5 PullConsumer配置

参数名默认值说明
consumerGroupDEFAULT_CONSUMERConsumer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
messageModelBROADCASTING消费模型支持集群消费和广播消费两种
brokerSuspendMaxTimeMillis20000长轮询,Consumer拉消息请求在Broker挂起最长时间,单位毫秒
consumerTimeoutMillisWhenSuspend30000长轮询,Consumer拉消息请求在Broker挂起超过指定时间,客户端认为超时,单位毫秒
consumerPullTimeoutMillis10000非长轮询,拉消息超时时间,单位毫秒
messageQueueListener监听队列变化
offsetStore消费进度存储
registerTopics注册的topic集合
allocateMessageQueueStrategyAllocateMessageQueueAveragelyRebalance算法实现策略

3.5.6 Message数据结构

参数名默认值说明
Topicnull必填,消息所属topic的名称
Bodynull必填,消息体
Tagsnull选填,消息标签,方便服务器过滤使用。目前只支持每个消息设置一个tag
Keysnull选填,代表这条消息的业务关键词,服务器会根据keys创建哈希索引,设置后,可以在Console系统根据Topic、Keys来查询消息,由于是哈希索引,请尽可能保证key唯一,例如订单号,商品Id等
Flag0选填,完全由应用来设置,RocketMQ不做干预
DelayTimeLevel0选填,消息延时级别,0表示不延时,大于0会延时特定的时间才会被消费
WaitStoreMsgOKTRUE选填,表示消息是否在服务器落盘后才返回应答

3.6 系统配置

  • jvm

    设置Xms和Xmx一样大,防止JVM重新调整堆空间大小影响性能。

    -server -Xms8g -Xmx8g -Xmn4g
    

    设置DirectByteBuffer内存大小。当DirectByteBuffer占用达到这个值,就会触发Full GC。

    -XX:MaxDirectMemorySize=15g
    

    如果不太关心RocketMQ的启动时间,可以设置pre-touch,这样在JVM启动的时候就会分配完整的页空间。

    -XX:+AlwaysPreTouch
    

    禁用偏向锁可能减少JVM的停顿,因为偏向锁在线程需要获取锁之前会判断当前线程是否拥有锁,如果拥有,就不用再去获取锁了。

    在并发小的时候使用偏向锁有利于提升JVM效率,在高并发场合禁用掉。

    -XX:-UseBiasedLocking
    

    推荐使用JDK1.8的G1垃圾回收器:

    当在G1的GC日志中看到 to-space overflow 或者 to-space exhausted 的时候,表示G1没有足够的内存使用的(可能是 survivor 区不够了,可能是老年代不够了,也可能是两者都不够了),这时候表示Java堆占用大小已经达到了最大值。为了解决这个问题,可以尝试做以下调整:

    • 增加预留内存:增大参数 -XX:G1ReservePercent 的值(相应的增加堆内存)来增加预留内存;

    • 更早的开始标记周期:减小 -XX:InitiatingHeapOccupancyPercent 参数的值,以更早的开始标记周期;

    • 增加并发收集线程数:增大 -XX:ConcGCThreads 参数值,以增加并行标记线程数

    • 如果你看到因为大对象的分配导致不断的启动并发收集,并且这种分配使得老年代碎片化不断加剧,那么请增加-XX:G1HeapRegionSize参数的值,这样的话,大对象将不再被G1认为是大对象,它会走普通对象的分配流程。

      # G1回收器将堆空间划分为1024个region,此选项指定堆空间region的大小
      -XX:+UseG1GC -XX:G1HeapRegionSize=16m
      -XX:G1ReservePercent=25
      -XX:InitiatingHeapOccupancyPercent=30
      

    上述设置可能有点儿激进,但是对于生产环境,性能很好。

    -XX:MaxGCPauseMillis不要设置的太小,否则JVM会使用小的年轻代空间以达到此设置的值,同时引起很频繁的minor GC。推荐使用GC log文件:

    -XX:+UseGCLogFileRotation
    -XX:NumberOfGCLogFiles=5
    -XX:GCLogFileSize=30m
    

    如果写GC文件增加了Broker的延迟,可以考虑将GC log文件写到内存文件系统

    -Xloggc:/dev/shm/mq_gc_%p.log123
    
  • linux

    • vm.extra_free_kbytes

      告诉VM在后台回收(kswapd)启动的阈值与直接回收(通过分配进程)的阈值之间保留额外的可用内存。RocketMQ使用此参数来避免内存分配中的长延迟。(与具体内核版本相关)

    • vm.min_free_kbytes

      如果将其设置为低于1024KB,将会巧妙的将系统破坏,并且系统在高负载下容易出现死锁。

    • vm.max_map_count

      限制一个进程可能具有的最大内存映射区域数。RocketMQ将使用mmap加载CommitLog和ConsumeQueue,因此建议将为此参数设置较大的值。

    • vm.swappiness

      定义内核交换内存页面的积极程度。较高的值会增加攻击性,较低的值会减少交换量。建议将值设置为10来避免交换延迟。

    • File descriptor limits

      RocketMQ需要为文件(CommitLog和ConsumeQueue)和网络连接打开文件描述符。我们建议设置文件描述符的值为655350。

    • Disk scheduler

      RocketMQ建议使用I/O截止时间调度器,它试图为请求提供有保证的延迟。

3.7 动态扩缩容Broker

由于业务增长,需要对集群进行扩容的时候,可以动态增加Broker角色的机器。只增加Broker不会对原有的Topic产生影响,原来创建好的Topic中数据的读写依然在原来的那些Broker上进行。

集群扩容后,一是可以把新建的Topic指定到新的Broker机器上,均衡利用资源;另一种方式是通过updateTopic命令更改现有的Topic配置,在新加的Broker上创建新的队列。比如TestTopic是现有的一个Topic,因为数据量增大需要扩容,新增的一个Broker机器地址是192.168.0.1:10911,这个时候执行下面的命令:sh./bin/mqadmin updateTopic-b 192.168.0.1:10911-t TestTopic-n 192.168.0.100:9876结果是在新增的Broker机器上,为TestTopic新创建了8个读写队列。

减少Broker要看是否有持续运行的Producer,当一个Topic只有一个Master Broker,停掉这个Broker后,消息的发送肯定会受到影响,需要在停止这个Broker前,停止发送消息。

当某个Topic有多个Master Broker,停了其中一个,是否会丢失消息和Producer使用的发送消息的方式有关,如果使用同步方式send(msg)发送,在DefaultMQProducer内部有个自动重试逻辑,其中一个Broker停了,会自动向另一个Broker发消息,不会发生丢消息现象。如果使用异步方式发送send(msg,callback),或者用sendOneWay方式,会丢失切换过程中的消息。因为在异步和sendOneWay这两种发送方式下,Producer.setRetryTimesWhenSendFailed设置不起作用,发送失败不会重试。

DefaultMQProducer默认每30秒到NameServer请求最新的路由消息,Producer如果获取不到已停止的Broker下的队列信息,后续就自动不再向这些队列发送消息。

如果Producer程序能够暂停,在有一个Master和一个Slave的情况下也可以顺利切换。可以关闭Producer后关闭Master Broker,这个时候所有的读取都会被定向到Slave机器,消费消息不受影响。把Master Broker机器置换完后,基于原来的数据启动这个Master Broker,然后再启动Producer程序正常发送消息

用Linux的kill pid命令就可以正确地关闭Broker,BrokerController下有个shutdown函数,这个函数被加到了ShutdownHook里,当用Linux的kill命令时(不能用kill-9),shutdown函数会先被执行。也可以通过RocketMQ提供的工具(mqshutdown broker)来关闭Broker,它们的原理是一样的

3.8 故障对消息的影响

  1. Broker正常关闭,启动;
  2. Broker异常Crash,然后启动;
  3. OS Crash,重启;
  4. 机器断电,但能马上恢复供电;
  5. 磁盘损坏;
  6. CPU、主板、内存等关键设备损坏。

假设现有的RocketMQ集群,每个Topic都配有多Master角色的Broker供写入,并且每个Master都至少有一个Slave机器(用两台物理机就可以实现上述配置),我们来看看在上述情况下消息的可靠性情况。

第1种情况属于可控的软件问题,内存中的数据不会丢失。如果重启过程中有持续运行的Consumer,Master机器出故障后,Consumer会自动重连到对应的Slave机器,不会有消息丢失和偏差。当Master角色的机器重启以后,Consumer又会重新连接到Master机器(注意在启动Master机器的时候,如果Consumer正在从Slave消费消息,不要停止Consumer。假如此时先停止Consumer后再启动Master机器,然后再启动Consumer,这个时候Consumer就会去读Master机器上已经滞后的offset值,造成消息大量重复)。

如果第1种情况出现时有持续运行的Producer,一台Master出故障后,Producer只能向Topic下其他的Master机器发送消息,如果Producer采用同步发送方式,不会有消息丢失。

第2、3、4种情况属于软件故障,内存的数据可能丢失,所以刷盘策略不同,造成的影响也不同,如果Master、Slave都配置成SYNC_FLUSH,可以达到和第1种情况相同的效果。

第5、6种情况属于硬件故障,发生第5、6种情况的故障,原有机器的磁盘数据可能会丢失。如果Master和Slave机器间配置成同步复制方式,某一台机器发生5或6的故障,也可以达到消息不丢失的效果。如果Master和Slave机器间是异步复制,两次Sync间的消息会丢失。

总的来说,当设置成:

  1. 多Master,每个Master带有Slave;
  2. 主从之间设置成SYNC_MASTER;
  3. Producer用同步方式写;
  4. 刷盘策略设置成SYNC_FLUSH。

就可以消除单点依赖,即使某台机器出现极端故障也不会丢消息

四.RocketMQ源码简单分析

4.1 源码环境搭建

工具:JDK :1.8+、Maven、IntelliJ IDEA

源码: https://github.com/apache/rocketmq

关闭maven test,执行:clean install -Dmaven.test.skip=true

创建 conf 配置文件夹,从 distribution 拷贝 broker.conf 和 logback_broker.xml 和logback_namesrv.xml

创建数据文件夹 dataDir

4.1.1 启动NameServer

入口类是org.apache.rocketmq.namesrv.NamesrvStartup

启动配置主要是配置ROCKERTMQ_HOME,配置成自己的源码目录就可以

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

启动成功后控制台打印:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.1.2 启动Broker

修改conf目录broker.conf文件内容

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH

# namesrvAddr地址
namesrvAddr=127.0.0.1:9876
# 启用自动创建主题
autoCreateTopicEnable=true
# 存储路径
storePathRootDir=/My/rocketmq-rocketmq-all-4.5.1/dataDir
# commitLog路径
storePathCommitLog=/My/rocketmq-rocketmq-all-4.5.1/dataDir/commitlog
# 消息队列存储路径
storePathConsumeQueue=/My/rocketmq-rocketmq-all-4.5.1/dataDir/consumequeue
# 消息索引存储路径
storePathIndex=/My/rocketmq-rocketmq-all-4.5.1/dataDir/index
# checkpoint文件路径
storeCheckpoint=/My/rocketmq-rocketmq-all-4.5.1/dataDir/checkpoint
# abort文件存储路径
abortFile=/My/rocketmq-rocketmq-all-4.5.1/dataDir/abort

启动的入口类是org.apache.rocketmq.broker.BrokerStartup

配置启动环境,主要是配置ROCKERTMQ_HOME和启动参数

截屏2023-11-24 14.14.16

启动成功后控制台打印

The broker[broker-a, 172.16.25.227:10911] boot success. serializeType=JSON and name server is 127.0.0.1:9876

4.1.3 发送消息

进入example模块的org.apache.rocketmq.example.quickstart.Producer配置NameServer

DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("127.0.0.1:9876");

启动main方法就可以发消息

4.1.4 消费消息

进入example模块的org.apache.rocketmq.example.quickstart.Consumer

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
consumer.setNamesrvAddr("127.0.0.1:9876");

启动main方法就可以消费消息

4.2 NameServer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2.1 架构

Broker消息服务器在启动时向所有NameServer注册,生产者(Producer)在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载均衡算法从列表中选择一台服务器进行发送。

每台Broker都会和NameServer保持长连接,每间隔30S发送心跳,如果NameServer检测到Broker宕机,则从路由注册表中删除。

但是路由变化不会马上通知生产者。这样设计的目的是为了降低NameServer实现的复杂度,在消息发送端提供容错机制保证消息发送的可用性。

NameServer本身的高可用是通过部署多台NameServer来实现,但彼此之间不通讯,也就是NameServer服务器之间在某一个时刻的数据并不完全相同,但这对消息发送并不会造成任何影响,这也是NameServer设计的一个亮点

4.2.2 启动流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public static void main(String[] args) {
    main0(args);
}

public static NamesrvController main0(String[] args) {

    try {
        //创建controller
        NamesrvController controller = createNamesrvController(args);
        //启动controller
        start(controller);
        String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
        log.info(tip);
        System.out.printf("%s%n", tip);
        return controller;
    } catch (Throwable e) {
        e.printStackTrace();
        System.exit(-1);
    }

    return null;
}
  1. 创建NamesrvController

    • 创建NameServerConfig

    • 创建NettyServerConfig

    • 解析配置文件

    • 创建NamesrvController对象

    org.apache.rocketmq.namesrv.NamesrvStartup#createNamesrvController

    public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
        //PackageConflictDetect.detectFastjson();
    
        Options options = ServerUtil.buildCommandlineOptions(new Options());
        commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
        if (null == commandLine) {
            System.exit(-1);
            return null;
        }
    
        final NamesrvConfig namesrvConfig = new NamesrvConfig();
        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        nettyServerConfig.setListenPort(9876);
        //解析命令行中 -c 指定的文件
        if (commandLine.hasOption('c')) {
            String file = commandLine.getOptionValue('c');
            if (file != null) {
                InputStream in = new BufferedInputStream(new FileInputStream(file));
                properties = new Properties();
                properties.load(in);
                MixAll.properties2Object(properties, namesrvConfig);
                MixAll.properties2Object(properties, nettyServerConfig);
    
                namesrvConfig.setConfigStorePath(file);
    
                System.out.printf("load config properties file OK, %s%n", file);
                in.close();
            }
        }
        //将命令行中 -p 指定的信息打印信息
        if (commandLine.hasOption('p')) {
            InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
            MixAll.printObjectProperties(console, namesrvConfig);
            MixAll.printObjectProperties(console, nettyServerConfig);
            System.exit(0);
        }
        //将命令行中的参数封装到namesrvConfig
        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
    
        if (null == namesrvConfig.getRocketmqHome()) {
            System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
            System.exit(-2);
        }
    
        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(lc);
        lc.reset();
        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
    
        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
        //打印
        MixAll.printObjectProperties(log, namesrvConfig);
        MixAll.printObjectProperties(log, nettyServerConfig);
        //创建NameServerController
        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
    
        // remember all configs to prevent discard
        controller.getConfiguration().registerConfig(properties);
    
        return controller;
    }
    

    org.apache.rocketmq.common.namesrv.NamesrvConfig的属性

    		//rocketmq主目录
        private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
        //NameServer存储KV配置属性的持久化路径
        private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
        //nameServer默认配置文件路径
        private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
        private String productEnvName = "center";
        private boolean clusterTest = false;
        //是否支持顺序消息
        private boolean orderMessageEnable = false;
    

    org.apache.rocketmq.remoting.netty.NettyServerConfig属性

    //该值默认会被初始化为9876
    private int listenPort = 8888;
    //Netty业务线程池线程个数
    private int serverWorkerThreads = 8;
    //Netty public任务线程池线程个数
    //Netty网络设计中根据业务类型会创建不同的线程池,比如处理消息发送、消息消费、心跳检测等。如果该业务类型未注册线程池,则由public线程池执行。 
    private int serverCallbackExecutorThreads = 0;
    //IO线程池个数,主要是NameServer、Broker端解析请求、返回相应的线程个数,这类线程主要是处理网路请求的,解析请求包,然后转发到各个业务线程池完成具体的操作,然后将结果返回给调用方
    private int serverSelectorThreads = 3;
    //send oneway消息请求并发读(Broker端参数)
    private int serverOnewaySemaphoreValue = 256;
    //异步消息发送最大并发度)
    private int serverAsyncSemaphoreValue = 64;
    //网络连接最大的空闲时间,默认120s
    private int serverChannelMaxIdleTimeSeconds = 120;
    //网络socket发送缓冲区大小 默认65535
    private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
    //网络接收端缓存区大小 默认 65535
    private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
    //ByteBuffer是否开启缓存
    private boolean serverPooledByteBufAllocatorEnable = true;
    
    /**
     * make make install
     *是否启用Epoll IO模型
     *
     * ../glibc-2.10.1/configure \ --prefix=/usr \ --with-headers=/usr/include \
     * --host=x86_64-linux-gnu \ --build=x86_64-pc-linux-gnu \ --without-gd
     */
    private boolean useEpollNativeSelector = false;
    
  2. 启动NamesrvController

    public static NamesrvController start(final NamesrvController controller) throws Exception {
    
        if (null == controller) {
            throw new IllegalArgumentException("NamesrvController is null");
        }
        //初始化
        boolean initResult = controller.initialize();
        if (!initResult) {
            controller.shutdown();
            System.exit(-3);
        }
        //添加关闭的钩子函数
        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
              	//释放资源
                controller.shutdown();
                return null;
            }
        }));
        //启动
        controller.start();
    
        return controller;
    }
    
    • 初始化

      org.apache.rocketmq.namesrv.NamesrvController#initialize

      public boolean initialize() {
          //加载KV配置
          this.kvConfigManager.load();
          //创建NettyServer网络处理对象
          this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
          this.remotingExecutor =
              Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
          //注册处理器
          this.registerProcessor();
      
          this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
      
              @Override
              public void run() {
                  //扫描不活跃的broker,从元数据删除
                  NamesrvController.this.routeInfoManager.scanNotActiveBroker();
              }
          }, 5, 10, TimeUnit.SECONDS);
      
          this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
              //隔10s打印KV配置信息
              @Override
              public void run() {
                  NamesrvController.this.kvConfigManager.printAllPeriodically();
              }
          }, 1, 10, TimeUnit.MINUTES);
      
          if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
              // Register a listener to reload SslContext
              try {
                  fileWatchService = new FileWatchService(
                      new String[] {
                          TlsSystemConfig.tlsServerCertPath,
                          TlsSystemConfig.tlsServerKeyPath,
                          TlsSystemConfig.tlsServerTrustCertPath
                      },
                      new FileWatchService.Listener() {
                          boolean certChanged, keyChanged = false;
                          @Override
                          public void onChanged(String path) {
                              if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
                                  log.info("The trust certificate changed, reload the ssl context");
                                  reloadServerSslContext();
                              }
                              if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
                                  certChanged = true;
                              }
                              if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
                                  keyChanged = true;
                              }
                              if (certChanged && keyChanged) {
                                  log.info("The certificate and private key changed, reload the ssl context");
                                  certChanged = keyChanged = false;
                                  reloadServerSslContext();
                              }
                          }
                          private void reloadServerSslContext() {
                              ((NettyRemotingServer) remotingServer).loadSslContext();
                          }
                      });
              } catch (Exception e) {
                  log.warn("FileWatchService created error, can't load the certificate dynamically");
              }
          }
      
          return true;
      }
      
    • 关闭钩子函数

      org.apache.rocketmq.namesrv.NamesrvController#shutdown

      public void shutdown() {
          this.remotingServer.shutdown();
          this.remotingExecutor.shutdown();
          this.scheduledExecutorService.shutdown();
      
          if (this.fileWatchService != null) {
              this.fileWatchService.shutdown();
          }
      }
      
    • 启动

4.2.3 路由

NameServer的主要作用是为消息的生产者和消息消费者提供关于主题Topic的路由信息,这就要求NameServer存储路由的基础信息,还要管理Broker节点,包括路由注册、路由删除等。

4.2.3.1 路由元信息

主要维护在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager类中

private final ReadWriteLock lock = new ReentrantReadWriteLock();
//Topic消息队列路由信息,消息发送时根据路由表进行负载均衡 一个brokerName对应一个QueueData
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
//Broker基础信息,包括brokerName、所属集群名称、主备Broker地址
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
//Broker集群信息,存储集群中所有Broker名称
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
//Broker状态信息,NameServer每次收到心跳包是会替换该信息
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
//Broker上的FilterServer列表,用于消息过滤
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

RocketMQ基于订阅发布机制,一个Topic拥有多个消息队列,一个Broker为每一个主题创建4个读队列和4个写队列。多个Broker组成一个集群,集群由相同的多台Broker组成Master-Slave架构,brokerId为0代表Master,大于0为Slave。BrokerLiveInfo中的lastUpdateTimestamp存储上次收到Broker心跳包的时间。

4.2.3.2 路由注册

RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。Broker启动时向集群中所有的NameServer发送心跳信息,每隔30s向集群中所有NameServer发送心跳包,NameServer收到心跳包时会更新brokerLiveTable缓存中BrokerLiveInfo的lastUpdataTimeStamp信息,然后NameServer每隔10s扫描brokerLiveTable,如果连续120S没有收到心跳包,NameServer将移除Broker的路由信息同时关闭Socket连接

  • 发起注册

    见Broker

  • NameServer接受注册

    org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest是NameServer处理网络请求的

    如果请求类型是为REGISTER_BROKER,则说明是注册请求将会调用 org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#registerBroker将请求转发到org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker

    public RegisterBrokerResult registerBroker(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final Channel channel) {
        RegisterBrokerResult result = new RegisterBrokerResult();
        try {
            try {
                //加锁
                this.lock.writeLock().lockInterruptibly();
                //维护clusterAddrTable
                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
                //该集群第一次注册 创建一个brokerNames
                if (null == brokerNames) {
                    brokerNames = new HashSet<String>();
                    this.clusterAddrTable.put(clusterName, brokerNames);
                }
                //brokerNames 中加入该brokerName
                brokerNames.add(brokerName);
    
                boolean registerFirst = false;
    
                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                //第一次注册,则创建brokerData
                if (null == brokerData) {
                    registerFirst = true;
                    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
                    this.brokerAddrTable.put(brokerName, brokerData);
                }
                //更新brokerData
                Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
                //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
                //The same IP:PORT must only have one record in brokerAddrTable 如果当前注册的broker和原始记录的brokerId不一样就说明存在主从切换,把原始记录删除
                Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
                while (it.hasNext()) {
                    Entry<Long, String> item = it.next();
                    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
                        //如果brokerAddr相等 但是brokerId不一样就移除
                        it.remove();
                    }
                }
                //设置新的brokerId和brokerAddr
                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
                //第一次注册或者是老的Addr被移除
                registerFirst = registerFirst || (null == oldAddr);
                //是master 维护topicQueueTable
                if (null != topicConfigWrapper
                    && MixAll.MASTER_ID == brokerId) {
                    //配置更改或者是第一次注册
                    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
                        || registerFirst) {
                        //获取TopicConfig
                        ConcurrentMap<String, TopicConfig> tcTable =
                            topicConfigWrapper.getTopicConfigTable();
                        if (tcTable != null) {
                            //遍历TopicConfig 维护topicQueueTable
                            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                                this.createAndUpdateQueueData(brokerName, entry.getValue());
                            }
                        }
                    }
                }
                //维护brokerLiveTable
                BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                    new BrokerLiveInfo(
                        System.currentTimeMillis(),
                        topicConfigWrapper.getDataVersion(),
                        channel,
                        haServerAddr));
                if (null == prevBrokerLiveInfo) {
                    log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
                }
                //维护filterServerTable
                if (filterServerList != null) {
                    if (filterServerList.isEmpty()) {
                        this.filterServerTable.remove(brokerAddr);
                    } else {
                        this.filterServerTable.put(brokerAddr, filterServerList);
                    }
                }
                //是slave
                if (MixAll.MASTER_ID != brokerId) {
                    //找到对应的master
                    String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
                    if (masterAddr != null) {
                        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
                        if (brokerLiveInfo != null) {
                            //返回master的信息
                            result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
                            result.setMasterAddr(masterAddr);
                        }
                    }
                }
            } finally {
                //释放锁
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("registerBroker Exception", e);
        }
    
        return result;
    }
    

    org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#createAndUpdateQueueData

    private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
        //创建QueueData
        QueueData queueData = new QueueData();
        queueData.setBrokerName(brokerName);
        queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
        queueData.setReadQueueNums(topicConfig.getReadQueueNums());
        queueData.setPerm(topicConfig.getPerm());
        queueData.setTopicSynFlag(topicConfig.getTopicSysFlag());
        //获得topicQueueTable中队列集合
        List<QueueData> queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());
        //topicQueueTable为空,则直接添加queueData到队列集合
        if (null == queueDataList) {
            queueDataList = new LinkedList<QueueData>();
            queueDataList.add(queueData);
            this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList);
            log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
        } else {
            //是否是要添加的队列标识
            boolean addNewOne = true;
    
            Iterator<QueueData> it = queueDataList.iterator();
            while (it.hasNext()) {
                QueueData qd = it.next();
                //判断该topic下的队列中是否存在当前队列 如果存在addNewOne为false
                //如果该topic下的队列中存在该brokerName下的队列 但是非该队列 就删除存在的队列(先删除老的添加最新的)
                if (qd.getBrokerName().equals(brokerName)) {
                    if (qd.equals(queueData)) {
                        addNewOne = false;
                    } else {
                        log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd,
                            queueData);
                        it.remove();
                    }
                }
            }
            //如果是新的队列,则添加队列到queueDataList
            if (addNewOne) {
                queueDataList.add(queueData);
            }
        }
    }
    
4.2.3.3 路由删除

Broker 每隔30s向 NameServer 发送一个心跳包,心跳包包含 BrokerId、Broker地址、Broker名称、Broker所属集群名称、 Broker 关联的FilterServer 列表。但是如果 Broker 宕机,NameServer 无法收到心跳包,此时 NameServer 如何来剔除这些失效的 Broker 呢? NameServer 会每隔10s扫描 brokerLiveTable 状态表,如果 BrokerLive 的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为 Broker 失效,移除该 Broker ,关闭与 Broker 连接,同时更新topicQueueTable 、 brokerAddrTable 、 brokerLiveTable 、 filterServerTable 。

RocketMQ有两个触发点来删除路由信息

  • NameServer定期扫描brokerLiveTable检测上次心跳包与当前系统的时间差,如果时间超过120s,则需要移除broker。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    在NameServer启动的时候会调用org.apache.rocketmq.namesrv.NamesrvController#initialize进行初始化,在initialize会注册一个时间间隔为10s的定时任务

    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
        @Override
        public void run() {
            //每隔10s扫描不活跃的broker,从元数据删除
            NamesrvController.this.routeInfoManager.scanNotActiveBroker();
        }
    }, 5, 10, TimeUnit.SECONDS);
    

    org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#scanNotActiveBroker

    public void scanNotActiveBroker() {
        //获得brokerLiveTable
        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
        //遍历brokerLiveTable
        while (it.hasNext()) {
            Entry<String, BrokerLiveInfo> next = it.next();
            long last = next.getValue().getLastUpdateTimestamp();
            //如果收到心跳包的时间距当时时间是否超过120s
            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
                //关闭连接
                RemotingUtil.closeChannel(next.getValue().getChannel());
                //在brokerLiveTable中移除broker
                it.remove();
                log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
                //维护路由表
                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
            }
        }
    }
    

    org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#onChannelDestroy

    public void onChannelDestroy(String remoteAddr, Channel channel) {
        String brokerAddrFound = null;//第一个使用当前channel的broker地址
        if (channel != null) {
            try {
                try {
                    this.lock.readLock().lockInterruptibly();
                    Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
                        this.brokerLiveTable.entrySet().iterator();
                    while (itBrokerLiveTable.hasNext()) {
                        Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
                        if (entry.getValue().getChannel() == channel) {
                            //找到brokerLiveTable中第一个和当前channel相等的Broker地址
                            brokerAddrFound = entry.getKey();
                            break;
                        }
                    }
                } finally {
                    this.lock.readLock().unlock();
                }
            } catch (Exception e) {
                log.error("onChannelDestroy Exception", e);
            }
        }
    
        if (null == brokerAddrFound) {
            //如果brokerAddrFound没有就用当前的地址
            brokerAddrFound = remoteAddr;
        } else {
            log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
        }
    
        if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
    
            try {
                try {
                    //申请写锁
                    this.lock.writeLock().lockInterruptibly();
                    //移除brokerLiveTable中的broker
                    this.brokerLiveTable.remove(brokerAddrFound);
                    //移除filterServerTable中的broker
                    this.filterServerTable.remove(brokerAddrFound);
                    //维护brokerAddrTable用
                    String brokerNameFound = null;
                    boolean removeBrokerName = false;
                    Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
                        this.brokerAddrTable.entrySet().iterator();
                    //在brokerAddrTable.brokerData.brokerAddrs中找到brokerAddrFound对应的数据并删除
                    //如果在删除后brokerData中brokerAddrs是空就证明该brokerName下已经没有节点,从brokerAddrTable中删除
                    while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
                        BrokerData brokerData = itBrokerAddrTable.next().getValue();
    
                        Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
                        while (it.hasNext()) {
                            Entry<Long, String> entry = it.next();
                            Long brokerId = entry.getKey();
                            String brokerAddr = entry.getValue();
                            if (brokerAddr.equals(brokerAddrFound)) {
                                brokerNameFound = brokerData.getBrokerName();
                                it.remove();
                                log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
                                    brokerId, brokerAddr);
                                break;
                            }
                        }
    
                        if (brokerData.getBrokerAddrs().isEmpty()) {
                            removeBrokerName = true;
                            itBrokerAddrTable.remove();
                            log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
                                brokerData.getBrokerName());
                        }
                    }
                    //如果brokerName对应的brokerAddrTable已经被删除 就把clusterAddrTable中brokerName数据移除
                    if (brokerNameFound != null && removeBrokerName) {
                        Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
                        while (it.hasNext()) {
                            Entry<String, Set<String>> entry = it.next();
                            String clusterName = entry.getKey();
                            Set<String> brokerNames = entry.getValue();
                            boolean removed = brokerNames.remove(brokerNameFound);
                            if (removed) {
                                log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
                                    brokerNameFound, clusterName);
    
                                if (brokerNames.isEmpty()) {
                                    log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
                                        clusterName);
                                    it.remove();
                                }
    
                                break;
                            }
                        }
                    }
                    //如果brokerName对应的brokerAddrTable已经被删除 
                    // 就把topicQueueTable中brokerName的队列都移除
                    //如果该topic下队列已经空了就在topicQueueTable中移除该topic数据
                    if (removeBrokerName) {
                        Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
                            this.topicQueueTable.entrySet().iterator();
                        while (itTopicQueueTable.hasNext()) {
                            Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
                            String topic = entry.getKey();
                            List<QueueData> queueDataList = entry.getValue();
    
                            Iterator<QueueData> itQueueData = queueDataList.iterator();
                            while (itQueueData.hasNext()) {
                                QueueData queueData = itQueueData.next();
                                if (queueData.getBrokerName().equals(brokerNameFound)) {
                                    itQueueData.remove();
                                    log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
                                        topic, queueData);
                                }
                            }
    
                            if (queueDataList.isEmpty()) {
                                itTopicQueueTable.remove();
                                log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
                                    topic);
                            }
                        }
                    }
                } finally {
                    this.lock.writeLock().unlock();
                }
            } catch (Exception e) {
                log.error("onChannelDestroy Exception", e);
            }
        }
    }
    
  • Broker在正常关闭的情况下,会执行unregisterBroker指令

    unregisterBroker方法就是从各个集合中删除该broker对应的数据

    public void unregisterBroker(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId) {
        try {
            try {
                //获取读锁
                this.lock.writeLock().lockInterruptibly();
                //从brokerLiveTable移除brokerAddr数据
                BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddr);
                log.info("unregisterBroker, remove from brokerLiveTable {}, {}",
                    brokerLiveInfo != null ? "OK" : "Failed",
                    brokerAddr
                );
                //从filterServerTable移除brokerAddr数据
                this.filterServerTable.remove(brokerAddr);
    
                boolean removeBrokerName = false;
                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                if (null != brokerData) {
                    //从brokerAddrTable.brokerData.brokerAddrs移除brokerAddr数据
                    String addr = brokerData.getBrokerAddrs().remove(brokerId);
                    log.info("unregisterBroker, remove addr from brokerAddrTable {}, {}",
                        addr != null ? "OK" : "Failed",
                        brokerAddr
                    );
    
                    if (brokerData.getBrokerAddrs().isEmpty()) {
                        //从brokerAddrTable移除数据
                        this.brokerAddrTable.remove(brokerName);
                        log.info("unregisterBroker, remove name from brokerAddrTable OK, {}",
                            brokerName
                        );
    
                        removeBrokerName = true;
                    }
                }
                //从clusterAddrTable移除数据
                if (removeBrokerName) {
                    Set<String> nameSet = this.clusterAddrTable.get(clusterName);
                    if (nameSet != null) {
                        boolean removed = nameSet.remove(brokerName);
                        log.info("unregisterBroker, remove name from clusterAddrTable {}, {}",
                            removed ? "OK" : "Failed",
                            brokerName);
    
                        if (nameSet.isEmpty()) {
                            this.clusterAddrTable.remove(clusterName);
                            log.info("unregisterBroker, remove cluster from clusterAddrTable {}",
                                clusterName
                            );
                        }
                    }
                    this.removeTopicByBrokerName(brokerName);
                }
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("unregisterBroker Exception", e);
        }
    }
    

这两种方式路由删除的方法都是一样的,就是从相关路由表中删除与该broker相关的信息。

4.2.3.4 路由发现

RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不会主动推送给客户端,而是由客户端定时拉取主题最新的路由。

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    //取出请求头
    final GetRouteInfoRequestHeader requestHeader =
        (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
    //调用RouteInfoManager的方法从路由表topicQueueTable、brokerAddrTable、filterServerTable中分别填充TopicRouteData的List<QueueData>、List<BrokerData>、filterServer
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
    //如果找到主题对应的路由信息并且该主题为顺序消息,则从NameServer KVConfig中获取关于顺序消息相关的配置填充路由信息
    if (topicRouteData != null) {
        if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
            String orderTopicConf =
                this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
                    requestHeader.getTopic());
            topicRouteData.setOrderTopicConf(orderTopicConf);
        }

        byte[] content = topicRouteData.encode();
        response.setBody(content);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }

    response.setCode(ResponseCode.TOPIC_NOT_EXIST);
    response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
        + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
    return response;
}

4.3 Broker

BrokerStartup

  • Broker发起注册

    Broker在启动时会调用org.apache.rocketmq.broker.BrokerController#start方法启动Broker

    public void start() throws Exception {
        if (this.messageStore != null) {
            this.messageStore.start();
        }
    
        if (this.remotingServer != null) {
            this.remotingServer.start();
        }
    
        if (this.fastRemotingServer != null) {
            this.fastRemotingServer.start();
        }
    
        if (this.fileWatchService != null) {
            this.fileWatchService.start();
        }
    
        if (this.brokerOuterAPI != null) {
            this.brokerOuterAPI.start();
        }
    
        if (this.pullRequestHoldService != null) {
            this.pullRequestHoldService.start();
        }
    
        if (this.clientHousekeepingService != null) {
            this.clientHousekeepingService.start();
        }
    
        if (this.filterServerManager != null) {
            this.filterServerManager.start();
        }
    
        if (!messageStoreConfig.isEnableDLegerCommitLog()) {
            startProcessorByHa(messageStoreConfig.getBrokerRole());
            handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
        }
    
    
        //注册Broker信息
        this.registerBrokerAll(true, false, true);
        //每隔30s上报Broker信息到NameServer
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
            @Override
            public void run() {
                try {
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
    
        if (this.brokerStatsManager != null) {
            this.brokerStatsManager.start();
        }
    
        if (this.brokerFastFailure != null) {
            this.brokerFastFailure.start();
        }
    
    
    }
    

    其内部会调用org.apache.rocketmq.broker.BrokerController#registerBrokerAll来像NameServer发起注册

    最后会调用org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBrokerAll来注册

    public List<RegisterBrokerResult> registerBrokerAll(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final boolean oneway,
        final int timeoutMills,
        final boolean compressed) {
        //获得所有nameServer地址信息
        final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
        List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
        if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
            //封装请求头
            final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
            requestHeader.setBrokerAddr(brokerAddr);
            requestHeader.setBrokerId(brokerId);
            requestHeader.setBrokerName(brokerName);
            requestHeader.setClusterName(clusterName);
            requestHeader.setHaServerAddr(haServerAddr);
            requestHeader.setCompressed(compressed);
            //封装请求body
            RegisterBrokerBody requestBody = new RegisterBrokerBody();
            requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
            requestBody.setFilterServerList(filterServerList);
            final byte[] body = requestBody.encode(compressed);
            final int bodyCrc32 = UtilAll.crc32(body);
            requestHeader.setBodyCrc32(bodyCrc32);
            final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
            //遍历所有的nameServer 多线程发送注册请求
            for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                          	//发起注册
                            RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                            if (result != null) {
                                registerBrokerResultList.add(result);
                            }
    
                            log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            countDownLatch.countDown();
                        }
                    }
                });
            }
    
            try {
                //等待所有注册结果
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }
    
        return registerBrokerResultList;
    }
    

    org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBroker

    private RegisterBrokerResult registerBroker(
        final String namesrvAddr,
        final boolean oneway,
        final int timeoutMills,
        final RegisterBrokerRequestHeader requestHeader,
        final byte[] body
    ) throws RemotingCommandException, MQBrokerException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException,
        InterruptedException {
        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
        request.setBody(body);
    
        if (oneway) {
            try {
                this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
            } catch (RemotingTooMuchRequestException e) {
                // Ignore
            }
            return null;
        }
        //同步注册
        RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
        assert response != null;
        switch (response.getCode()) {
            case ResponseCode.SUCCESS: {
                RegisterBrokerResponseHeader responseHeader =
                    (RegisterBrokerResponseHeader) response.decodeCommandCustomHeader(RegisterBrokerResponseHeader.class);
                RegisterBrokerResult result = new RegisterBrokerResult();
                result.setMasterAddr(responseHeader.getMasterAddr());
                result.setHaServerAddr(responseHeader.getHaServerAddr());
                if (response.getBody() != null) {
                    result.setKvTable(KVTable.decode(response.getBody(), KVTable.class));
                }
                return result;
            }
            default:
                break;
        }
    
        throw new MQBrokerException(response.getCode(), response.getRemark());
    }
    

4.4 Product

相对于Broker,product是客户端,用来发送消息

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.4.1 启动流程

入口在org.apache.rocketmq.client.producer.DefaultMQProducer#start

public void start() throws MQClientException {
    //重新设置生产者分组
    this.setProducerGroup(withNamespace(this.producerGroup));
    //启动
    this.defaultMQProducerImpl.start();
    if (null != traceDispatcher) {
        try {
            traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
        } catch (MQClientException e) {
            log.warn("trace dispatcher start failed ", e);
        }
    }
}

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start(boolean)

public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            this.serviceState = ServiceState.START_FAILED;
            //配置检查 主要检查组名是否合法
            this.checkConfig();

            if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                //将客户端的instanceName修改为客户端pid
                this.defaultMQProducer.changeInstanceNameToPID();
            }
            //获取客户端实例 保证 MQClientInstance唯一
            this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
            //注册生产者 保证该生产者分组中生产者对象唯一,在一个项目中同一个生产者分组只能有一个生产者
            boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
            if (!registerOK) {
                this.serviceState = ServiceState.CREATE_JUST;
                throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            }

            this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

            if (startFactory) {
                mQClientFactory.start();
            }

            log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                this.defaultMQProducer.isSendMessageWithVIPChannel());
            this.serviceState = ServiceState.RUNNING;
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The producer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }

    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}

org.apache.rocketmq.client.impl.MQClientManager#getAndCreateMQClientInstance(org.apache.rocketmq.client.ClientConfig, org.apache.rocketmq.remoting.RPCHook)

public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
    String clientId = clientConfig.buildMQClientId();
    MQClientInstance instance = this.factoryTable.get(clientId);
    if (null == instance) {
        instance =
            new MQClientInstance(clientConfig.cloneClientConfig(),
                this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
        //保证该客户端只有一个MQClientInstance
        MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
        if (prev != null) {
            instance = prev;
            log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
        } else {
            log.info("Created new MQClientInstance for clientId:[{}]", clientId);
        }
    }

    return instance;
}

整个JVM中只存在一个MQClientManager实例,维护一个MQClientInstance缓存表

private ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable =
    new ConcurrentHashMap<String, MQClientInstance>();

同一个clientId只会创建一个MQClientInstance。

MQClientInstance封装了RocketMQ网络处理API,是消息生产者和消息消费者与NameServer、Broker打交道的网络通

4.4.2 发送消息

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

入口在:org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message)

public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    //校验topic、body是否为空、消息是否达到最大长度
    Validators.checkMessage(msg, this);
    msg.setTopic(withNamespace(msg.getTopic()));
    //调用defaultMQProducerImpl 发送消息
    return this.defaultMQProducerImpl.send(msg);
}

具体实现在org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl

private SendResult sendDefaultImpl(
        Message msg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        this.makeSureStateOK();
        //消息校验
        Validators.checkMessage(msg, this.defaultMQProducer);

        final long invokeID = random.nextLong();
        long beginTimestampFirst = System.currentTimeMillis();
        long beginTimestampPrev = beginTimestampFirst;
        long endTimestamp = beginTimestampFirst;
        //根据topic获得路由信息
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            boolean callTimeout = false;
            MessageQueue mq = null;
            Exception exception = null;
            SendResult sendResult = null;
            //发送次数
            int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
            int times = 0;
            String[] brokersSent = new String[timesTotal];
            for (; times < timesTotal; times++) {
                //第一次发送是null
                String lastBrokerName = null == mq ? null : mq.getBrokerName();
                //根据路由信息和brokerName查询出一个queue
                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                if (mqSelected != null) {
                    mq = mqSelected;
                    brokersSent[times] = mq.getBrokerName();
                    try {
                        //抛能捕获的异常或者在同步发送时sendResult.getSendStatus() != SendStatus.SEND_OK 且打开retryAnotherBrokerWhenNotStoreOK 会重试
                        beginTimestampPrev = System.currentTimeMillis();
                        if (times > 0) {
                            //Reset topic with namespace during resend. 是重试
                            msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
                        }
                        long costTime = beginTimestampPrev - beginTimestampFirst;
                        if (timeout < costTime) {
                            //如果超时直接break 不发送消息
                            callTimeout = true;
                            break;
                        }
                        //发送消息
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                        endTimestamp = System.currentTimeMillis();
                        //发送消息的耗时
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        switch (communicationMode) {
                            case ASYNC:
                                return null;
                            case ONEWAY:
                                return null;
                            case SYNC:
                                if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
                                    if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                                        continue;
                                    }
                                }

                                return sendResult;
                            default:
                                break;
                        }
                    } catch (RemotingException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        continue;
                    } catch (MQClientException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        continue;
                    } catch (MQBrokerException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        switch (e.getResponseCode()) {
                            case ResponseCode.TOPIC_NOT_EXIST:
                            case ResponseCode.SERVICE_NOT_AVAILABLE:
                            case ResponseCode.SYSTEM_ERROR:
                            case ResponseCode.NO_PERMISSION:
                            case ResponseCode.NO_BUYER_ID:
                            case ResponseCode.NOT_IN_CURRENT_UNIT:
                                continue;
                            default:
                                if (sendResult != null) {
                                    return sendResult;
                                }

                                throw e;
                        }
                    } catch (InterruptedException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());

                        log.warn("sendKernelImpl exception", e);
                        log.warn(msg.toString());
                        throw e;
                    }
                } else {
                    break;
                }
            }

            if (sendResult != null) {
                return sendResult;
            }

            String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s",
                times,
                System.currentTimeMillis() - beginTimestampFirst,
                msg.getTopic(),
                Arrays.toString(brokersSent));

            info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);

            MQClientException mqClientException = new MQClientException(info, exception);
            if (callTimeout) {
                throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
            }

            if (exception instanceof MQBrokerException) {
                mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
            } else if (exception instanceof RemotingConnectException) {
                mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION);
            } else if (exception instanceof RemotingTimeoutException) {
                mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT);
            } else if (exception instanceof MQClientException) {
                mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION);
            }

            throw mqClientException;
        }

        List<String> nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList();
        if (null == nsList || nsList.isEmpty()) {
            throw new MQClientException(
                "No name server address, please set it." + FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL), null).setResponseCode(ClientErrorCode.NO_NAME_SERVER_EXCEPTION);
        }

        throw new MQClientException("No route info of this topic, " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
            null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
    }
4.4.2.1 消息校验

org.apache.rocketmq.client.Validators#checkMessage

public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
    throws MQClientException {
    //判断msg是否为空
    if (null == msg) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null");
    }
    // topic
    Validators.checkTopic(msg.getTopic());

    // body 校验消息体是否为空
    if (null == msg.getBody()) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null");
    }
    //校验消息长度是否为0
    if (0 == msg.getBody().length) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero");
    }
    //校验消息长度是否超出最大值
    if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
            "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
    }
}

org.apache.rocketmq.client.Validators#checkTopic

public static void checkTopic(String topic) throws MQClientException {
    //判断topic是否为空
    if (UtilAll.isBlank(topic)) {
        throw new MQClientException("The specified topic is blank", null);
    }
    //判断topic 命名是否合法
    if (!regularExpressionMatcher(topic, PATTERN)) {
        throw new MQClientException(String.format(
            "The specified topic[%s] contains illegal characters, allowing only %s", topic,
            VALID_PATTERN_STR), null);
    }
    //校验topic 长度是否超限
    if (topic.length() > CHARACTER_MAX_LENGTH) {
        throw new MQClientException("The specified topic is longer than topic max length 255.", null);
    }

    //whether the same with system reserved keyword 校验topic是否和关键字重复
    if (topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
        throw new MQClientException(
            String.format("The topic[%s] is conflict with AUTO_CREATE_TOPIC_KEY_TOPIC.", topic), null);
    }
}
4.4.2.2 路由

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#tryToFindTopicPublishInfo

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    //从缓存中获得主题的路由信息
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    //路由信息为空,则从NameServer获取路由
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }

    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        //路由信息为空,则从NameServer获取路由
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer(java.lang.String, boolean, org.apache.rocketmq.client.producer.DefaultMQProducer)

public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
                                                      DefaultMQProducer defaultMQProducer) {
        try {
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
                try {
                    TopicRouteData topicRouteData;
                    if (isDefault && defaultMQProducer != null) {
                        //获取默认主题的信息
                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                                1000 * 3);
                        if (topicRouteData != null) {
                            for (QueueData data : topicRouteData.getQueueDatas()) {
                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                                data.setReadQueueNums(queueNums);
                                data.setWriteQueueNums(queueNums);
                            }
                        }
                    } else {
                        //获取对应主题的信息
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
                    if (topicRouteData != null) {
                        TopicRouteData old = this.topicRouteTable.get(topic);
                        //路由信息是否已经更改
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);
                        if (!changed) {
                            //是否需要更改topic对应的路由信息
                            //生产者中topic对应的队列是否为空或者size是0
                            //消费者中是否没有订阅
                            changed = this.isNeedUpdateTopicRouteInfo(topic);
                        } else {
                            log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
                        }

                        if (changed) {
                            //更新路由信息
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();

                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }

                            // Update Pub info 更新生产者信息
                            {
                                //将topicRouteData转换为发布队列
                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
                                publishInfo.setHaveTopicRouterInfo(true);
                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQProducerInner> entry = it.next();
                                    MQProducerInner impl = entry.getValue();
                                    if (impl != null) {
                                        //生产者不为空时,更新publishInfo信息
                                        impl.updateTopicPublishInfo(topic, publishInfo);
                                    }
                                }
                            }

                            // Update sub info 更新消费者信息
                            {
                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQConsumerInner> entry = it.next();
                                    MQConsumerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }
                            log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    } else {
                        log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic);
                    }
                } catch (Exception e) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
                        log.warn("updateTopicRouteInfoFromNameServer Exception", e);
                    }
                } finally {
                    this.lockNamesrv.unlock();
                }
            } else {
                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", LOCK_TIMEOUT_MILLIS);
            }
        } catch (InterruptedException e) {
            log.warn("updateTopicRouteInfoFromNameServer Exception", e);
        }

        return false;
    }

org.apache.rocketmq.client.impl.factory.MQClientInstance#topicRouteData2TopicPublishInfo

public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
    //创建TopicPublishInfo对象
    TopicPublishInfo info = new TopicPublishInfo();
    //关联topicRoute
    info.setTopicRouteData(route);
    //如果指定了Topic的Queue的发送顺序
    if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) {
        //解析配置,创建消息队列
        String[] brokers = route.getOrderTopicConf().split(";");
        for (String broker : brokers) {
            String[] item = broker.split(":");
            int nums = Integer.parseInt(item[1]);
            for (int i = 0; i < nums; i++) {
                MessageQueue mq = new MessageQueue(topic, item[0], i);
                info.getMessageQueueList().add(mq);
            }
        }
        // 设置Topic是有序的(消息的发送顺序按配置来)
        info.setOrderTopic(true);
    } else {
        //非顺序消息更新TopicPublishInfo
        List<QueueData> qds = route.getQueueDatas();
        Collections.sort(qds);
        //遍历topic队列信息 找到每个QueueData的BrokerData
        for (QueueData qd : qds) {
            //是否是写队列
            if (PermName.isWriteable(qd.getPerm())) {
                BrokerData brokerData = null;
                //遍历写队列Broker
                for (BrokerData bd : route.getBrokerDatas()) {
                    //根据名称获得读队列对应的Broker
                    if (bd.getBrokerName().equals(qd.getBrokerName())) {
                        brokerData = bd;
                        break;
                    }
                }

                if (null == brokerData) {
                    continue;
                }
                // 如果BrokerData中没有Master节点id,可能Master挂了,此时不处理消息
                if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
                    continue;
                }
                //封装TopicPublishInfo写队列
                for (int i = 0; i < qd.getWriteQueueNums(); i++) {
                    MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
                    info.getMessageQueueList().add(mq);
                }
            }
        }
        // 设置Topic消息发送是无序的
        info.setOrderTopic(false);
    }
    //返回TopicPublishInfo对象
    return info;
}
4.4.2.3 选择队列

入口在org.apache.rocketmq.client.latency.MQFaultStrategy#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            //开启故障延迟等待
            try {
                //如果是重试就会向下轮询找到lastBrokerName中的队列,如果找不到就会从其他BrokerName中随机找一个

                //单线程 获取本线程本次发送的QueueId 每次递增
                int index = tpInfo.getSendWhichQueue().getAndIncrement();

                //这个for在重试的时候会找到一个lastBrokerName中的队列
                //有四个队列 1.1、1.2(同一个brokerName)、2.1、2.2 (同一个brokerName)开启sendLatencyFaultEnable
                //第一次发消息 通过这个for使用队列1.1发送成功
                //第二次发消息 通过for使用队列1.2 发送失败
                //然后进行重试 会找到2.1这个队列  isAvailable校验可以过
                //但是if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName)) 过不了,无法返回mq
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    //验证该队列是否可用,某个Broker发送失败则在一定时间内不使用其mq
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }
                //如果lastBrokerName不可用 或者lastBrokerName中队列都已经发送过消息了 走下面逻辑

                //从规避的Broker中选择一个可用的Broker
                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                //获得Broker的写队列集合
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    //获得一个队列,指定broker和队列ID并返回
                    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);
            }
            //发生异常就轮询出一个MessageQueue
            return tpInfo.selectOneMessageQueue();
        }
        //未开启故障延迟等待
        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

org.apache.rocketmq.client.impl.producer.TopicPublishInfo#selectOneMessageQueue(java.lang.String)

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName == null) {
            //轮询出一个MessageQueue
            return selectOneMessageQueue();
        } else {
            int index = this.sendWhichQueue.getAndIncrement();
            //轮询出一个不是lastBrokerName的MessageQueue
            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;
                }
            }
            //如果找不到就直接轮询出一个(因为上面sendWhichQueue加1了,所以在这里面调用selectOneMessageQueue会跳过一个MessageQueue)
            return selectOneMessageQueue();
        }
    }

org.apache.rocketmq.client.impl.producer.TopicPublishInfo#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);
}
4.4.2.4 发送消息

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl

private SendResult sendKernelImpl(final Message msg,
                                  final MessageQueue mq,
                                  final CommunicationMode communicationMode,
                                  final SendCallback sendCallback,
                                  final TopicPublishInfo topicPublishInfo,
                                  final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    long beginStartTime = System.currentTimeMillis();
    //获得broker master的网络地址信息
    String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    if (null == brokerAddr) {
        //没有找到从NameServer更新broker网络地址信息
        tryToFindTopicPublishInfo(mq.getTopic());
        brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    }

    SendMessageContext context = null;
    if (brokerAddr != null) {
        brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);

        byte[] prevBody = msg.getBody();
        try {
            //for MessageBatch,ID has been set in the generating process
            //不是批消息则为消息设置唯一ID
            if (!(msg instanceof MessageBatch)) {
                MessageClientIDSetter.setUniqID(msg);
            }

            boolean topicWithNamespace = false;
            if (null != this.mQClientFactory.getClientConfig().getNamespace()) {
                msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());
                topicWithNamespace = true;
            }

            int sysFlag = 0;
            boolean msgBodyCompressed = false;
            //消息大小超过4K,启用消息压缩
            if (this.tryToCompressMessage(msg)) {
                sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
                msgBodyCompressed = true;
            }
            //如果是事务消息,设置消息标记MessageSysFlag.TRANSACTION_PREPARED_TYPE
            final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
            if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
                sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
            }

            if (hasCheckForbiddenHook()) {
                CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
                checkForbiddenContext.setNameSrvAddr(this.defaultMQProducer.getNamesrvAddr());
                checkForbiddenContext.setGroup(this.defaultMQProducer.getProducerGroup());
                checkForbiddenContext.setCommunicationMode(communicationMode);
                checkForbiddenContext.setBrokerAddr(brokerAddr);
                checkForbiddenContext.setMessage(msg);
                checkForbiddenContext.setMq(mq);
                checkForbiddenContext.setUnitMode(this.isUnitMode());
                this.executeCheckForbiddenHook(checkForbiddenContext);
            }
            //如果注册了消息发送钩子函数,在执行消息发送前的增强逻辑
            if (this.hasSendMessageHook()) {
                context = new SendMessageContext();
                context.setProducer(this);
                context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
                context.setCommunicationMode(communicationMode);
                context.setBornHost(this.defaultMQProducer.getClientIP());
                context.setBrokerAddr(brokerAddr);
                context.setMessage(msg);
                context.setMq(mq);
                context.setNamespace(this.defaultMQProducer.getNamespace());
                String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
                if (isTrans != null && isTrans.equals("true")) {
                    context.setMsgType(MessageType.Trans_Msg_Half);
                }

                if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
                    context.setMsgType(MessageType.Delay_Msg);
                }
                this.executeSendMessageHookBefore(context);
            }
            //构建requestHeader
            SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
            requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
            requestHeader.setTopic(msg.getTopic());
            //默认创建主题Key
            requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
            //默认队列树
            requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
            //队列ID
            requestHeader.setQueueId(mq.getQueueId());
            //消息系统标记
            requestHeader.setSysFlag(sysFlag);
            //消息发送时间
            requestHeader.setBornTimestamp(System.currentTimeMillis());
            //消息标记
            requestHeader.setFlag(msg.getFlag());
            //消息扩展信息
            requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
            //消息重试次数
            requestHeader.setReconsumeTimes(0);
            requestHeader.setUnitMode(this.isUnitMode());
            //是否是批量消息等
            requestHeader.setBatch(msg instanceof MessageBatch);
            if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
                if (reconsumeTimes != null) {
                    requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
                    MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
                }

                String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
                if (maxReconsumeTimes != null) {
                    requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
                    MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
                }
            }

            SendResult sendResult = null;
            switch (communicationMode) {
                case ASYNC:
                    Message tmpMessage = msg;
                    boolean messageCloned = false;
                    if (msgBodyCompressed) {
                        //If msg body was compressed, msgbody should be reset using prevBody.
                        //Clone new message using commpressed message body and recover origin massage.
                        //Fix bug:https://github.com/apache/rocketmq-externals/issues/66
                        tmpMessage = MessageAccessor.cloneMessage(msg);
                        messageCloned = true;
                        msg.setBody(prevBody);
                    }

                    if (topicWithNamespace) {
                        if (!messageCloned) {
                            tmpMessage = MessageAccessor.cloneMessage(msg);
                            messageCloned = true;
                        }
                        msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace()));
                    }

                    long costTimeAsync = System.currentTimeMillis() - beginStartTime;
                    if (timeout < costTimeAsync) {
                        throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
                    }
                    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                        brokerAddr,
                        mq.getBrokerName(),
                        tmpMessage,
                        requestHeader,
                        timeout - costTimeAsync,
                        communicationMode,
                        sendCallback,
                        topicPublishInfo,
                        this.mQClientFactory,
                        this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
                        context,
                        this);
                    break;
                case ONEWAY:
                case SYNC:
                    long costTimeSync = System.currentTimeMillis() - beginStartTime;
                    if (timeout < costTimeSync) {
                        throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
                    }
                    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                        brokerAddr,
                        mq.getBrokerName(),
                        msg,
                        requestHeader,
                        timeout - costTimeSync,
                        communicationMode,
                        context,
                        this);
                    break;
                default:
                    assert false;
                    break;
            }
            //如果注册了钩子函数,则发送完毕后执行钩子函数
            if (this.hasSendMessageHook()) {
                context.setSendResult(sendResult);
                this.executeSendMessageHookAfter(context);
            }

            return sendResult;
        } catch (RemotingException e) {
            if (this.hasSendMessageHook()) {
                context.setException(e);
                this.executeSendMessageHookAfter(context);
            }
            throw e;
        } catch (MQBrokerException e) {
            if (this.hasSendMessageHook()) {
                context.setException(e);
                this.executeSendMessageHookAfter(context);
            }
            throw e;
        } catch (InterruptedException e) {
            if (this.hasSendMessageHook()) {
                context.setException(e);
                this.executeSendMessageHookAfter(context);
            }
            throw e;
        } finally {
            msg.setBody(prevBody);
            msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace()));
        }
    }

    throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
}
4.4.2.5 故障延迟等待

在发送完消息后 会调用一个updateFaultItem来判断是否要熔断

org.apache.rocketmq.client.latency.MQFaultStrategy#updateFaultItem

public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
        if (this.sendLatencyFaultEnable) {
            //开启故障延迟等待
            //计算broker规避的时长
            long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
            //更新该FaultItem规避时长
            this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
        }
    }

org.apache.rocketmq.client.latency.MQFaultStrategy#computeNotAvailableDuration

private long computeNotAvailableDuration(final long currentLatency) {
    //遍历latencyMax 找到第一个比currentLatency小的latencyMax值 然后根据下标去notAvailableDuration取出时间
    for (int i = latencyMax.length - 1; i >= 0; i--) {
        if (currentLatency >= latencyMax[i])
            return this.notAvailableDuration[i];
    }

    return 0;
}

org.apache.rocketmq.client.latency.LatencyFaultToleranceImpl#updateFaultItem

@Override
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
    FaultItem old = this.faultItemTable.get(name);
    if (null == old) {
        //为空新建faultItem对象,设置规避时长和结束时间
        final FaultItem faultItem = new FaultItem(name);
        faultItem.setCurrentLatency(currentLatency);
        faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);

        old = this.faultItemTable.putIfAbsent(name, faultItem);
        if (old != null) {
            //如果保存的时候发现有了就覆盖
            old.setCurrentLatency(currentLatency);
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    } else {
        //更新规避时长和结束时间
        old.setCurrentLatency(currentLatency);
        old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
    }
}

4.4.3 消息批量发送

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

批量消息发送是将同一个主题的多条消息一起打包发送到消息服务端,减少网络调用次数,提高网络传输效率。

并不是在同一批次中发送的消息数量越多越好,其判断依据是单条消息的长度,如果单条消息内容比较长,则打包多条消息发送会影响其他线程发送消息的响应时间,并且单批次消息总长度不能超过DefaultMQProducer#maxMessageSize。

批量消息发送要解决的问题是如何将这些消息编码以便服务端能够正确解码出每条消息的消息内容

入口在org.apache.rocketmq.client.producer.DefaultMQProducer#send(java.util.Collection<org.apache.rocketmq.common.message.Message>)

@Override
public SendResult send(
    Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    //压缩消息集合成一条消息,然后发送出去
    return this.defaultMQProducerImpl.send(batch(msgs));
}

和普通消息比只是多了一个batch方法,改方法会把多条消息压缩成一条

org.apache.rocketmq.client.producer.DefaultMQProducer#batch

private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
    MessageBatch msgBatch;
    try {
        //将集合消息封装到MessageBatch
        msgBatch = MessageBatch.generateFromList(msgs);
        for (Message message : msgBatch) {
            //遍历消息集合,检查消息合法性,设置消息ID,设置Topic
            Validators.checkMessage(message, this);
            MessageClientIDSetter.setUniqID(message);
            message.setTopic(withNamespace(message.getTopic()));
        }
        //压缩消息,设置消息body
        msgBatch.setBody(msgBatch.encode());
    } catch (Exception e) {
        throw new MQClientException("Failed to initiate the MessageBatch", e);
    }
    //设置msgBatch的topic
    msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
    return msgBatch;
}

4.5 消息储存

4.5.1 消息储存流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

入口在org.apache.rocketmq.store.DefaultMessageStore#putMessage

public PutMessageResult putMessage(MessageExtBrokerInner msg) {
    //关闭状态不能写入
    if (this.shutdown) {
        log.warn("message store has shutdown, so putMessage is forbidden");
        return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
    }
    //如果当前节点是slave 不需要写入
    if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
        long value = this.printTimes.getAndIncrement();
        if ((value % 50000) == 0) {
            log.warn("message store is slave mode, so putMessage is forbidden ");
        }

        return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
    }
    //如果当前正在写入,则拒绝写入
    if (!this.runningFlags.isWriteable()) {
        long value = this.printTimes.getAndIncrement();
        if ((value % 50000) == 0) {
            log.warn("message store is not writeable, so putMessage is forbidden " + this.runningFlags.getFlagBits());
        }

        return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
    } else {
        this.printTimes.set(0);
    }
    //如果topic的长度超限就拒绝写入
    if (msg.getTopic().length() > Byte.MAX_VALUE) {
        log.warn("putMessage message topic length too long " + msg.getTopic().length());
        return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
    }
    //如果消息属性长度超过限制就拒绝写入
    if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) {
        log.warn("putMessage message properties length too long " + msg.getPropertiesString().length());
        return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
    }
    //如果系统PageCache缓存不可用就拒绝写入
    if (this.isOSPageCacheBusy()) {
        return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
    }

    long beginTime = this.getSystemClock().now();
    //将消息写入CommitLog文件
    PutMessageResult result = this.commitLog.putMessage(msg);

    long eclipseTime = this.getSystemClock().now() - beginTime;
    if (eclipseTime > 500) {
        log.warn("putMessage not in lock eclipse time(ms)={}, bodyLength={}", eclipseTime, msg.getBody().length);
    }
    this.storeStatsService.setPutMessageEntireTimeMax(eclipseTime);

    if (null == result || !result.isOk()) {
        this.storeStatsService.getPutMessageFailedTimes().incrementAndGet();
    }

    return result;
}

org.apache.rocketmq.store.CommitLog#putMessage

public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    // Set the storage time 记录消息存储时间
    msg.setStoreTimestamp(System.currentTimeMillis());
    // Set the message body BODY CRC (consider the most appropriate setting
    // on the client)
    msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
    // Back to Results
    AppendMessageResult result = null;
    //获取存储服务
    StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();

    String topic = msg.getTopic();
    int queueId = msg.getQueueId();

    final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
    //判断是否是非事务或者是事务的提交
    if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
        || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
        // Delay Delivery 如果是延迟消息
        if (msg.getDelayTimeLevel() > 0) {
            if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
            }
            //把延迟消息的主题设置成 SCHEDULE_TOPIC_XXXX
            topic = ScheduleMessageService.SCHEDULE_TOPIC;
            //根据DelayTimeLevel获取对应的队列
            queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

            // Backup real topic, queueId 把原始的主题和队列放到消息属性中
            MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
            MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
            msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
            //保存修改
            msg.setTopic(topic);
            msg.setQueueId(queueId);
        }
    }

    long eclipseTimeInLock = 0;
    MappedFile unlockMappedFile = null;
    //获取最后一个mappedFile
    MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();

    putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
    try {
        long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
        this.beginTimeInLock = beginLockTimestamp;

        // Here settings are stored timestamp, in order to ensure an orderly
        // global 更新消息存储时间
        msg.setStoreTimestamp(beginLockTimestamp);
        //判断如果mappedFile如果为空或者已满,创建新的mappedFile文件
        if (null == mappedFile || mappedFile.isFull()) {
            mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
        }
        //如果创建失败,直接返回
        if (null == mappedFile) {
            log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
            beginTimeInLock = 0;
            return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
        }
        //写入消息到mappedFile中
        result = mappedFile.appendMessage(msg, this.appendMessageCallback);
        switch (result.getStatus()) {
            case PUT_OK:
                break;
            case END_OF_FILE:
                unlockMappedFile = mappedFile;
                // Create a new file, re-write the message
                mappedFile = this.mappedFileQueue.getLastMappedFile(0);
                if (null == mappedFile) {
                    // XXX: warn and notify me
                    log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
                }
                result = mappedFile.appendMessage(msg, this.appendMessageCallback);
                break;
            case MESSAGE_SIZE_EXCEEDED:
            case PROPERTIES_SIZE_EXCEEDED:
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
            case UNKNOWN_ERROR:
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
            default:
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
        }

        eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
        beginTimeInLock = 0;
    } finally {
        putMessageLock.unlock();
    }

    if (eclipseTimeInLock > 500) {
        log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result);
    }

    if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
        this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
    }

    PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);

    // Statistics
    storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
    storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());
    //刷盘
    handleDiskFlush(result, putMessageResult, msg);
    //同步处理HA 主从同步
    handleHA(result, putMessageResult, msg);

    return putMessageResult;
}

org.apache.rocketmq.store.MappedFile#appendMessagesInner

public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
    assert messageExt != null;
    assert cb != null;
    //获得文件的写入指针
    int currentPos = this.wrotePosition.get();
    //如果指针小于文件大小 才可以写入
    if (currentPos < this.fileSize) {
        //通过writeBuffer.slice()创建一个与MappedFile共享的内存区,并设置position为当前指针
        ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
        byteBuffer.position(currentPos);
        AppendMessageResult result = null;
        if (messageExt instanceof MessageExtBrokerInner) {
            //通过回调方法写入
            result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
        } else if (messageExt instanceof MessageExtBatch) {
            result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
        } else {
            return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
        }
        this.wrotePosition.addAndGet(result.getWroteBytes());
        this.storeTimestamp = result.getStoreTimestamp();
        return result;
    }
    log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
    return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}

org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback#doAppend(long, java.nio.ByteBuffer, int, org.apache.rocketmq.store.MessageExtBrokerInner)

public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,
    final MessageExtBrokerInner msgInner) {
    // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET <br>

    // PHY OFFSET 文件写入位置
    long wroteOffset = fileFromOffset + byteBuffer.position();

    this.resetByteBuffer(hostHolder, 8);
    //设置消息ID
    String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);

    // Record ConsumeQueue information 获得该消息在消息队列中的偏移量
    keyBuilder.setLength(0);
    keyBuilder.append(msgInner.getTopic());
    keyBuilder.append('-');
    keyBuilder.append(msgInner.getQueueId());
    String key = keyBuilder.toString();
    Long queueOffset = CommitLog.this.topicQueueTable.get(key);
    if (null == queueOffset) {
        queueOffset = 0L;
        CommitLog.this.topicQueueTable.put(key, queueOffset);
    }

    // Transaction messages that require special handling 判断是否是事务消息
    final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag());
    switch (tranType) {
        // Prepared and Rollback message is not consumed, will not enter the
        // consumer queuec
        case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
        case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
            queueOffset = 0L;
            break;
        case MessageSysFlag.TRANSACTION_NOT_TYPE:
        case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
        default:
            break;
    }

    /**
     * Serialize message 获得消息属性长度
     */
    final byte[] propertiesData =
        msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);

    final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;

    if (propertiesLength > Short.MAX_VALUE) {
        log.warn("putMessage message properties length too long. length={}", propertiesData.length);
        return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
    }
    //获得消息主题大小
    final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
    final int topicLength = topicData.length;
    获得消息体大小
    final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
    //计算消息总长度
    final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);

    // Exceeds the maximum message 消息长度不能超过4M
    if (msgLen > this.maxMessageSize) {
        CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
            + ", maxMessageSize: " + this.maxMessageSize);
        return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
    }

    // Determines whether there is sufficient free space 如果没有足够的存储空间则新创建CommitLog文件
    if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
        this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
        // 1 TOTALSIZE
        this.msgStoreItemMemory.putInt(maxBlank);
        // 2 MAGICCODE
        this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
        // 3 The remaining space may be any value
        // Here the length of the specially set maxBlank 将消息存储到ByteBuffer中,返回AppendMessageResult
        final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
        byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
        return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
            queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
    }

    // Initialization of storage space
    this.resetByteBuffer(msgStoreItemMemory, msgLen);
    // 1 TOTALSIZE
    this.msgStoreItemMemory.putInt(msgLen);
    // 2 MAGICCODE
    this.msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE);
    // 3 BODYCRC
    this.msgStoreItemMemory.putInt(msgInner.getBodyCRC());
    // 4 QUEUEID
    this.msgStoreItemMemory.putInt(msgInner.getQueueId());
    // 5 FLAG
    this.msgStoreItemMemory.putInt(msgInner.getFlag());
    // 6 QUEUEOFFSET
    this.msgStoreItemMemory.putLong(queueOffset);
    // 7 PHYSICALOFFSET
    this.msgStoreItemMemory.putLong(fileFromOffset + byteBuffer.position());
    // 8 SYSFLAG
    this.msgStoreItemMemory.putInt(msgInner.getSysFlag());
    // 9 BORNTIMESTAMP
    this.msgStoreItemMemory.putLong(msgInner.getBornTimestamp());
    // 10 BORNHOST
    this.resetByteBuffer(hostHolder, 8);
    this.msgStoreItemMemory.put(msgInner.getBornHostBytes(hostHolder));
    // 11 STORETIMESTAMP
    this.msgStoreItemMemory.putLong(msgInner.getStoreTimestamp());
    // 12 STOREHOSTADDRESS
    this.resetByteBuffer(hostHolder, 8);
    this.msgStoreItemMemory.put(msgInner.getStoreHostBytes(hostHolder));
    //this.msgBatchMemory.put(msgInner.getStoreHostBytes());
    // 13 RECONSUMETIMES
    this.msgStoreItemMemory.putInt(msgInner.getReconsumeTimes());
    // 14 Prepared Transaction Offset
    this.msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset());
    // 15 BODY
    this.msgStoreItemMemory.putInt(bodyLength);
    if (bodyLength > 0)
        this.msgStoreItemMemory.put(msgInner.getBody());
    // 16 TOPIC
    this.msgStoreItemMemory.put((byte) topicLength);
    this.msgStoreItemMemory.put(topicData);
    // 17 PROPERTIES
    this.msgStoreItemMemory.putShort((short) propertiesLength);
    if (propertiesLength > 0)
        this.msgStoreItemMemory.put(propertiesData);

    final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
    // Write messages to the queue buffer
    byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);

    AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId,
        msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);

    switch (tranType) {
        case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
        case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
            break;
        case MessageSysFlag.TRANSACTION_NOT_TYPE:
        case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
            // The next update ConsumeQueue information 事务提交消息 更新消息队列偏移量
            CommitLog.this.topicQueueTable.put(key, ++queueOffset);
            break;
        default:
            break;
    }
    return result;
}

4.5.2 储存文件说明

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • commitLog

    消息存储目录,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,这里的偏移量也就是文件大小的字节表示,这样的好处是只要我们知道了消息的偏移量就能很快知道该消息在哪个文件里了。假设某消息物理偏移量是1073741830,则相对的偏移量是6(6 = 1073741830 - 1073741824),于是判断出该消息位于第二个commitLog文件上

    • 顺序写入,一个文件写满再写下一个
    • 消息是先写入到mappedFile这个内存映射文件中,然后根据刷盘策略写到硬盘中,最后进行主从同步
    • header+body格式,其中header是定长的,记录了消息的长度
    • 读取消息时先解析出header,从中获取到消息长度,接着读取消息体
  • consumerqueue

    消息消费队列存储目录,ConsumerQueue是CommitLog的一个索引文件。文件里记录着某个消息投递到某个队列里的位置信息,消息是存在CommitLog文件里的,从ConsumerQueue获取消息的偏移量然后再根据偏移量去CommitLog里进行查询

    顺序写,Kafka 在分区级别实现文件顺序写,所以随着topic、分区变大时会对性能有一定影响;而RocketMQ不分主题一律顺序写入commitlog 文件,所以性能比较稳定。这里有个问题,既然RocmetMQ里的commitlog没有区分队列,那消息是如何定位的呢?答案是在comsummerQueue里,comsummerQueue是按队列组织的:/topic/{queue}

    • 每个topic默认为4个队列
    • 单个队列下最大可有30W个条目,每个ConsumeQueue文件(条目)大小约5.72M
    • consumequeue文件采取定长设计,共20个字节(8字节储存消息在CommitLog中的起始物理偏移量offset,4字节储存消息大小size,8字节储存消息Tag的HashCode值)。使之可以使用类似访问数组的方式快速定位数据。
      ConsumerQueue不存消息的tag而是存tag的hashCode主要是为了保证条目的固定长度。
  • index

    消息索引文件存储目录,文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故rocketmq的索引文件其底层实现为hash索引

    IndexFile由三部分组成:

    • IndexHeader

      索引文件由索引文件头IndexHeader。头文件由40个字节的数据组成,主要内容有

    //8位 该索引文件的第一个消息(Message)的存储时间(落盘时间)
    this.byteBuffer.putLong(beginTimestampIndex, this.beginTimestamp.get());
    //8位 该索引文件的最后一个消息(Message)的存储时间(落盘时间)
    this.byteBuffer.putLong(endTimestampIndex, this.endTimestamp.get());
    //8位 该索引文件第一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量(可以通过该物理偏移直接获取到该消息)
    this.byteBuffer.putLong(beginPhyoffsetIndex, this.beginPhyOffset.get());
    //8位 该索引文件最后一个消息(Message)的在CommitLog(消息存储文件)的物理位置偏移量
    this.byteBuffer.putLong(endPhyoffsetIndex, this.endPhyOffset.get());
    //4位 该索引文件目前的hash slot的个数
    this.byteBuffer.putInt(hashSlotcountIndex, this.hashSlotCount.get());
    //4位 索引文件目前的索引个数
    this.byteBuffer.putInt(indexCountIndex, this.indexCount.get());

    
    
    
    - Slot
    
    ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fdingkaige%2Ftypora-images%2Fimg%2F20231201110327.png&pos_id=img-41LEch8z-1721269493984)
    
    紧临着IndexHeader,默认slot是500万个,每个固定大小为4byte,slot中存着一个int值,表示当前slot下最新的一个index序号。
    在计算对应的槽位时,会先算出MessageKey的hashCode,然后用Hashcode对slot的总数进行取模,决定该消息key的位置,slot的总数默认是500W个。
    只要取hash就必然面临着hash冲突的问题,indexfile也是采用链表结构来解决hash冲突(注意,500w个slot很大,另外冲突的情形一般不会很大,所以没有使用红黑树)。slot的值对应当前slot下最新的那个index的序号,index中存储了当前slot下、当前index的前一个index序号,这就把slot下的所有index链起来了
    
    ```java
    //slot的数据存放位置 40 + keyHash %(500W)* 4
    int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
    //Slot Table 4字节
    //记录该slot当前index,如果hash冲突(即absSlotPos一致)作为下一次该slot新增的前置index
    this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());
    
    • index

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      //Index Linked list
      //topic+message key的hash值
      this.mappedByteBuffer.putInt(absIndexPos, keyHash);
      //消息在CommitLog的物理文件地址, 可以直接查询到该消息(索引的核心机制)
      this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
      //消息的落盘时间与header里的beginTimestamp的差值(为了节省存储空间,如果直接存message的落盘时间就得8bytes)
      this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
      //9、记录该slot上一个index
      //hash冲突处理的关键之处, 相同hash值上一个消息索引的index(如果当前消息索引是该hash值的第一个索引,则prevIndex=0, 也是消息索引查找时的停止条件),每个slot位置的第一个消息的prevIndex就是0的
      this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
      
  • config:

    运行期间一些配置信息

  • abort

    启动时创建,正常关闭时删除,如果存在该文件寿命Broker非正常关闭

  • checkpoint

    文件检查点,存储CommitLog文件最后一次刷盘时间戳、consumerquueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。

4.5.3 储存文件内存映射

RocketMQ通过使用内存映射文件提高IO访问性能,无论是CommitLog、ConsumerQueue还是IndexFile,单个文件都被设计为固定长度,如果一个文件写满以后再创建一个新文件,文件名就为该文件第一条消息对应的全局物理偏移量

4.5.3.1 MappedFileQueue

常用的方法有如下几个

  • getMappedFileByTime

    根据存储时间查询MappedFile

    public MappedFile getMappedFileByTime(final long timestamp) {
        //复制一份mappedFiles
        Object[] mfs = this.copyMappedFiles(0);
    
        if (null == mfs)
            return null;
        //遍历MappedFile文件数组 找到最后修改时间大于timestamp的第一个mappedFile
        for (int i = 0; i < mfs.length; i++) {
            MappedFile mappedFile = (MappedFile) mfs[i];
            if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
                return mappedFile;
            }
        }
        //默认返回最后一个mappedFile
        return (MappedFile) mfs[mfs.length - 1];
    }
    
  • findMappedFileByOffset

    public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
        try {
            //获得第一个MappedFile文件
            MappedFile firstMappedFile = this.getFirstMappedFile();
            //获得最后一个MappedFile文件
            MappedFile lastMappedFile = this.getLastMappedFile();
            //第一个文件和最后一个文件均不为空,则进行处理
            if (firstMappedFile != null && lastMappedFile != null) {
                if (offset < firstMappedFile.getFileFromOffset() || offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
                    //没有找到
                    LOG_ERROR.warn("Offset not matched. Request offset: {}, firstOffset: {}, lastOffset: {}, mappedFileSize: {}, mappedFiles count: {}",
                        offset,
                        firstMappedFile.getFileFromOffset(),
                        lastMappedFile.getFileFromOffset() + this.mappedFileSize,
                        this.mappedFileSize,
                        this.mappedFiles.size());
                } else {
                    //获得文件索引
                    int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
                    MappedFile targetFile = null;
                    try {
                        //根据索引返回目标文件
                        targetFile = this.mappedFiles.get(index);
                    } catch (Exception ignored) {
                    }
                    //如果offset落在targetFile中就返回
                    if (targetFile != null && offset >= targetFile.getFileFromOffset()
                        && offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
                        return targetFile;
                    }
                    //如果没找到就遍历一遍mappedFiles 看看有没有对应的mappedFile
                    for (MappedFile tmpMappedFile : this.mappedFiles) {
                        if (offset >= tmpMappedFile.getFileFromOffset()
                            && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
                            return tmpMappedFile;
                        }
                    }
                }
                //如果没找到要返回第一个mappedFile就返回
                if (returnFirstOnNotFound) {
                    return firstMappedFile;
                }
            }
        } catch (Exception e) {
            log.error("findMappedFileByOffset Exception", e);
        }
    
        return null;
    }
    
  • getMinOffset

    获取存储文件最小偏移量

    public long getMinOffset() {
    
        if (!this.mappedFiles.isEmpty()) {
            try {
                return this.mappedFiles.get(0).getFileFromOffset();
            } catch (IndexOutOfBoundsException e) {
                //continue;
            } catch (Exception e) {
                log.error("getMinOffset has exception.", e);
            }
        }
        return -1;
    }
    
  • getMaxOffset

    获取存储文件最大偏移量

    public long getMaxOffset() {
        MappedFile mappedFile = getLastMappedFile();
        if (mappedFile != null) {
            return mappedFile.getFileFromOffset() + mappedFile.getReadPosition();
        }
        return 0;
    }
    
  • getMaxWrotePosition

    返回存储文件当前写指针

    public long getMaxWrotePosition() {
        MappedFile mappedFile = getLastMappedFile();
        if (mappedFile != null) {
            return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
        }
        return 0;
    }
    
4.5.3.2 MappedFile
4.5.3.2.1 MappedFile初始化

transientStorePoolEnable=true 为 true 表示数据先存储到堆外内存,然后通过 Commit 线程将数据提交到内存映射Buffer中,再通过 Flush线程将内存映射 Buffer 中数据持久化磁盘

  • 未开启 transientStorePoolEnable

    org.apache.rocketmq.store.MappedFile#init(java.lang.String, int),写数据的时候是直接写到目标物理文件对应的内存映射中

  • 开启 transientStorePoolEnable

    比未开启多初始化了writeBuffer、transientStorePool

    org.apache.rocketmq.store.MappedFile#init(java.lang.String, int, org.apache.rocketmq.store.TransientStorePool)

4.5.3.2.2 提交commit

提交数据到FileChannel,commitLeastPages为本次提交最小的页数,如果待提交数据不满commitLeastPages,则不执行本次提交操作。如果writeBuffer如果为空,直接返回writePosition指针,无需执行commit操作。commit操作主体是writeBuffer。

org.apache.rocketmq.store.MappedFile#commit

public int commit(final int commitLeastPages) {
    if (writeBuffer == null) {
        //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
        //如果writeBuffer如果为空,直接返回writePosition指针,无需执行commit操作
        return this.wrotePosition.get();
    }
    //判断是否满足提交条件
    if (this.isAbleToCommit(commitLeastPages)) {
        if (this.hold()) {
            commit0(commitLeastPages);
            this.release();
        } else {
            log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
        }
    }

    // All dirty data has been committed to FileChannel. 所有数据提交后,清空缓冲区
    if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
        this.transientStorePool.returnBuffer(writeBuffer);
        this.writeBuffer = null;
    }

    return this.committedPosition.get();
}
  • isAbleToCommit

    判断是否执行commit操作,如果文件已满返回true;如果commitLeastpages大于0,则比较writePosition与上一次提交的指针commitPosition的差值,除以OS_PAGE_SIZE得到当前脏页的数量,如果大于commitLeastPages则返回true,如果commitLeastpages小于0表示只要存在脏页就提交。

    org.apache.rocketmq.store.MappedFile#isAbleToCommit

    protected boolean isAbleToCommit(final int commitLeastPages) {
        //已经刷盘指针
        int flush = this.committedPosition.get();
        //文件写指针
        int write = this.wrotePosition.get();
        //写满就行commit操
        if (this.isFull()) {
            return true;
        }
    
        if (commitLeastPages > 0) {
            //脏页数量大于等于commitLeastPages就commit
            return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
        }
        //存在脏页 就commit
        return write > flush;
    }
    
  • commit0

    具体提交的实现,首先创建WriteBuffer共享缓存区,然后将新创建的position回退到上一次提交的位置(commitPosition),设置limit为wrotePosition(当前最大有效数据指针),然后把commitPosition到wrotePosition的数据写入到FileChannel中,然后更新committedPosition指针为wrotePosition。commit的作用就是将MappedFile的writeBuffer中数据提交到文件通道FileChannel中。

    org.apache.rocketmq.store.MappedFile#commit0

    protected void commit0(final int commitLeastPages) {
        //写指针
        int writePos = this.wrotePosition.get();
        //上次提交指针
        int lastCommittedPosition = this.committedPosition.get();
        //判断是否有要提交的数据
        if (writePos - this.committedPosition.get() > 0) {
            try {
                //复制共享内存区域
                ByteBuffer byteBuffer = writeBuffer.slice();
                //设置提交位置是上次提交位置
                byteBuffer.position(lastCommittedPosition);
                //最大提交数量
                byteBuffer.limit(writePos);
                //设置fileChannel位置为上次提交位置
                this.fileChannel.position(lastCommittedPosition);
                //将lastCommittedPosition到writePos的数据复制到FileChannel中
                this.fileChannel.write(byteBuffer);
                //重置提交位置
                this.committedPosition.set(writePos);
            } catch (Throwable e) {
                log.error("Error occurred when commit data to FileChannel.", e);
            }
        }
    }
    
4.5.3.2.3 刷盘flush

刷写磁盘,直接调用MappedByteBuffer或fileChannel的force方法将内存中的数据持久化到磁盘

如果writeBuffer为空,就代表数据都已经提交了,设置flushedPosition为wrotePosition。

如果writeBuffer不为空,则设置flushedPosition应该等于上一次的commit指针

org.apache.rocketmq.store.MappedFile#flush

public int flush(final int flushLeastPages) {
    //判断是否达到刷盘条件
    if (this.isAbleToFlush(flushLeastPages)) {
        //加锁,同步刷盘
        if (this.hold()) {
            //获得读指针
            int value = getReadPosition();

            try {
                //We only append data to fileChannel or mappedByteBuffer, never both.
                if (writeBuffer != null || this.fileChannel.position() != 0) {
                    //有未提交的数据 从fileChannel刷新到磁盘
                    this.fileChannel.force(false);
                } else {
                    //没有未提交的数据全提交到mappedByteBuffer了就从mmap刷新数据到磁盘
                    this.mappedByteBuffer.force();
                }
            } catch (Throwable e) {
                log.error("Error occurred when force data to disk.", e);
            }

            this.flushedPosition.set(value);
            //释放锁
            this.release();
        } else {
            log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
            this.flushedPosition.set(getReadPosition());
        }
    }
    return this.getFlushedPosition();
}
4.5.3.2.4 getReadPosition

获取当前文件最大可读指针。如果writeBuffer为空,则直接返回当前的写指针;如果writeBuffer不为空,则返回上一次提交的指针。在MappedFile设置中,只有提交了的数据(写入到MappedByteBuffer或FileChannel中的数据)才是安全的数据

public int getReadPosition() {
    return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get();
}
4.5.3.2.5 selectMappedBuffer

查找pos到当前最大可读之间的数据

public SelectMappedBufferResult selectMappedBuffer(int pos) {
    //获得最大可读指针
    int readPosition = getReadPosition();
    //pos小于最大可读指针,并且大于0
    if (pos < readPosition && pos >= 0) {
        if (this.hold()) {
            //复制mappedByteBuffer读共享区
            ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
            //设置读指针位置
            byteBuffer.position(pos);
            //获得可读范围
            int size = readPosition - pos;
            //设置最大可读范围
            ByteBuffer byteBufferNew = byteBuffer.slice();
            byteBufferNew.limit(size);
            return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
        }
    }

    return null;
}
4.5.3.2.6 shutdown

MappedFile文件销毁的实现方法为public boolean destory(long intervalForcibly),intervalForcibly表示拒绝被销毁的最大存活时间。其会调用shutdown释放资源

public void shutdown(final long intervalForcibly) {
    if (this.available) {
        //关闭MapedFile
        this.available = false;
        //设置当前关闭时间戳
        this.firstShutdownTimestamp = System.currentTimeMillis();
        //释放资源
        this.release();
    } else if (this.getRefCount() > 0) {
        if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
            this.refCount.set(-1000 - this.getRefCount());
            this.release();
        }
    }
}
4.5.3.3 TransientStorePool

短暂的存储池。RocketMQ单独创建一个ByteBuffer内存缓存池,用来临时存储数据,数据先写入该内存映射中,然后由commit线程定时将数据从该内存复制到与目标物理文件对应的内存映射中。RocketMQ引入该机制主要的原因是提供一种内存锁定,将当前堆外内存一直锁定在内存中,避免被进程将内存交换到磁盘。

public void init() {
    //创建poolSize个堆外内存
    for (int i = 0; i < poolSize; i++) {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);

        final long address = ((DirectBuffer) byteBuffer).address();
        Pointer pointer = new Pointer(address);
        //使用com.sun.jna.Library类库将该批内存锁定,避免被置换到交换区,提高存储性能
        LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));

        availableBuffers.offer(byteBuffer);
    }
}

4.5.4 实时更新消息消费队列与索引文件

消息消费队列文件、消息属性索引文件都是基于CommitLog文件构建的,当消息存储在CommitLog文件后ConsumerQueue、IndexFile需要及时更新,否则消息无法及时被消费,根据消息属性查找消息也会出现较大延迟。RocketMQ通过开启一个线程ReputMessageService准实时转发CommitLog文件更新事件,相应的任务处理器根据转发的消息及时更新ConsumerQueue、IndexFile文件。

截屏2023-11-29 16.53.26

BrokerStartup在启动的时候会调用org.apache.rocketmq.broker.BrokerController#start,其内部会调用this.messageStore.start()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

org.apache.rocketmq.store.DefaultMessageStore#start

//设置CommitLog内存中最大偏移量
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
//启动
this.reputMessageService.start();

具体实现是org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService,其run方法如下:

@Override
public void run() {
    DefaultMessageStore.log.info(this.getServiceName() + " service started");
    //每隔1毫秒就尝试推送消息到消息消费队列和索引文件
    while (!this.isStopped()) {
        try {
            Thread.sleep(1);
            //推送消息到消息消费队列和索引文件
            this.doReput();
        } catch (Exception e) {
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

    DefaultMessageStore.log.info(this.getServiceName() + " service end");
}

具体逻辑在org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService#doReput

private void doReput() {
            if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
                log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
                        this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
                this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
            }
            for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {

                if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
                        && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
                    break;
                }
                //从commitLog中取出数据
                SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
                if (result != null) {
                    try {
                        this.reputFromOffset = result.getStartOffset();
                        //从result中循环遍历消息,一次读一条,创建DispatherRequest对象
                        for (int readSize = 0; readSize < result.getSize() && doNext; ) {
                            DispatchRequest dispatchRequest =
                                    DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
                            int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();

                            if (dispatchRequest.isSuccess()) {
                                if (size > 0) {
                                    //逻辑处理
                                    DefaultMessageStore.this.doDispatch(dispatchRequest);
                                    //长轮询逻辑,如果不是slave节点且开启长轮询,就唤醒拉取消息的等待线程
                                    if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
                                            && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
                                        DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
                                                dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
                                                dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
                                                dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
                                    }

                                    this.reputFromOffset += size;
                                    readSize += size;
                                    if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
                                        DefaultMessageStore.this.storeStatsService
                                                .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
                                        DefaultMessageStore.this.storeStatsService
                                                .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
                                                .addAndGet(dispatchRequest.getMsgSize());
                                    }
                                } else if (size == 0) {
                                    this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
                                    readSize = result.getSize();
                                }
                            } else if (!dispatchRequest.isSuccess()) {

                                if (size > 0) {
                                    log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
                                    this.reputFromOffset += size;
                                } else {
                                    doNext = false;
                                    // If user open the dledger pattern or the broker is master node,
                                    // it will not ignore the exception and fix the reputFromOffset variable
                                    if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
                                            DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
                                        log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
                                                this.reputFromOffset);
                                        this.reputFromOffset += result.getSize() - readSize;
                                    }
                                }
                            }
                        }
                    } finally {
                        result.release();
                    }
                } else {
                    doNext = false;
                }
            }
        }

org.apache.rocketmq.store.DefaultMessageStore#doDispatch

 public void doDispatch(DispatchRequest req) {
        //dispatcherList 在构造函数中添加了 CommitLogDispatcherBuildConsumeQueue、CommitLogDispatcherBuildIndex
        for (CommitLogDispatcher dispatcher : this.dispatcherList) {
            dispatcher.dispatch(req);
        }
    }
4.5.4.1 转发到ConsumerQueue
class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {

    @Override
    public void dispatch(DispatchRequest request) {
        final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
        switch (tranType) {
            case MessageSysFlag.TRANSACTION_NOT_TYPE:
            case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                //普通消息和事务提交消息 进行消息分发
                DefaultMessageStore.this.putMessagePositionInfo(request);
                break;
            case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                break;
        }
    }
}

org.apache.rocketmq.store.DefaultMessageStore#putMessagePositionInfo

public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    //获取消费队列,如果不存在会进行初始化
    ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    //消费队列分发消息 
    cq.putMessagePositionInfoWrapper(dispatchRequest);
}

putMessagePositionInfoWrapper最终会调用org.apache.rocketmq.store.ConsumeQueue#putMessagePositionInfo来把消息加到消费队列

private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
    final long cqOffset) {

    if (offset + size <= this.maxPhysicOffset) {
        log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
        return true;
    }
    //依次将消息偏移量、消息长度、tag写入到ByteBuffer中
    this.byteBufferIndex.flip();
    this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
    this.byteBufferIndex.putLong(offset);
    this.byteBufferIndex.putInt(size);
    this.byteBufferIndex.putLong(tagsCode);
    //消费队列和消息队列是一对一的,而且消费队列中每条数据是定长CQ_STORE_UNIT_SIZE
    //expectLogicOffset就是消息在消费队列中的Offset,cqOffset是消费队列逻辑offset
    final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE;
    //获得内存映射文件
    MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
    if (mappedFile != null) {

        if (mappedFile.isFirstCreateInQueue() && cqOffset != 0 && mappedFile.getWrotePosition() == 0) {
            this.minLogicOffset = expectLogicOffset;
            this.mappedFileQueue.setFlushedWhere(expectLogicOffset);
            this.mappedFileQueue.setCommittedWhere(expectLogicOffset);
            this.fillPreBlank(mappedFile, expectLogicOffset);
            log.info("fill pre blank space " + mappedFile.getFileName() + " " + expectLogicOffset + " "
                + mappedFile.getWrotePosition());
        }

        if (cqOffset != 0) {
            long currentLogicOffset = mappedFile.getWrotePosition() + mappedFile.getFileFromOffset();

            if (expectLogicOffset < currentLogicOffset) {
                log.warn("Build  consume queue repeatedly, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
                    expectLogicOffset, currentLogicOffset, this.topic, this.queueId, expectLogicOffset - currentLogicOffset);
                return true;
            }

            if (expectLogicOffset != currentLogicOffset) {
                LOG_ERROR.warn(
                    "[BUG]logic queue order maybe wrong, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
                    expectLogicOffset,
                    currentLogicOffset,
                    this.topic,
                    this.queueId,
                    expectLogicOffset - currentLogicOffset
                );
            }
        }
        this.maxPhysicOffset = offset + size;
        //将消息追加到内存映射文件,异步刷盘
        return mappedFile.appendMessage(this.byteBufferIndex.array());
    }
    return false;
}
4.5.4.1 转发到Index

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

org.apache.rocketmq.store.DefaultMessageStore.CommitLogDispatcherBuildIndex

class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {

    @Override
    public void dispatch(DispatchRequest request) {
        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
            //追加索引文件
            DefaultMessageStore.this.indexService.buildIndex(request);
        }
    }
}

org.apache.rocketmq.store.index.IndexService#putKey

private IndexFile putKey(IndexFile indexFile, DispatchRequest msg, String idxKey) {
    for (boolean ok = indexFile.putKey(idxKey, msg.getCommitLogOffset(), msg.getStoreTimestamp()); !ok; ) {
        log.warn("Index file [" + indexFile.getFileName() + "] is full, trying to create another one");

        indexFile = retryGetAndCreateIndexFile();
        if (null == indexFile) {
            return null;
        }
        //在indexFile中构建索引,indexFile是一个map结构
        ok = indexFile.putKey(idxKey, msg.getCommitLogOffset(), msg.getStoreTimestamp());
    }

    return indexFile;
}

4.5.5 消息队列和索引文件恢复

由于RocketMQ存储首先将消息全量存储在CommitLog文件中,然后异步生成转发任务更新ConsumerQueue和Index文件。如果消息成功存储到CommitLog文件中,转发任务未成功执行,此时消息服务器Broker由于某个原因宕机,导致CommitLog、ConsumerQueue、IndexFile文件数据不一致。如果不加以人工修复的话,会有一部分消息即便在CommitLog中文件中存在,但由于没有转发到ConsumerQueue,这部分消息将永远复发被消费者消费外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.5.5.1 存储文件加载流程

判断上一次是否异常退出。实现机制是Broker在启动时创建abort文件,在退出时通过JVM钩子函数删除abort文件。如果下次启动时存在abort文件。说明Broker时异常退出的,CommitLog与ConsumerQueue数据有可能不一致,需要进行修复。

org.apache.rocketmq.store.DefaultMessageStore#load

public boolean load() {
    boolean result = true;

    try {
        //判断临时文件是否存在 如果不存在就是正常退出
        boolean lastExitOK = !this.isTempFileExist();
        log.info("last shutdown {}", lastExitOK ? "normally" : "abnormally");
        //加载延时队列
        if (null != scheduleMessageService) {
            result = result && this.scheduleMessageService.load();
        }

        // load Commit Log 加载CommitLog文件
        result = result && this.commitLog.load();

        // load Consume Queue 加载消费队列文件
        result = result && this.loadConsumeQueue();

        if (result) {
            //加载存储监测点,监测点主要记录CommitLog文件、ConsumerQueue文件、Index索引文件的刷盘点
            this.storeCheckpoint =
                new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));
            //加载index文件
            this.indexService.load(lastExitOK);
            //根据Broker是否异常退出,执行不同的恢复策略
            this.recover(lastExitOK);

            log.info("load over, and the max phy offset = {}", this.getMaxPhyOffset());
        }
    } catch (Exception e) {
        log.error("load exception", e);
        result = false;
    }

    if (!result) {
        //加载失败就关闭资源退出
        this.allocateMappedFileService.shutdown();
    }

    return result;
}
4.5.5.2 加载CommitLog

org.apache.rocketmq.store.CommitLog#load

public boolean load() {
  	//加载
    boolean result = this.mappedFileQueue.load();
    log.info("load commit log " + (result ? "OK" : "Failed"));
    return result;
}

org.apache.rocketmq.store.MappedFileQueue#load

public boolean load() {
    //文件目录
    File dir = new File(this.storePath);
    //获得文件数组
    File[] files = dir.listFiles();
    if (files != null) {
        // ascending order 排序
        Arrays.sort(files);
        //遍历文件
        for (File file : files) {
            //如果文件大小和配置文件不一致,退出
            if (file.length() != this.mappedFileSize) {
                log.warn(file + "\t" + file.length()
                    + " length not matched message store config value, please check it manually");
                return false;
            }

            try {
                //创建映射文件
                MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);

                mappedFile.setWrotePosition(this.mappedFileSize);
                mappedFile.setFlushedPosition(this.mappedFileSize);
                mappedFile.setCommittedPosition(this.mappedFileSize);
                //将映射文件添加到队列
                this.mappedFiles.add(mappedFile);
                log.info("load " + file.getPath() + " OK");
            } catch (IOException e) {
                log.error("load file " + file + " error", e);
                return false;
            }
        }
    }

    return true;
}
4.5.5.3 加载ConsumeQueue

org.apache.rocketmq.store.DefaultMessageStore#loadConsumeQueue

private boolean loadConsumeQueue() {
    //消费队列目录
    File dirLogic = new File(StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()));
    //消费队列目录下的主题目录
    File[] fileTopicList = dirLogic.listFiles();
    if (fileTopicList != null) {
        //遍历主题目录
        for (File fileTopic : fileTopicList) {
            //获得目录名称,即topic名称
            String topic = fileTopic.getName();
            //主题下消费队列文件
            File[] fileQueueIdList = fileTopic.listFiles();
            if (fileQueueIdList != null) {
                //遍历消费队列文件
                for (File fileQueueId : fileQueueIdList) {
                    int queueId;
                    try {
                        //文件名称即队列ID
                        queueId = Integer.parseInt(fileQueueId.getName());
                    } catch (NumberFormatException e) {
                        continue;
                    }
                    //创建消费队列
                    ConsumeQueue logic = new ConsumeQueue(
                        topic,
                        queueId,
                        StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()),
                        this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(),
                        this);
                    //添加到consumeQueueTable中
                    this.putConsumeQueue(topic, queueId, logic);
                    if (!logic.load()) {
                        return false;
                    }
                }
            }
        }
    }

    log.info("load logics queue all over, OK");

    return true;
}
4.5.5.4 加载Index

org.apache.rocketmq.store.index.IndexService#load

public boolean load(final boolean lastExitOK) {
    //索引文件目录
    File dir = new File(this.storePath);
    File[] files = dir.listFiles();
    if (files != null) {
        // ascending order 排序
        Arrays.sort(files);
        //遍历索引文件
        for (File file : files) {
            try {
                //构建及加载索引文件
                IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0);
                f.load();

                if (!lastExitOK) {
                    //如果是异常退出 索引文件上次刷盘时间大于检测点时间戳
                    if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint()
                        .getIndexMsgTimestamp()) {
                        //删除(有恢复逻辑)
                        f.destroy(0);
                        continue;
                    }
                }
                //将索引文件添加到队列
                log.info("load index file OK, " + f.getFileName());
                this.indexFileList.add(f);
            } catch (IOException e) {
                log.error("load file {} error", file, e);
                return false;
            } catch (NumberFormatException e) {
                log.error("load file {} error", file, e);
            }
        }
    }

    return true;
}
4.5.5.5 文件恢复

org.apache.rocketmq.store.DefaultMessageStore#recover

private void recover(final boolean lastExitOK) {
    //获得消费队列中最大的物理偏移量
    long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();

    if (lastExitOK) {
        //正常恢复
        this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
    } else {
        //异常恢复
        this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
    }
    //在CommitLog中保存每个消息消费队列当前的存储逻辑偏移量
    //恢复ConsumerQueue后,将在CommitLog实例中保存每个消息队列当前的逻辑偏移量
    //这也是CommitLog不仅存储主题、消息队列ID、还存储了消费队列的关键所在。
    this.recoverTopicQueueTable();
}
4.5.5.5.1 正常恢复

org.apache.rocketmq.store.CommitLog#recoverNormally

public void recoverNormally(long maxPhyOffsetOfConsumeQueue) {
    boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover();
    final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
    if (!mappedFiles.isEmpty()) {
        // Began to recover from the last third file
        // Broker正常停止再重启时,从倒数第三个开始恢复,如果不足3个文件,则从第一个文件开始恢复。
        int index = mappedFiles.size() - 3;
        if (index < 0)
            index = 0;

        MappedFile mappedFile = mappedFiles.get(index);
        ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
        long processOffset = mappedFile.getFileFromOffset();
        //代表当前已校验通过的offset
        long mappedFileOffset = 0;
        while (true) {
            //查找消息
            DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
            int size = dispatchRequest.getMsgSize();
            // Normal data 查找结果为true,并且消息长度大于0,表示消息正确.mappedFileOffset向前移动本消息长度
            if (dispatchRequest.isSuccess() && size > 0) {
                mappedFileOffset += size;
            }
            // Come the end of the file, switch to the next file Since the
            // return 0 representatives met last hole,
            // this can not be included in truncate offset
            //如果查找结果为true且消息长度等于0,表示已到该文件末尾
            //如果还有下一个文件,则重置processOffset和MappedFileOffset重复查找下一个文件,否则跳出循环。
            else if (dispatchRequest.isSuccess() && size == 0) {
                index++;
                if (index >= mappedFiles.size()) {
                    // Current branch can not happen
                    log.info("recover last 3 physics file over, last mapped file " + mappedFile.getFileName());
                    break;
                } else {
                    //取出每个文件
                    mappedFile = mappedFiles.get(index);
                    byteBuffer = mappedFile.sliceByteBuffer();
                    processOffset = mappedFile.getFileFromOffset();
                    mappedFileOffset = 0;
                    log.info("recover next physics file, " + mappedFile.getFileName());
                }
            }
            // Intermediate file read error 查找结果为false,表明该文件未填满所有消息,跳出循环,结束循环
            else if (!dispatchRequest.isSuccess()) {
                log.info("recover physics file end, " + mappedFile.getFileName());
                break;
            }
        }
        //更新MappedFileQueue的flushedWhere和committedWhere指针
        processOffset += mappedFileOffset;
        this.mappedFileQueue.setFlushedWhere(processOffset);
        this.mappedFileQueue.setCommittedWhere(processOffset);
        //删除offset之后的所有文件(消息不能完整的解析出来,数据有问题)
        this.mappedFileQueue.truncateDirtyFiles(processOffset);

        // Clear ConsumeQueue redundant data
        //如果消费队列中的偏移量大于processOffset 就删除消费队列中的多余的数据
        if (maxPhyOffsetOfConsumeQueue >= processOffset) {
            log.warn("maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset);
            this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
        }
    } else {
        // Commitlog case files are deleted
        log.warn("The commitlog files are deleted, and delete the consume queue files");
        this.mappedFileQueue.setFlushedWhere(0);
        this.mappedFileQueue.setCommittedWhere(0);
        this.defaultMessageStore.destroyLogics();
    }
}

org.apache.rocketmq.store.MappedFileQueue#truncateDirtyFiles

public void truncateDirtyFiles(long offset) {
    List<MappedFile> willRemoveFiles = new ArrayList<MappedFile>();
    //遍历文件
    for (MappedFile file : this.mappedFiles) {
        //文件尾部的偏移量
        long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
        //文件尾部的偏移量大于offset
        if (fileTailOffset > offset) {
            //offset大于文件的起始偏移量
            if (offset >= file.getFileFromOffset()) {
                //更新wrotePosition、committedPosition、flushedPosistion
                file.setWrotePosition((int) (offset % this.mappedFileSize));
                file.setCommittedPosition((int) (offset % this.mappedFileSize));
                file.setFlushedPosition((int) (offset % this.mappedFileSize));
            } else {
                //offset小于文件的起始偏移量,说明该文件是有效文件后面创建的,释放mappedFile占用内存,删除文件
                file.destroy(1000);
                willRemoveFiles.add(file);
            }
        }
    }

    this.deleteExpiredFile(willRemoveFiles);
}
4.5.5.5.2 异常恢复

Broker异常停止文件恢复的实现为org.apache.rocketmq.store.CommitLog#recoverAbnormally。异常文件恢复步骤与正常停止文件恢复流程基本相同,其主要差别有二个

  • 正常停止默认从倒数第三个文件开始进行恢复,而异常停止则需要从最后一个文件往前走,找到第一个消息存储正常的文件,然后开始恢复。
  • 会调用doDispatch进行消费队列和index的加载

org.apache.rocketmq.store.CommitLog#recoverAbnormally

public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) {
    // recover by the minimum time stamp
    boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover();
    final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
    if (!mappedFiles.isEmpty()) {
        // Looking beginning to recover from which file
        int index = mappedFiles.size() - 1;
        MappedFile mappedFile = null;
        //从最后一个MappedFile开始
        for (; index >= 0; index--) {
            mappedFile = mappedFiles.get(index);
            //判断消息文件是否是正常文件 如果是就跳出循环
            if (this.isMappedFileMatchedRecover(mappedFile)) {
                log.info("recover from this mapped file " + mappedFile.getFileName());
                break;
            }
        }
        //根据索引取出mappedFile文件
        if (index < 0) {
            index = 0;
            mappedFile = mappedFiles.get(index);
        }
        //验证消息的合法性,并将消息转发到消息消费队列和索引文件
        ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
        long processOffset = mappedFile.getFileFromOffset();
        long mappedFileOffset = 0;
        while (true) {
            DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
            int size = dispatchRequest.getMsgSize();

            if (dispatchRequest.isSuccess()) {
                // Normal data
                //查找结果为true,并且消息长度大于0,表示消息正确

                if (size > 0) {
                    //.mappedFileOffset向前移动本消息长度
                    mappedFileOffset += size;

                    if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) {
                        if (dispatchRequest.getCommitLogOffset() < this.defaultMessageStore.getConfirmOffset()) {
                            //构建消费队列和index
                            this.defaultMessageStore.doDispatch(dispatchRequest);
                        }
                    } else {
                        this.defaultMessageStore.doDispatch(dispatchRequest);
                    }
                }
                // Come the end of the file, switch to the next file
                // Since the return 0 representatives met last hole, this can
                // not be included in truncate offset
                //如果查找结果为true且消息长度等于0,表示已到该文件末尾
                //如果还有下一个文件,则重置processOffset和MappedFileOffset重复查找下一个文件,否则跳出循环。
                else if (size == 0) {
                    index++;
                    if (index >= mappedFiles.size()) {
                        // The current branch under normal circumstances should
                        // not happen
                        log.info("recover physics file over, last mapped file " + mappedFile.getFileName());
                        break;
                    } else {
                        mappedFile = mappedFiles.get(index);
                        byteBuffer = mappedFile.sliceByteBuffer();
                        processOffset = mappedFile.getFileFromOffset();
                        mappedFileOffset = 0;
                        log.info("recover next physics file, " + mappedFile.getFileName());
                    }
                }
            } else {
                log.info("recover physics file end, " + mappedFile.getFileName() + " pos=" + byteBuffer.position());
                break;
            }
        }

        processOffset += mappedFileOffset;
        //更新MappedFileQueue的flushedWhere和committedWhere指针
        this.mappedFileQueue.setFlushedWhere(processOffset);
        this.mappedFileQueue.setCommittedWhere(processOffset);
        //删除offset之后的所有文件
        this.mappedFileQueue.truncateDirtyFiles(processOffset);

        // Clear ConsumeQueue redundant data
        if (maxPhyOffsetOfConsumeQueue >= processOffset) {
            //如果消费队列中的便宜量大于processOffset 就删除消费队列中的多余的数据
            log.warn("maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset);
            this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
        }
    }
    // Commitlog case files are deleted
    else {
        //未找到mappedFile,重置flushWhere、committedWhere都为0,销毁消息队列文件
        log.warn("The commitlog files are deleted, and delete the consume queue files");
        this.mappedFileQueue.setFlushedWhere(0);
        this.mappedFileQueue.setCommittedWhere(0);
        this.defaultMessageStore.destroyLogics();
    }
}
4.5.5.5.3 保存消费队列逻辑偏移量

恢复ConsumerQueue后,将在CommitLog实例中保存每个消息队列当前的逻辑偏移量,这也是CommitLog不仅存储主题、消息队列ID、还存储了消费队列的关键所在。

org.apache.rocketmq.store.DefaultMessageStore#recoverTopicQueueTable

public void recoverTopicQueueTable() {
    HashMap<String/* topic-queueid */, Long/* offset */> table = new HashMap<String, Long>(1024);
    //CommitLog最小偏移量
    long minPhyOffset = this.commitLog.getMinOffset();
    //遍历消费队列
    for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
        for (ConsumeQueue logic : maps.values()) {
            String key = logic.getTopic() + "-" + logic.getQueueId();
            table.put(key, logic.getMaxOffsetInQueue());
            logic.correctMinOffset(minPhyOffset);
        }
    }
    //将消费队列逻辑偏移量保存在CommitLog中
    this.commitLog.setTopicQueueTable(table);
}

4.5.6 刷盘

RocketMQ的存储是基于JDK NIO的内存映射机制(MappedByteBuffer)的,消息存储首先将消息追加到内存,再根据配置的刷盘策略在不同时间进行刷写磁盘。

BrokerStartup在启动的时候会调用org.apache.rocketmq.broker.BrokerController#start,其内部会调用this.messageStore.start(),其内部会调用this.commitLog.start();来启动commitLog(开启刷盘线程和提交线程)

org.apache.rocketmq.store.CommitLog#start

public void start() {
        //刷盘线程 如果是同步刷盘是GroupCommitService,异步刷盘是FlushRealTimeService
        this.flushCommitLogService.start();
        //提交线程
        if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            this.commitLogService.start();
        }
    }

在发送消息时会调用org.apache.rocketmq.store.CommitLog#handleDiskFlush执行刷盘逻辑

public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
        // Synchronization flush broker开启同步刷盘
        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            //刷盘服务
            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
            //该消息是否是同步刷盘 默认同步
            if (messageExt.isWaitStoreMsgOK()) {
                //封装刷盘请求
                GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
                //提交刷盘请求
                service.putRequest(request);
                //线程阻塞5秒,等待刷盘结束
                boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                if (!flushOK) {
                    log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
                        + " client address: " + messageExt.getBornHostString());
                    putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
                }
            } else {
                service.wakeup();
            }
        }
        // Asynchronous flush
        else {
            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                flushCommitLogService.wakeup();
            } else {
                commitLogService.wakeup();
            }
        }
    }
4.5.6.1 同步刷盘

消息追加到内存后,立即将数据刷写到磁盘文件

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同步刷盘的方法为

org.apache.rocketmq.store.CommitLog.GroupCommitService#run

public void run() {
    CommitLog.log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            //线程等待10ms
            this.waitForRunning(10);
            //执行提交
            this.doCommit();
        } catch (Exception e) {
            CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

    // Under normal circumstances shutdown, wait for the arrival of the
    // request, and then flush
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        CommitLog.log.warn("GroupCommitService Exception, ", e);
    }

    synchronized (this) {
        this.swapRequests();
    }

    this.doCommit();

    CommitLog.log.info(this.getServiceName() + " service end");
}

org.apache.rocketmq.store.CommitLog.GroupCommitService#doCommit

private void doCommit() {
    //加锁
    synchronized (this.requestsRead) {
        if (!this.requestsRead.isEmpty()) {
            //遍历requestsRead
            for (GroupCommitRequest req : this.requestsRead) {
                // There may be a message in the next file, so a maximum of
                // two times the flush
                boolean flushOK = false;
                //尝试2次
                for (int i = 0; i < 2 && !flushOK; i++) {
                    //是否已经刷过了
                    flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
                    //刷盘
                    if (!flushOK) {
                        CommitLog.this.mappedFileQueue.flush(0);
                    }
                }
                //唤醒发送消息客户端
                req.wakeupCustomer(flushOK);
            }
            //更新刷盘监测点
            long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
            if (storeTimestamp > 0) {
                CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
            }

            this.requestsRead.clear();
        } else {
            // Because of individual messages is set to not sync flush, it
            // will come to this process
            CommitLog.this.mappedFileQueue.flush(0);
        }
    }
}
4.5.6.2 异步刷盘

在消息追加到内存后,立即返回给消息发送端。如果开启transientStorePoolEnable,RocketMQ会单独申请一个与目标物理文件(commitLog)同样大小的堆外内存,该堆外内存将使用内存锁定,确保不会被置换到虚拟内存中去,消息首先追加到堆外内存,然后提交到物理文件的内存映射中,然后刷写到磁盘。如果未开启transientStorePoolEnable,消息直接追加到物理文件的内存映射中,然后刷写到磁盘中。

开启transientStorePoolEnable后异步刷盘步骤:

  1. 将消息直接追加到ByteBuffer(堆外内存)
  2. CommitRealTimeService线程每隔200ms将ByteBuffer新追加内容提交到MappedByteBuffer中
  3. MappedByteBuffer在内存中追加提交的内容,wrotePosition指针向后移动
  4. commit操作成功返回,将committedPosition位置恢复
  5. FlushRealTimeService线程默认每500ms将MappedByteBuffer中新追加的内存刷写到磁盘

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提交线程org.apache.rocketmq.store.CommitLog.CommitRealTimeService

public void run() {
    CommitLog.log.info(this.getServiceName() + " service started");
    while (!this.isStopped()) {
        //间隔时间,默认200ms
        int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
        //一次提交的至少页数 默认是4
        int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();
        //两次真实提交的最大间隔,默认200ms
        int commitDataThoroughInterval =
            CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();
        //上次提交间隔超过commitDataThoroughInterval,则忽略commitDataLeastPages参数,直接提交
        long begin = System.currentTimeMillis();
        if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
            this.lastCommitTimestamp = begin;
            commitDataLeastPages = 0;
        }

        try {
            //执行提交操作,将待提交数据提交到物理文件的内存映射区
            boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
            long end = System.currentTimeMillis();
            if (!result) {
                this.lastCommitTimestamp = end; // result = false means some data committed.
                //now wake up flush thread.
                //唤醒刷盘线程
                flushCommitLogService.wakeup();
            }

            if (end - begin > 500) {
                log.info("Commit data to file costs {} ms", end - begin);
            }
            this.waitForRunning(interval);
        } catch (Throwable e) {
            CommitLog.log.error(this.getServiceName() + " service has exception. ", e);
        }
    }

    boolean result = false;
    for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
        result = CommitLog.this.mappedFileQueue.commit(0);
        CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
    }
    CommitLog.log.info(this.getServiceName() + " service end");
}

刷盘线程org.apache.rocketmq.store.CommitLog.FlushRealTimeService#run

public void run() {
    CommitLog.log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        //表示await方法等待,默认false
        boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
        //线程执行时间间隔
        int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
        //一次刷写任务至少包含页数 默认是4
        int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
        //两次真实刷写任务最大间隔
        int flushPhysicQueueThoroughInterval =
            CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();

        boolean printFlushProgress = false;

        // Print flush progress
        //距离上次提交间隔超过flushPhysicQueueThoroughInterval,则本次刷盘任务将忽略flushPhysicQueueLeastPages,直接提交
        long currentTimeMillis = System.currentTimeMillis();
        if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
            this.lastFlushTimestamp = currentTimeMillis;
            flushPhysicQueueLeastPages = 0;
            printFlushProgress = (printTimes++ % 10) == 0;
        }

        try {
            //执行一次刷盘前,先等待指定时间间隔
            if (flushCommitLogTimed) {
                Thread.sleep(interval);
            } else {
                this.waitForRunning(interval);
            }

            if (printFlushProgress) {
                this.printFlushProgress();
            }

            long begin = System.currentTimeMillis();
            //刷写磁盘
            CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
            long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
            if (storeTimestamp > 0) {
                //更新存储监测点文件的时间戳
                CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
            }
            long past = System.currentTimeMillis() - begin;
            if (past > 500) {
                log.info("Flush data to disk costs {} ms", past);
            }
        } catch (Throwable e) {
            CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
            this.printFlushProgress();
        }
    }

    // Normal shutdown, to ensure that all the flush before exit
    boolean result = false;
    for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
        result = CommitLog.this.mappedFileQueue.flush(0);
        CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
    }

    this.printFlushProgress();

    CommitLog.log.info(this.getServiceName() + " service end");
}

4.5.7 过期文件删除

由于RocketMQ操作CommitLog、ConsumerQueue文件是基于内存映射机制并在启动的时候加载CommitLog、ConsumerQueue目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以要引入一种机制来删除已过期的文件。RocketMQ顺序写CommitLog、ConsumerQueue文件,所有写操作全部落在最后一个CommitLog或者ConsumerQueue文件上,之前的文件在下一个文件创建后将不会再被更新

RocketMQ清除过期文件的方法时:如果当前文件在在一定时间间隔内没有再次被消费,则认为是过期文件,可以被删除,RocketMQ不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为72小时,通过在Broker配置文件中设置fileReservedTime来改变过期时间,单位为小时。

BrokerStartup在启动的时候会调用org.apache.rocketmq.broker.BrokerController#start,其内部会调用this.addScheduleTask(),添加过期文件删除任务

org.apache.rocketmq.store.DefaultMessageStore#addScheduleTask

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

org.apache.rocketmq.store.DefaultMessageStore#cleanFilesPeriodically

private void cleanFilesPeriodically() {
    //清除commitlog文件
    this.cleanCommitLogService.run();
    //清除consumeQueue文件
    this.cleanConsumeQueueService.run();
}
4.5.7.1 清除commitlog过期文件

org.apache.rocketmq.store.DefaultMessageStore.CleanCommitLogService#run

public void run() {
    try {
        //删除过期文件
        this.deleteExpiredFiles();
        //重新删除挂起文件
        this.redeleteHangedFile();
    } catch (Throwable e) {
        DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
    }
}

org.apache.rocketmq.store.MappedFileQueue#deleteExpiredFileByTime

public int deleteExpiredFileByTime(final long expiredTime,
    final int deleteFilesInterval,
    final long intervalForcibly,
    final boolean cleanImmediately) {
    //copy 一份MappedFiles
    Object[] mfs = this.copyMappedFiles(0);

    if (null == mfs)
        return 0;

    int mfsLength = mfs.length - 1;
    int deleteCount = 0;
    List<MappedFile> files = new ArrayList<MappedFile>();
    if (null != mfs) {
        //遍历MappedFiles
        for (int i = 0; i < mfsLength; i++) {
            MappedFile mappedFile = (MappedFile) mfs[i];
            //文件的过期时间
            long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
            //当前时间大于过期时间或者需要立刻删除
            if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
                //执行删除
                if (mappedFile.destroy(intervalForcibly)) {
                    //删除成功
                    files.add(mappedFile);
                    deleteCount++;

                    if (files.size() >= DELETE_FILES_BATCH_MAX) {
                        break;
                    }

                    if (deleteFilesInterval > 0 && (i + 1) < mfsLength) {
                        try {
                            Thread.sleep(deleteFilesInterval);
                        } catch (InterruptedException e) {
                        }
                    }
                } else {
                    break;
                }
            } else {
                //avoid deleting files in the middle
                break;
            }
        }
    }
    //从mappedFiles中移除删除的mappedFile
    deleteExpiredFile(files);

    return deleteCount;
}
4.5.7.2 清除consumeQueue过期文件

org.apache.rocketmq.store.DefaultMessageStore.CleanConsumeQueueService#run

public void run() {
    try {
        //删除过期文件
        this.deleteExpiredFiles();
    } catch (Throwable e) {
        DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
    }
}

org.apache.rocketmq.store.DefaultMessageStore.CleanConsumeQueueService#deleteExpiredFiles

private void deleteExpiredFiles() {
    int deleteLogicsFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteConsumeQueueFilesInterval();
    //commitLog 中最小的偏移量
    long minOffset = DefaultMessageStore.this.commitLog.getMinOffset();

    if (minOffset > this.lastPhysicalMinOffset) {
        this.lastPhysicalMinOffset = minOffset;

        ConcurrentMap<String, ConcurrentMap<Integer, ConsumeQueue>> tables = DefaultMessageStore.this.consumeQueueTable;

        for (ConcurrentMap<Integer, ConsumeQueue> maps : tables.values()) {
            for (ConsumeQueue logic : maps.values()) {
                //删除消息队列中偏移量小于minOffset的数据
                int deleteCount = logic.deleteExpiredFile(minOffset);

                if (deleteCount > 0 && deleteLogicsFilesInterval > 0) {
                    try {
                        Thread.sleep(deleteLogicsFilesInterval);
                    } catch (InterruptedException ignored) {
                    }
                }
            }
        }
        //删除索引文件中偏移量小于minOffset的数据
        DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset);
    }
}

4.5.8 小结

RocketMQ的存储文件包括消息文件(Commitlog)、消息消费队列文件(ConsumerQueue)、Hash索引文件(IndexFile)、监测点文件(checkPoint)、abort(关闭异常文件)。单个消息存储文件、消息消费队列文件、Hash索引文件长度固定以便使用内存映射机制进行文件的读写操作。RocketMQ消息文件以文件的起始偏移量来命名文件,这样根据偏移量能快速定位到真实的物理文件。RocketMQ基于内存映射文件机制提供了同步刷盘和异步刷盘两种机制,异步刷盘是指在消息存储时先追加到内存映射文件(也可能先放在堆外内存然后提交到内存映射文件),然后启动专门的刷盘线程定时将内存中的文件数据刷写到磁盘。

CommitLog,消息存储文件,RocketMQ为了保证消息发送的高吞吐量,采用单一文件存储所有主题消息,保证消息存储是完全的顺序写,但这样给文件读取带来了不便,为此RocketMQ为了方便消息消费构建了消息消费队列文件,基于主题与队列进行组织,同时RocketMQ为消息实现了Hash索引,可以为消息设置索引键,根据所以能够快速从CommitLog文件中检索消息。

当消息达到CommitLog后,会通过ReputMessageService线程接近实时地将消息转发给消息消费队列文件与索引文件。为了安全起见,RocketMQ引入abort文件,记录Broker的停机是否是正常关闭还是异常关闭,在重启Broker时为了保证CommitLog文件,消息消费队列文件与Hash索引文件的正确性,分别采用不同策略来恢复文件。

RocketMQ不会永久存储消息文件、消息消费队列文件,而是启动文件过期机制并在磁盘空间不足或者默认凌晨4点删除过期文件,文件保存72小时并且在删除文件时并不会判断该消息文件上的消息是否被消费

4.6 Consume

消息消费以组的模式开展,一个消费组内可以包含多个消费者,每一个消费者组可订阅多个主题,消费组内消费者之间有集群模式和广播模式两种消费模式。

  • 集群模式

    主题下的同一条消息只允许被其中一个消费者消费。

  • 广播模式

    主题下的同一条消息,将被集群内的所有消费者消费一次。

消息服务器与消费者之间的消息传递也有两种模式:推模式、拉模式

所谓的拉模式,是消费端主动拉起拉消息请求,

而推模式是消息达到消息服务器后,推送给消息消费者。

RocketMQ消息推模式的实现基于拉模式,在拉模式上包装一层,一个拉取任务完成后开始下一个拉取任务。

集群模式下,多个消费者如何对消息队列进行负载呢?

消息队列负载机制遵循一个通用思想:一个消息队列同一个时间只允许被一个消费者消费,一个消费者可以消费多个消息队列。

RocketMQ支持局部顺序消息消费,也就是保证同一个消息队列上的消息顺序消费。

不支持消息全局顺序消费,如果要实现某一个主题的全局顺序消费,可以将该主题的队列数设置为1,牺牲高可用性。

4.6.1 消费者启动流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

org.apache.rocketmq.client.consumer.DefaultMQPushConsumer#start

public void start() throws MQClientException {
    //设置消费者分组
    setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
    //启动
    this.defaultMQPushConsumerImpl.start();
    if (null != traceDispatcher) {
        try {
            traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
        } catch (MQClientException e) {
            log.warn("trace dispatcher start failed ", e);
        }
    }
}

org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start

public synchronized void start() throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
                this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
            this.serviceState = ServiceState.START_FAILED;
            //检查消息者是否合法
            this.checkConfig();
            //构建主题订阅信息
            this.copySubscription();
            //集群模式下设置消费者客户端实例名称为进程ID
            if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
                this.defaultMQPushConsumer.changeInstanceNameToPID();
            }
            //创建MQClient实例
            this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
            //构建rebalanceImpl
            this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
            this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
            this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
            this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);

            this.pullAPIWrapper = new PullAPIWrapper(
                mQClientFactory,
                this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
            //注册过滤器钩子函数
            this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);

            if (this.defaultMQPushConsumer.getOffsetStore() != null) {
                this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
            } else {
                switch (this.defaultMQPushConsumer.getMessageModel()) {
                    case BROADCASTING:
                        //消息消费广播模式,将消费进度保存在本地
                        this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    case CLUSTERING:
                        //消息消费集群模式,将消费进度保存在远端Broker
                        this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    default:
                        break;
                }
                this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
            }
            this.offsetStore.load();
            if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
                //创建顺序消息消费服务
                this.consumeOrderly = true;
                this.consumeMessageService =
                    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
            } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
                //创建并发消息消费服务
                this.consumeOrderly = false;
                this.consumeMessageService =
                    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
            }
            //消息消费服务启动
            this.consumeMessageService.start();
            //注册消费者实例
            boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
            if (!registerOK) {
                this.serviceState = ServiceState.CREATE_JUST;
                this.consumeMessageService.shutdown();
                throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            }
            //启动消费者客户端
            mQClientFactory.start();
            log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
            this.serviceState = ServiceState.RUNNING;
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }

    this.updateTopicSubscribeInfoWhenSubscriptionChanged();
    this.mQClientFactory.checkClientInBroker();
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    this.mQClientFactory.rebalanceImmediately();
}

4.6.2 消费拉取(push)

消息消费模式有两种模式:广播模式与集群模式。广播模式比较简单,每一个消费者需要拉取订阅主题下所有队列的消息

在集群模式下,同一个消费者组内有多个消息消费者,同一个主题存在多个消费队列,消费者通过负载均衡的方式消费消息。

消息队列负载均衡,通常的作法是一个消息队列在同一个时间只允许被一个消费消费者消费,一个消息消费者可以同时消费多个消息队列。

4.6.2.1 PullMessageService实现机制

在DefaultMQPush启动的时候会调用mQClientFactory.start();方法,其调用了this.pullMessageService.start(),RocketMQ使用一个单独的线程PullMessageService来负责消息的拉取

org.apache.rocketmq.client.impl.consumer.PullMessageService#run

public void run() {
        log.info(this.getServiceName() + " service started");
        //循环拉取消息
        while (!this.isStopped()) {
            try {
                //从请求队列中获取拉取消息请求,如果是push 会在负载的时候放入数据
                PullRequest pullRequest = this.pullRequestQueue.take();
                //拉取消息
                this.pullMessage(pullRequest);
            } catch (InterruptedException ignored) {
            } catch (Exception e) {
                log.error("Pull Message Service Run Method exception", e);
            }
        }

        log.info(this.getServiceName() + " service end");
    }

org.apache.rocketmq.client.impl.consumer.PullMessageService#pullMessage

private void pullMessage(final PullRequest pullRequest) {
    //获得消费者实例
    final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
    if (consumer != null) {
        //强转为推送模式消费者 pull模式走不到这
        DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
        //拉消息
        impl.pullMessage(pullRequest);
    } else {
        log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
    }
}
4.6.2.2 ProcessQueue实现机制

ProcessQueue是MessageQueue在消费端的重现、快照。PullMessageService从消息服务器默认每次拉取32条消息,按照消息的队列偏移量顺序存放在ProcessQueue中,PullMessageService然后将消息提交到消费者消费线程池,消息成功消费后从ProcessQueue中移除。

4.6.2.3 消息拉取基本流程
4.6.2.3.1 客户端发起请求

截屏2023-12-01 16.18.04

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage

public void pullMessage(final PullRequest pullRequest) {
    //从pullRequest获得ProcessQueue
    final ProcessQueue processQueue = pullRequest.getProcessQueue();
    //如果处理队列被丢弃,直接返回
    if (processQueue.isDropped()) {
        log.info("the pull request[{}] is dropped.", pullRequest.toString());
        return;
    }
    //如果处理队列未被丢弃,更新时间戳
    pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

    try {
        this.makeSureStateOK();
    } catch (MQClientException e) {
        log.warn("pullMessage exception, consumer state not ok", e);
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        return;
    }
    //如果处理队列被挂起,延迟1s后再执
    if (this.isPause()) {
        log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
        return;
    }
    //获得待处理消息数量
    long cachedMessageCount = processQueue.getMsgCount().get();
    //获得待处理消息大小
    long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
    //从数量进行流控
    if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
        if ((queueFlowControlTimes++ % 1000) == 0) {
            log.warn(
                "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
                this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
        }
        return;
    }
    //从消息大小进行流控
    if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
        if ((queueFlowControlTimes++ % 1000) == 0) {
            log.warn(
                "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
                this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
        }
        return;
    }

    if (!this.consumeOrderly) {
        //非顺序消费
        //根据消息跨度流控
        if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
            if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
                log.warn(
                    "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
                    processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
                    pullRequest, queueMaxSpanFlowControlTimes);
            }
            return;
        }
    } else {
        //顺序消费
        //加锁
        if (processQueue.isLocked()) {
            //判断该任务是否被锁定
            if (!pullRequest.isLockedFirst()) {
                //找到消息队列中未消费的消息最小偏移量
                final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
                boolean brokerBusy = offset < pullRequest.getNextOffset();
                log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
                    pullRequest, offset, brokerBusy);
                if (brokerBusy) {
                    log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
                        pullRequest, offset);
                }
                //锁定任务
                pullRequest.setLockedFirst(true);
                //以broker中记录的消费偏移量为准
                pullRequest.setNextOffset(offset);
            }
        } else {
            //3s后把任务放到PullMessageService.pullRequestQueue
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
            log.info("pull message later because not locked in broker, {}", pullRequest);
            return;
        }
    }
    //获得订阅信息
    final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
    if (null == subscriptionData) {
        //3s后把任务放到PullMessageService.pullRequestQueue
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        log.warn("find the consumer's subscription failed, {}", pullRequest);
        return;
    }

    final long beginTimestamp = System.currentTimeMillis();

    PullCallback pullCallback = new PullCallback() {
        //拉取成功,是调用成功的意思,broker返回了响应但是不一定有消息
        @Override
        public void onSuccess(PullResult pullResult) {
            if (pullResult != null) {
                //处理一下结果
                pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                    subscriptionData);

                switch (pullResult.getPullStatus()) {
                    case FOUND:
                        long prevRequestOffset = pullRequest.getNextOffset();
                        //设置下次拉取的偏移量
                        pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                        long pullRT = System.currentTimeMillis() - beginTimestamp;
                        DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                            pullRequest.getMessageQueue().getTopic(), pullRT);

                        long firstMsgOffset = Long.MAX_VALUE;
                        if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
                            //把任务放到PullMessageService.pullRequestQueue
                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                        } else {
                            //拉取到的消息的起始偏移量
                            firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();

                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
                            //将拉取到的消息存入processQueue
                            boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                            //将processQueue提交到consumeMessageService中供消费者消费
                            DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                pullResult.getMsgFoundList(),
                                processQueue,
                                pullRequest.getMessageQueue(),
                                dispatchToConsume);
                            //如果pullInterval大于0,则等待pullInterval毫秒后将pullRequest对象放入到 PullMessageService中的pullRequestQueue队列中
                            if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
                    
  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值