RocketMQ(1)-架构原理

RocketMQ(1)-架构原理

RocketMQ是阿里开源的分布式消息中间件,跟其它中间件相比,RocketMQ的特点是纯JAVA实现集群和HA实现相对简单在发生宕机和其它故障时消息丢失率更低

一、RocketMQ专业术语

先讲专业术语的含义,后面会画流程图来更好的去理解它们。

Producer

消息生产者,位于用户的进程内,Producer通过NameServer获取所有Broker的路由信息,根据负载均衡策略选择将消息发到哪个Broker,然后调用Broker接口提交消息。

Producer Group

生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组。

Consumer

消息消费者,位于用户进程内。Consumer通过NameServer获取所有broker的路由信息后,向Broker发送Pull请求来获取消息数据。Consumer可以以两种模式启动,广播(Broadcast)和集群(Cluster)广播模式下,一条消息会发送给所有Consumer,集群模式下消息只会发送给一个Consumer

Consumer Group

消费者组,和生产者类似,消费同一类消息的多个 Consumer 实例组成一个消费者组。

Topic

Topic用于将消息按主题做划分,Producer将消息发往指定的Topic,Consumer订阅该Topic就可以收到这条消息。Topic跟发送方和消费方都没有强关联关系,发送方可以同时往多个Topic投放消息,消费方也可以订阅多个Topic的消息。在RocketMQ中,Topic是一个上逻辑概念。消息存储不会按Topic分开

Message

代表一条消息,使用MessageId唯一识别,用户在发送时可以设置messageKey,便于之后查询和跟踪。一个 Message 必须指定 Topic,相当于寄信的地址。Message 还有一个可选的 Tag 设置,以便消费端可以基于 Tag 进行过滤消息。也可以添加额外的键值对,例如你需要一个业务 key 来查找 Broker 上的消息,方便在开发过程中诊断问题。

Tag

标签可以被认为是对 Topic 进一步细化。一般在相同业务模块中通过引入标签来标记不同用途的消息。

Broker

Broker是RocketMQ的核心模块,负责接收并存储消息,同时提供Push/Pull接口来将消息发送给Consumer。Consumer可选择从Master或者Slave读取数据。多个主/从组成Broker集群,集群内的Master节点之间不做数据交互。Broker同时提供消息查询的功能,可以通过MessageID和MessageKey来查询消息。Borker会将自己的Topic配置信息实时同步到NameServer。

Queue

Topic和Queue是1对多的关系一个Topic下可以包含多个Queue,主要用于负载均衡。发送消息时,用户只指定Topic,Producer会根据Topic的路由信息选择具体发到哪个Queue上。Consumer订阅消息时,会根据负载均衡策略决定订阅哪些Queue的消息。

Offset

RocketMQ在存储消息时会为每个Topic下的每个Queue生成一个消息的索引文件,每个Queue都对应一个Offset记录当前Queue中消息条数

NameServer

NameServer可以看作是RocketMQ的注册中心,它管理两部分数据:集群的Topic-Queue的路由配置;Broker的实时配置信息。其它模块通过Nameserv提供的接口获取最新的Topic配置和路由信息。

  • Producer/Consumer :通过查询接口获取Topic对应的Broker的地址信息
  • Broker : 注册配置信息到NameServer, 实时更新Topic信息到NameServer

 

二、流程图

我们由简单到复杂的来理解,它的一些核心概念

这个图很好理解,消息先发到Topic,然后消费者去Topic拿消息。只是Topic在这里只是个概念,那它到底是怎么存储消息数据的呢,这里就要引入Broker概念。

2、Topic的存储

​ Topic是一个逻辑上的概念,实际上Message是在每个Broker上以Queue的形式记录。

从上面的图片可以总结下几条结论。

1、消费者发送的Message会在Broker中的Queue队列中记录。
2、一个Topic的数据可能会存在多个Broker中。
3、一个Broker存在多个Queue。
4、单个的Queue也可能存储多个Topic的消息。

也就是说每个Topic在Broker上会划分成几个逻辑队列,每个逻辑队列保存一部分消息数据,但是保存的消息数据实际上不是真正的消息数据,而是指向commit log的消息索引。

Queue不是真正存储Message的地方,真正存储Message的地方是在CommitLog

如图(盗图)

左边的是CommitLog。这个是真正存储消息的地方。RocketMQ所有生产者的消息都是往这一个地方存的。

右边是ConsumeQueue。这是一个逻辑队列。和上文中Topic下的Queue是一一对应的。消费者是直接和ConsumeQueue打交道。ConsumeQueue记录了消费位点,这个消费位点关联了commitlog的位置。所以即使ConsumeQueue出问题,只要commitlog还在,消息就没丢,可以恢复出来。还可以通过修改消费位点来重放或跳过一些消息。

3、部署模型

在部署RocketMQ时,会部署两种角色。NameServer和Broker。如图(盗图)

针对这张图做个说明

1、Product和consumer集群部署,是你开发的项目进行集群部署。
2、Broker 集群部署是为了高可用,因为Broker是真正存储Message的地方,集群部署是为了避免一台挂掉,导致整个项目KO.

那Name SerVer是做什么用呢,它和Product、Consumer、Broker之前存在怎样的关系呢?

先简单概括Name Server的特点

1、Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
2、每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
3、Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息。
4、Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息。

这里面最核心的是每个Broker与Name Server集群中的所有节点建立长连接这样做好处多多。

1、这样可以使Name Server之间可以没有任何关联,因为它们绑定的Broker是一致的。

2、作为Producer或者Consumer可以绑定任何一个Name Server 因为它们都是一样的。

 

三、详解Broker

1、Broker与Name Server关系

1)连接 单个Broker和所有Name Server保持长连接。

2)心跳

心跳间隔:每隔30秒向所有NameServer发送心跳,心跳包含了自身的Topic配置信息。

心跳超时:NameServer每隔10秒,扫描所有还存活的Broker连接,若某个连接2分钟内没有发送心跳数据,则断开连接。

3)断开:当Broker挂掉;NameServer会根据心跳超时主动关闭连接,一旦连接断开,会更新Topic与队列的对应关系,但不会通知生产者和消费者。

2、 负载均衡

一个Topic分布在多个Broker上,一个Broker可以配置多个Topic,它们是多对多的关系。
如果某个Topic消息量很大,应该给它多配置几个Queue,并且尽量多分布在不同Broker上,减轻某个Broker的压力。

3 、可用性

由于消息分布在各个Broker上,一旦某个Broker宕机,则该Broker上的消息读写都会受到影响。

所以RocketMQ提供了Master/Slave的结构,Salve定时从Master同步数据,如果Master宕机,则Slave提供消费服务,但是不能写入消息,此过程对应用透明,由RocketMQ内部解决。
有两个关键点:
思考1一旦某个broker master宕机,生产者和消费者多久才能发现?

受限于Rocketmq的网络连接机制,默认情况下最多需要30秒,因为消费者每隔30秒从nameserver获取所有topic的最新队列情况,这意味着某个broker如果宕机,客户端最多要30秒才能感知。

思考2 master恢复恢复后,消息能否恢复。
消费者得到Master宕机通知后,转向Slave消费,但是Slave不能保证Master的消息100%都同步过来了,因此会有少量的消息丢失。但是消息最终不会丢的,一旦Master恢复,未同步过去的消息会被消费掉。

 

四 Consumer (消费者)

1 、Consumer与Name Server关系

1)连接 : 单个Consumer和一台NameServer保持长连接,如果该NameServer挂掉,消费者会自动连接下一个NameServer,直到有可用连接为止,并能自动重连。
2)心跳: 与NameServer没有心跳
3)轮询时间 : 默认情况下,消费者每隔30秒从NameServer获取所有Topic的最新队列情况,这意味着某个Broker如果宕机,客户端最多要30秒才能感知。

2、 Consumer与Broker关系

1)连接 :单个消费者和该消费者关联的所有broker保持长连接。

3、 负载均衡

集群消费模式下,一个消费者集群多台机器共同消费一个Topic的多个队列,一个队列只会被一个消费者消费。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。

 

五、 Producer(生产者)

1、 Producer与Name Server关系

1)连接 单个Producer和一台NameServer保持长连接,如果该NameServer挂掉,生产者会自动连接下一个NameServer,直到有可用连接为止,并能自动重连。
2)轮询时间 默认情况下,生产者每隔30秒从NameServer获取所有Topic的最新队列情况,这意味着某个Broker如果宕机,生产者最多要30秒才能感知,在此期间,
发往该broker的消息发送失败。
3)心跳 与nameserver没有心跳

2、 与broker关系

连接 单个生产者和该生产者关联的所有broker保持长连接。

RocketMQ(2)—Docker集群部署RocketMQ

=前言=

1、因为自己只买了一台阿里云服务器,所以RocketMQ集群都部署在单台服务器上只是端口不同,如果实际开发,可以分别部署在多台服务器上。
2、这里有关 Broker 和 NameServer 分别都做了了集群部署(各部署两个),且BroKer是按两主进行部署。

之所以选用Docker部署主要还是考虑 :通过Docker部署RocketMQ集群更快速,而且对系统的资源利用更好!

之前有写过Liunx如何部署Docker的博客:【Docker】(3)---linux部署Docker、Docker常用命令

之前有关RocketMQ概念做了介绍的博客:RocketMQ(1)-架构原理

下面先写好所需配置文件,在运行配置文件,最终看运行结果!

一、写配置文件

1、创建所需文件夹

mkdir -p  /opt/rocketmq/logs/nameserver-a
mkdir -p  /opt/rocketmq/logs/nameserver-b
mkdir -p /opt/rocketmq/store/nameserver-a
mkdir -p /opt/rocketmq/store/nameserver-b
mkdir -p /opt/rocketmq/logs/broker-a
mkdir -p /opt/rocketmq/logs/broker-b
mkdir -p /opt/rocketmq/store/broker-a
mkdir -p /opt/rocketmq/store/broker-b
mkdir -p /home/rocketmq/broker-a/
mkdir -p /home/rocketmq/broker-b/

2、创建broker.conf

broker.conf是Broker的配置文件,因为此时RocketMQ镜像还没有拉取,所以还没有默认的broker.conf。所以这里直接写好,到时候通过命令替换默认的broker.conf。

因为是双主模式部署,所以会有两个broker.conf,这里暂且命名 broker-a.conf 和 broker-b.conf

1) broker-a.conf

brokerClusterName = rocketmq-cluster
brokerName = broker-a
brokerId = 0
#这个很有讲究 如果是正式环境 这里一定要填写内网地址(安全)
#如果是用于测试或者本地这里建议要填外网地址,因为你的本地代码是无法连接到阿里云内网,只能连接外网。
brokerIP1 = xxxxx
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 内网的(阿里云有内网IP和外网IP)
namesrvAddr=172.18.0.5:9876;172.18.0.5:9877
autoCreateTopicEnable=true
#Broker 对外服务的监听端口,
listenPort = 10911
#Broker角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

这里配置的主要信息有:

1、当前Broker对外暴露的端口号
2、注册到NameServer的地址,看到这里有两个地址,说明NameServer也是集群部署。
3、当前Broker的角色,是主还是从,这里表示是主。

2)broker-b.conf

brokerClusterName = rocketmq-cluster
brokerName = broker-b
brokerId = 0
brokerIP1 = xxxxx
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 内网的(阿里云有内网IP和外网IP)
namesrvAddr=172.18.0.5:9876;172.18.0.5:9877
autoCreateTopicEnable=true
#Broker 对外服务的监听端口,
listenPort = 10909
#Broker角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

这个上面并无差别,只是当前Brocker对外暴露的端口不一样。

3、编写 docker-compose.yml

我们可以简单把docker-compose.yml理解成一个类似Shell的脚本,这个脚本定义了运行多容器应用程序的信息。

version: '3.5'
services:
  rmqnamesrv-a:
    image: rocketmqinc/rocketmq:4.3.0
    container_name: rmqnamesrv-a
    ports:
      - 9876:9876
    volumes:
      - /opt/rocketmq/logs/nameserver-a:/opt/logs
      - /opt/rocketmq/store/nameserver-a:/opt/store
    command: sh mqnamesrv
    networks:
        rmq:
          aliases:
            - rmqnamesrv-a

  rmqnamesrv-b:
    image: rocketmqinc/rocketmq:4.3.0
    container_name: rmqnamesrv-b
    ports:
      - 9877:9877
    volumes:
      - /opt/rocketmq/logs/nameserver-b:/opt/logs
      - /opt/rocketmq/store/nameserver-b:/opt/store
    command: sh mqnamesrv
    networks:
        rmq:
          aliases:
            - rmqnamesrv-b

  rmqbroker-a:
    image: rocketmqinc/rocketmq:4.3.0
    container_name: rmqbroker-a
    ports:
      - 10911:10911
    volumes:
      - /opt/rocketmq/logs/broker-a:/opt/logs
      - /opt/rocketmq/store/broker-a:/opt/store
      - /home/rocketmq/broker-a/broker-a.conf:/opt/rocketmq-4.3.0/conf/broker.conf 
    environment:
        TZ: Asia/Shanghai
        NAMESRV_ADDR: "rmqnamesrv-a:9876"
        JAVA_OPTS: " -Duser.home=/opt"
        JAVA_OPT_EXT: "-server -Xms256m -Xmx256m -Xmn256m"
    command: sh mqbroker -c /opt/rocketmq-4.3.0/conf/broker.conf autoCreateTopicEnable=true &
    links:
      - rmqnamesrv-a:rmqnamesrv-a
      - rmqnamesrv-b:rmqnamesrv-b
    networks:
      rmq:
        aliases:
          - rmqbroker-a

  rmqbroker-b:
    image: rocketmqinc/rocketmq:4.3.0
    container_name: rmqbroker-b
    ports:
      - 10909:10909
    volumes:
      - /opt/rocketmq/logs/broker-b:/opt/logs
      - /opt/rocketmq/store/broker-b:/opt/store
      - /home/rocketmq/broker-b/broker-b.conf:/opt/rocketmq-4.3.0/conf/broker.conf 
    environment:
        TZ: Asia/Shanghai
        NAMESRV_ADDR: "rmqnamesrv-b:9876"
        JAVA_OPTS: " -Duser.home=/opt"
        JAVA_OPT_EXT: "-server -Xms256m -Xmx256m -Xmn256m"
    command: sh mqbroker -c /opt/rocketmq-4.3.0/conf/broker.conf autoCreateTopicEnable=true &
    links:
      - rmqnamesrv-a:rmqnamesrv-a
      - rmqnamesrv-b:rmqnamesrv-b
    networks:
      rmq:
        aliases:
          - rmqbroker-b
  rmqconsole:
    image: styletang/rocketmq-console-ng
    container_name: rmqconsole
    ports:
      - 9001:9001
    environment:
        JAVA_OPTS: -Drocketmq.namesrv.addr=rmqnamesrv-a:9876;rmqnamesrv-b:9877 -Dcom.rocketmq.sendMessageWithVIPChannel=false
    networks:
      rmq:
        aliases:
          - rmqconsole
networks:
  rmq:
    name: rmq
    driver: bridge

从配置文件可以大致看出几点:

1、通过Docker总共拉取了5条镜像记录。rmqnamesrv-armqnamesrv-brmqbroker-armqbroker-brmqconsole
2、rmqconsole是一个可视化的工具,可以通过页面来查看RocketMQ相关信息。
3、可以看出对于broker的配置文件broker.conf已经被broker-a/b.conf替换。

 

二、环境部署

上面仅仅是把配置文件写好,但启动RocketMQ还有很多前提条件

1、所需环境

1. 建议使用64位操作系统,Linux / Unix / Mac;
2. 64位JDK 1.8+;
3. Maven 3.2.x;
4. Git的;
5. 4g +免费磁盘用于Broker服务器

同时需要运行docker-compose.yml还需要安装docker-compose

2、安装docker-compose

有关的介绍可以看官方GitHub : https://github.com/docker/compose

安装docker-compose

curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

查看是否安装成功

[root@izbp13196wp34obmnd4avdz ~]# docker-compose --version
docker-compose version 1.24.1, build 4667896b

3、关键一步

前面所有的操作,都是为这一步的铺垫,通过docker-compose执行docker-compose.yml配置文件

docker-compose -f docker-compose.yml up -d

如果你看到,那说明你大功告成!

查看可视化界面

完美!

注意 这里面涉及相关端口记得到阿里云后台开启!
说明 因为我这边是单机部署集群,我发现集群部署NameServer没有问题,集群部署Broker的会因为端口的问题只能使用一台。

 

SpringBoot整合RocketMQ

上篇博客讲解了服务器集群部署RocketMQ 博客地址:RocketMQ(2)---Docker部署RocketMQ集群

这篇在上篇搭建好的基础上,将SpringBoot整合RocketMQ实现生产消费。

GitHub地址https://github.com/yudiandemingzi/spring-boot-study

一、搭建步骤

先说下技术大致架构

SpringBoot2.1.6 + Maven3.5.4 + rocketmq4.3.0 + JDK1.8 +Lombok(插件)

1、添加rocketmq包

     <!--注意: 这里的版本,要和部署在服务器上的版本号一致-->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.3.0</version>
        </dependency>

2、JmsConfig(配置类)

连接RocketMQ服务器配置类,这里为了方便直接写成常量。

/**
 * @Description: 安装实际开发这里的信息 都是应该写在配置里,来读取,这里为了方便所以写成常量
 */
public class JmsConfig {
    /**
     * Name Server 地址,因为是集群部署 所以有多个用 分号 隔开
     */
    public static final String NAME_SERVER = "127.12.15.6:9876;127.12.15.6:9877";
    /**
     * 主题名称 主题一般是服务器设置好 而不能在代码里去新建topic( 如果没有创建好,生产者往该主题发送消息 会报找不到topic错误)
     */
    public static final String TOPIC = "topic_family";

}

3、Producer (生产者)

@Slf4j
@Component
public class Producer {
    private String producerGroup = "test_producer";
    private DefaultMQProducer producer;
    
    public Producer(){
        //示例生产者
        producer = new DefaultMQProducer(producerGroup);
        //不开启vip通道 开通口端口会减2
        producer.setVipChannelEnabled(false);
        //绑定name server
        producer.setNamesrvAddr(JmsConfig.NAME_SERVER);
        start();
    }
    /**
     * 对象在使用之前必须要调用一次,只能初始化一次
     */
    public void start(){
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }
  
    public DefaultMQProducer getProducer(){
        return this.producer;
    }
    /**
     * 一般在应用上下文,使用上下文监听器,进行关闭
     */
    public void shutdown(){
        this.producer.shutdown();
    }
}

4、Consumer (消费者)

@Slf4j
@Component
public class Consumer {

    /**
     * 消费者实体对象
     */
    private DefaultMQPushConsumer consumer;
    /**
     * 消费者组
     */
    public static final String CONSUMER_GROUP = "test_consumer";
    /**
     * 通过构造函数 实例化对象
     */
    public Consumer() throws MQClientException {

        consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        consumer.setNamesrvAddr(JmsConfig.NAME_SERVER);
        //消费模式:一个新的订阅组第一次启动从队列的最后位置开始消费 后续再启动接着上次消费的进度开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅主题和 标签( * 代表所有标签)下信息
        consumer.subscribe(JmsConfig.TOPIC, "*");
        // //注册消费的监听 并在此监听中消费信息,并返回消费的状态信息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            // msgs中只收集同一个topic,同一个tag,并且key相同的message
            // 会把不同的消息分别放置到不同的队列中
            try {
                for (Message msg : msgs) {

                    //消费者获取消息 这里只输出 不做后面逻辑处理
                    String body = new String(msg.getBody(), "utf-8");
                    log.info("Consumer-获取消息-主题topic为={}, 消费消息为={}", msg.getTopic(), body);
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("消费者 启动成功=======");
    }
}

大致就是这边简单,下面就是测试。

 

二、测试

先写个测试接口进行测试。

1、Controller

@Slf4j
@RestController
public class Controller {

    @Autowired
    private Producer producer;

    private List<String> mesList;

    /**
     * 初始化消息
     */
    public Controller() {
        mesList = new ArrayList<>();
        mesList.add("小小");
        mesList.add("爸爸");
        mesList.add("妈妈");
        mesList.add("爷爷");
        mesList.add("奶奶");

    }

    @RequestMapping("/text/rocketmq")
    public Object callback() throws Exception {
        //总共发送五次消息
        for (String s : mesList) {
            //创建生产信息
            Message message = new Message(JmsConfig.TOPIC, "testtag", ("小小一家人的称谓:" + s).getBytes());
            //发送
            SendResult sendResult = producer.getProducer().send(message);
            log.info("输出生产者信息={}",sendResult);
        }
        return "成功";
    } 
}

2、测试结果

很明显生产发送消息已经成功,二消费者也成功接收了消息!

另外我们再来看下RocketMQ控制台是否也有消费记录

很明显在控制台这边也会有消费记录!

总结这边只是简单的整合,后面会通过RocketMQ实现分布式事务,可以用于线上实际环境中,到时候会深入讲解下源码。

 

RocketMQ核心配置讲解

RocketMQ的核心配置在broker.conf配置文件里,下面我们来分析下它。

一、broker.conf配置

下面只列举一些常用的核心配置讲解。

1、broker.conf核心配置讲解

# nameServer地址,如果nameserver是多台集群的话,就用分号分割
namesrvAddr=172.1.21.29:9876;143.13.262.43:9876
# 所属集群名字(同一主从下:Master和slave名称要一致)
brokerClusterName=rocketmq-cluster
# broker名字,注意此处不同的配置文件填写的不一样  例如:在a.properties 文件中写 broker-a  在b.properties 文件中写 broker-b
brokerName=broker-a
# 0 表示 Master,>0 表示 Slave
brokerId=0
# Broker 对外服务的监听端口
listenPort=10911
# 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数。由于是4个broker节点,所以设置为4
# defaultTopicQueueNums=4
# 是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
# 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
# commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
# ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
# 检测可用的磁盘空间大小,当磁盘被占用超过90%,消息写入会直接报错                    
diskMaxUsedSpaceRatio=90
# Broker 的角色: ASYNC_MASTER 异步复制Master ; SYNC_MASTER 同步双写Master; SLAVE
brokerRole=SYNC_MASTER
# 刷盘方式 ASYNC_FLUSH 异步刷盘; SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

2、同步刷盘 or 异步刷盘

同步刷盘和异步刷盘指的是 内存和磁盘 的关系。

RocketMQ的消息最终是是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。

从客户端发送消息,一开始先写到内存,再写到磁盘上。如下图所示

两种策略

同步刷盘:当数据成功写到内存中之后立刻刷盘(同步),在保证消息写到磁盘也成功的前提下返回写成功状态。

异步刷盘 :数据写入内存后,直接返回成功状态。异步将内存中的数据持久化到磁盘上。

同步刷盘和异步输盘的优劣

同步刷盘

优点:保证了数据的可靠性,保证数据不会丢失。

缺点:同步刷盘效率较低,因为需要内存将消息写入磁盘后才返回成功状态。

异步刷盘

优点:异步刷盘可以提高系统的吞吐量。因为它仅仅是写入内存成功后,就返回成功状态。

缺点:异步刷盘不能保证数据的可靠性。因为写入内存成功,但写入磁盘的时候因为某种原因写入失败,那就会丢失该条消息。

3、同步复制 or 异步复制

同步复制和异步复制指的是 Master节点和slave节点 的关系。

如果一个Broker组有Master和Slave,消息需要从Master复制到Slave上

两种策略

同步复制: 当数据成功写到内存中Master节点之后立刻同步到Slave中,当Slave也成功的前提下返回写成功状态。

异步复制: 当数据成功写到内存中Master节点之后,直接返回成功状态,异步将Master数据存入Slave节点。

同步复制和异步复制的优劣:

同步复制 : 数据安全性高,性能低一点。

异步复制 : 数据可能丢失,性能高一点。

 

建议 线上采用 同步复制 + 异步刷盘;

 

RocketMQ重试机制

消息重试分为两种:Producer发送消息的重试和 Consumer消息消费的重试

一、Producer端重试

Producer端重试是指: Producer往MQ上发消息没有发送成功,比如网络原因导致生产者发送消息到MQ失败。

看一下代码:

@Slf4j
public class RocketMQTest {
    /**
     * 生产者组
     */
    private static String PRODUCE_RGROUP = "test_producer";
  
    public static void main(String[] args) throws Exception {
        //1、创建生产者对象
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCE_RGROUP);
        //设置重试次数(默认2次)
        producer.setRetryTimesWhenSendFailed(3000);
        //绑定name server
        producer.setNamesrvAddr("74.49.203.55:9876");
        producer.start();
        //创建消息
        Message message = new Message("topic_family", ("小小今年3岁" ).getBytes());
        //发送 这里填写超时时间是5毫秒 所以每次都会发送失败
        SendResult sendResult = producer.send(message,5);
        log.info("输出生产者信息={}",sendResult);
    }
}

超时重试 针对网上说的超时异常会重试的说法都是错误的,想想都觉得可怕,我查的所以文章都说超时异常都会重试,难道这么多人都没有去测试一下 或者去看个源码。

我发现这个问题,是因为我上面超时时间设置为5毫秒 ,按照正常肯定会报超时异常,但我设置1次重试和3000次的重试,虽然最终都会报下面异常,但输出错误时间报

显然不应该是一个级别。但测试发现无论我设置的多少次的重试次数,报异常的时间都差不多。

org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: sendDefaultImpl call timeout

针对这个疑惑,我去看了源码之后,才恍然大悟。

   /**
     * 说明 抽取部分代码
     */
    private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) {
        
        //1、获取当前时间
        long beginTimestampFirst = System.currentTimeMillis();
        long beginTimestampPrev ;
        //2、去服务器看下有没有主题消息
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            boolean callTimeout = false;
            //3、通过这里可以很明显看出 如果不是同步发送消息 那么消息重试只有1次
            int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
            //4、根据设置的重试次数,循环再去获取服务器主题消息
            for (times = 0; times < timesTotal; times++) {
                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                beginTimestampPrev = System.currentTimeMillis();
                long costTime = beginTimestampPrev - beginTimestampFirst;
                //5、前后时间对比 如果前后时间差 大于 设置的等待时间 那么直接跳出for循环了 这就说明连接超时是不进行多次连接重试的
                if (timeout < costTime) {
                    callTimeout = true;
                    break;

                }
                //6、如果超时直接报错
                if (callTimeout) {
                    throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
                }
        }
    }

通过这段源码很明显可以看出以下几点

  1. 如果是异步发送 那么重试次数只有1次
  2. 对于同步而言,超时异常也是不会再去重试
  3. 如果发生重试是在一个for 循环里去重试,所以它是立即重试而不是隔一段时间去重试。

真是实践出真知!!!

 

二、 Consumer端重试

消费端比较有意思,而且在实际开发过程中,我们也更应该考虑的是消费端的重试。

消费者端的失败主要分为2种情况,Exception 和 Timeout

1、Exception

@Slf4j
@Component
public class Consumer {
    /**
     * 消费者实体对象
     */
    private DefaultMQPushConsumer consumer;
    /**
     * 消费者组
     */
    public static final String CONSUMER_GROUP = "test_consumer";
    /**
     * 通过构造函数 实例化对象
     */
    public Consumer() throws MQClientException {
        consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        consumer.setNamesrvAddr("47.99.203.55:9876;47.99.203.55:9877");
        //订阅topic和 tags( * 代表所有标签)下信息
        consumer.subscribe("topic_family", "*");
        //注册消费的监听 并在此监听中消费信息,并返回消费的状态信息
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            //1、获取消息
            Message msg = msgs.get(0);
            try {
                //2、消费者获取消息
                String body = new String(msg.getBody(), "utf-8");
                //3、获取重试次数
                int count = ((MessageExt) msg).getReconsumeTimes();
                log.info("当前消费重试次数为 = {}", count);
                //4、这里设置重试大于3次 那么通过保存数据库 人工来兜底
                if (count >= 2) {
                    log.info("该消息已经重试3次,保存数据库。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                //直接抛出异常
                throw new Exception("=======这里出错了============");
                //return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            } catch (Exception e) {
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });
        //启动监听
        consumer.start();
    }
}

这里的代码意思很明显: 主动抛出一个异常,然后如果超过3次,那么就不继续重试下去,而是将该条记录保存到数据库由人工来兜底。

看下运行结果

注意 消费者和生产者的重试还是有区别的,主要有两点

1、默认重试次数:Product默认是2次,而Consumer默认是16次

2、重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的。它照1S,5S,10S,30S,1M,2M····2H进行重试。
3、Product在异步情况重试失效,而对于Consumer在广播情况下重试失效。

2、Timeout

说明 这里的超时异常并非真正意义上的超时,它指的是指获取消息后,因为某种原因没有给RocketMQ返回消费的状态,即没有return ConsumeConcurrentlyStatus.CONSUME_SUCCESS 或 return ConsumeConcurrentlyStatus.RECONSUME_LATER

那么 RocketMQ会认为该消息没有发送,会一直发送。因为它会认为该消息根本就没有发送给消费者,所以肯定没消费。

做这个测试很简单。

        //1、消费者获得消息
        String body = new String(msg.getBody(), "utf-8");
        //2、获取重试次数
        int count = ((MessageExt) msg).getReconsumeTimes();
        log.info("当前消费重试次数为 = {}", count);
        //3、这里睡眠60秒
        Thread.sleep(60000);
       log.info("休眠60秒 看还能不能走到这里。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
        //返回成功
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

当获得 当前消费重试次数为 = 0 后 , 关掉该进程。再重新启动该进程,那么依然能够获取该条消息

consumer消费者  当前消费重试次数为 = 0
休眠60秒 看还能不能走到这里。topic=topic_family,keys=1a2b3c4d5f,msg=小小今年3岁
posted @ 2022-02-21 22:28  hanease  阅读(162)  评论(0编辑  收藏  举报