消息中间件RocketMQ

消息中间件RocketMQ

一、消息中间件简介

MQ完整的英文单词是Message Queue.

Queue表示队列,有先进先出的特性.

我们今天需要学习的中间件本质上就是一个功能齐全且强大的队列.

我们先看看消息中间件可以帮我们解决什么样的开发问题

1.1 应用场景

1.1.1 业务解耦

假设我们现在有这样的需求: 用户在支付完成之后,需要给用户增加积分

伪代码如下:

@Transational
public void payOrder(){
      // 修改订单的状态
      // 远程调用积分服务(同步调用)
      // 通知仓库进行出库操作
     //  ......
}

在这里插入图片描述

假设现在积分微服务出现异常了

在这里插入图片描述

这样的情况,积分微服务出现异常就会影响订单微服务的业务流程,此时说明订单微服务和积分微服务存在耦合的关系。订单微服务依赖积分微服务。

这时候可以使用消息中间件,实现订单微服务和积分微服务的解耦.

我们的业务伪代码如下:

@Transational
public void payOrder(){
      // 修改订单的状态
      // 发送消息到消息中间件
      // 通知仓库进行出库操作
     //  ......
}

在这里插入图片描述

这样处理的话,订单微服务只是负责给消息中间件发送消息(把以前远程调用积分服务传递的参数封装成消息对象发给消息中间件),然后积分服务从消息中间件中获取消息,然后去执行相关的业务逻辑。

这样处理的的话,即使积分微服务宕机了,也并不会影响订单微服务业务流程,从而实现了订单微服务和积分微服务的解耦。

1.1.2 削峰填谷

假设现在有这样的情况,有一个系统,非高峰期的访问量有2000请求/s,中午大概有20分钟的高峰期,此时访问量有5000请求/s,但是我们的系统单台机器业务处理能力只有3000请求/s,如下图所示:

在这里插入图片描述

目前这种情况的话,在高峰期有5000请求/s访问,服务器处理不过来的话,就有可能会奔溃了.

那应该如何解决呢?

我们最容易想到的就是,对服务器做集群,提高处理能力,如下图所示:

在这里插入图片描述

我们这样进行部署的话,即使高峰期有5000请求/s的访问,流量被平均分配到两台的机器上了,每台机器就只有2500请求/s,是低于单台服务器的处理能力的.

上面的方案虽然可以解决我们的问题,但是资源有些浪费。我们就只有20分钟的流量高峰期,为了这20分钟多部署了一台服务器. 95%时间只需要一台服务器。

这个问题和餐厅接待顾客的场景挺像的,假设某个餐厅有50个座位,周一到周五逛街的人比较少,50个座位完全足够了,但是周末的时间逛街的人多了,50个座位是不够的? 那么我们生活中餐厅是怎么处理的呢?

这时候我们就可以使用MQ来实现削峰填谷

在这里插入图片描述

当业务高峰期的时候,超过处理能力的请求会在消息中间件中排队,等流量高峰过去之后,在慢慢把这些请求处理掉. 流量处理图如下:

在这里插入图片描述

当请求流量 > 处理能力的时候,请求进入消息中间件等待

当请求流量< 处理能力的时候,开始处理消息中间件中的请求

1.1.3 数据分发

假设我们现在有这样的需求,A系统会产生一些业务数据会放入到Redis中,然后需要通知B,C系统从Redis中获取数据,进行处理。伪代码如下:

pubilc void processData(){
     // 处理业务数据
     // 远程调用B服务
     // 远程调用C服务
}

在这里插入图片描述

假设现在我们系统结构发生变化了,B服务不在使用了,然后D服务上线了,也需要这些业务数据.

这样的变化导致A服务的处理代码需要发生变化

pubilc void processData(){
     // 处理业务数据
     // 远程调用C服务
     // 远程调用D服务
}

在这里插入图片描述

这样的话,如果我们下游的服务变更比较频繁的情况下,服务A的代码就需要频繁变更.

那么我们可以使用MQ来实现数据分发,伪代码如下:

pubilc void processData(){
     // 处理业务数据
     // 发送消息
}

在这里插入图片描述

使用MQ的话,我们就可以实现,服务A只需要发送1条消息给消息中间件,其他的服务监听消息中间件即可,这样下游的服务的变更并不会影响到服务A的代码

1.2 常用消息中间件

  • ActiveMQ:ActiveMQ是Apache出品,比较老的一个开源的消息中间件,以前在中小企业应用广泛,目前市场份额较小.
  • Kafka:Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。
  • RabbitMQ:RabbitMQ 是一个基于Erlang 语言开发的消息中间件,RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。对数据的一致性,稳定性和可靠性要求比较高的场景
  • RocketMQ:RocketMQ 是阿里巴巴在 2012 年开源的分布式消息中间件,目前已经捐赠给 Apache 软件基金会,并于 2017 年 9 月 25 日成为 Apache 的顶级项目。作为经历过多次阿里巴巴双十一这种“超级工程”的洗礼并有稳定出色表现的国产中间件,以其高性能、低延时和高可靠等特性近年来已经也被越来越多的国内企业使用。淘宝内部的交易系统使用了淘宝自主研发的 Notify 消息中间件,使用 MySQL 作为消息存储媒介,可完全水平扩容,为了进一步降低成本,我们认为存储部分可以进一步优化,2011 年初,Linkin开源了 Kafka 这个优秀的消息中间件,淘宝中间件团队在对 Kafka 做过充分 Review 之后, Kafka 无限消息堆积,高效的持久化速度吸引了我们,但是同时发现这个消息系统主要定位于日志传输,对于使用在淘宝交易、订单、充值等场景下还有诸多特性不满足,为此我们重新用 Java 语言编写了 RocketMQ ,定位于非日志的可靠消息传输(日志场景也OK),目前 RocketMQ 在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理, binlog 分发等场景。

常用消息中间级对比

在这里插入图片描述

1.3 RocketMQ核心概念

在这里插入图片描述

在这里插入图片描述

  • 生产者Producer: 负责生产消息,一般由业务系统负责生产消息。
  • 消费者Consumer: 负责消费消息,执行业务逻辑处理
  • 命名服务Name Server: 命名服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。
  • 代理服务器Broker Server:消息中转角色,负责存储消息、转发消息。
  • 消息主题Topic:表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位
  • 消息队列MessageQueue:对于每个Topic都可以设置一定数量的消息队列用来进行数据的写入/读取
  • 消息内容Message:消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
  • 标签Tag: 为消息设置的标志,用于同一主题下区分不同类型的消息。

二、RocketMQ的安装部署

我们将在Linux环境下进行部署

2.1 在Linux中部署RocketMQ

  • 环境要求,需要JDK11

  • 安装步骤

    # 上传rocketmq-all-4.9.4-bin-release.zip到/usr/local
    # 使用解压命令进行解压
    unzip /usr/local/rocketmq-all-4.9.4-bin-release.zip
    # 软件文件名重命名
    mv  /usr/local/rocketmq-all-4.9.4-bin-release/  /usr/local/rocketmq-4.9/
    # 设置环境变量
    vi /etc/profile
    # 追加内容
    export JAVA_HOME=/usr/local/jdk11
    export ROCKETMQ_HOME=/usr/local/rocketmq-4.9
    export PATH=$JAVA_HOME/bin:$ROCKETMQ_HOME/bin:$PATH
    # 重新加载环境变量的内容
    source /etc/profile
    
  • 启动RocketMQ

    # 修改脚本中的JVM相关参数,修改文件如下
    vi  /usr/local/rocketmq-4.9/bin/runbroker.sh
    # 修改参数内容如下:
    JAVA_OPT="${JAVA_OPT} -server -Xms2g -Xmx2g"
    
    vi  /usr/local/rocketmq-4.9/bin/runserver.sh
    # 修改启动参数配置
    JAVA_OPT="${JAVA_OPT} -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
    
    # 1.启动NameServer
    nohup sh mqnamesrv &
    # 2.查看启动日志
    tail -f ~/logs/rocketmqlogs/namesrv.log
    
    # 1.启动Broker
    nohup sh mqbroker -n localhost:9876 -c /usr/local/rocketmq-4.9/conf/broker.conf &
    # 2.查看启动日志
    tail -f ~/logs/rocketmqlogs/broker.log 
    
  • 关闭RocketMQ

    # 关闭nameserver:
    sh mqshutdown namesrv
    # 关闭broker 
    sh mqshutdown broker
    

2.2 在Linux中部署管控台

将资料中的管控台文件夹下的这两个文件上传到Linux服务器

application.yml
rocketmq-dashboard-1.0.1-SNAPSHOT.jar

修改application.yml中的nameserver的地址

server:
  port: 9999
rocketmq:
  config:
    namesrvAddrs:
      - 192.168.202.129:9876

启动程序

java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar

然后访问http://localhost:9999 就可以看到如下的界面了,在集群中能看到数据才说明启动成功了.

在这里插入图片描述

三、核心功能讲解

3.1 环境准备

创建项目结构如下:

在这里插入图片描述

3.2 入门案例-生产者

3.2.1 生产者配置

  • 在producer-demo的pom.xml中添加SpringBoot相关配置

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.11</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
        	<groupId>org.apache.rocketmq</groupId>
        	<artifactId>rocketmq-spring-boot-starter</artifactId>
        	<version>2.2.2</version>
    	</dependency>
    </dependencies>
    
  • 在producer-demo中的application.yml中添加如下配置

    rocketmq:
      name-server: 192.168.20.161:9876
      producer:
        group: my-group
    
  • 添加启动类

    @SpringBootApplication
    public class ProducerDemo {
        public static void main(String[] args) {
            SpringApplication.run(ProducerDemo.class,args);
        }
    }
    

3.2.2 测试类编写

方式一:

@SpringBootTest
public class ProducerTest {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    //注意需要导入的是Junit5的@Test注解,否则RocketMQTemplate会注入为空
    @Test
    public void testSendMsg(){
        //定义消息主题
        String topic = "helloTopic";
        Message msg = MessageBuilder.withPayload("同步消息1").build();
        SendResult sendResult = rocketMQTemplate.syncSend(topic, msg);
        System.out.println("发送结果:"+sendResult.getSendStatus());
    }
}

方式二:

@SpringBootTest
public class ProducerTest {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Test
    public void testSendMsg(){
        //定义消息主题
        String topic = "helloTopic";
        SendResult sendResult = rocketMQTemplate.syncSend(topic, "同步消息2");
        System.out.println("发送结果:"+sendResult.getSendStatus());
    }
}

3.3 入门案例-消费者

3.3.1 消费者配置

  • 在consumer-demo的pom.xml中添加SpringBoot相关配置

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.11</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
    </dependencies>
    
  • 在consumer-demo中的application.yml中添加如下配置

    rocketmq:
      name-server: 192.168.20.161:9876
    
  • 添加启动类

    @SpringBootApplication
    public class ConsumerDemo {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerDemo.class,args);
        }
    }
    

3.3.2 消费监听类定义

方式一:

@Component
@RocketMQMessageListener(topic = "helloTopic",consumerGroup = "helloGroup")
public class HelloMessageListener implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt message) {
        System.out.println("线程:"+Thread.currentThread()+",消息内容:"+new String(message.getBody()));
    }
}

我们可以通过MessageExt获取消息中间件的消息,MessageExt除了存储消息内容本身之外,还存储了消息存储的大小,消息存储的队列,消息存储的主机等信息,如果我们需要这些额外信息的话,我们在监听类中就使用MessageExt类型接收,否则就是传过来什么类型,我们就使用什么类型去接收.

方式二:

@Component
@RocketMQMessageListener(topic = "helloTopic",consumerGroup = "helloGroup")
public class HelloMessageListener implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("线程:"+Thread.currentThread()+",消息内容:"+message);
    }
}

3.3.3 多线程消费测试

我们知道队列符合先进先出的原则,我们在生产者中发送10条的消息.

@Test
public void testSendMoreMsg(){
    String topic = "helloTopic";
    for(int i=0;i<10;i++){
         rocketMQTemplate.syncSend(topic, "同步消息"+i);
    }
    System.out.println("发送完毕");
}

然后我们启动消费者进行消息消费,观察一下控制台输出的结果

在这里插入图片描述

认真观察发现,队列先进先出的特性,这10条消息怎么不是按照0~9的顺序消费呢?

RocketMQ的消费者默认是多线程获取消息,然后执行消息。在一个队列中的消息确实是先进先出,但是由于CPU线程间切换的问题,后面取出消息的线程有可能更先的执行完,所以看到上面的执行结果.

3.4 消息发送三种方式

消息从生产者发送到消息中间件有三种形式.

消息发送指的是灰色框包裹的部分,特指发送的过程

在这里插入图片描述

3.4.1 同步消息

同步消息指的是生产者发送消息之后,需要等待消息在消息中间件持久化完毕之后并响应回来之后才能执行后续的代码.

在这里插入图片描述

代码如下:

String topic = "helloTopic";
SendResult sendResult = rocketMQTemplate.syncSend(topic, "同步消息");
System.out.println("发送结果:"+sendResult.getSendStatus());
// 其他的业务  (等待140ms)

适用场景: 重要的消息通知

3.4.2 异步消息

异步消息指的是生产者发送消息之后,不需要等待消息在消息中间件持久化完毕,可以直接执行后续代码,同时开启一个新的线程监听存储结果,当消息中间件存储完毕后,会通知消息生产者消息的存储结果.

在这里插入图片描述

代码如下:

String topic = "helloTopic";
System.out.println("线程:"+Thread.currentThread()+"消息发送前");
rocketMQTemplate.asyncSend(topic, "异步消息", new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.println("线程:"+Thread.currentThread()+",发送结果:"+sendResult.getSendStatus());
    }

    @Override
    public void onException(Throwable e) {
        e.printStackTrace();
    }
});
System.out.println("线程:"+Thread.currentThread()+"消息发送后");
// 其他的业务  (无需等待)
try {
    TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
    e.printStackTrace();
}

这段代码输出的结果:

在这里插入图片描述

可以看到发送结果是由另外一个线程执行的,而且是在消息发送后执行的.(因为是开了其他线程异步执行的缘故)

注意: 我们需要在最后面进行sleep睡眠,因为代码一发送代码就往下执行了,方法执行完了,整个程序就关闭了,这时候的SendCallback就无法获取到消息中间件返回的存储结果,所以我们需要稍微睡眠一下,等消息中间件存储完毕并响应结果之后再把程序关闭掉.

因为我们这里使用的是Junit的缘故,单元测试方法运行完程序就关闭了,所以我们需要使用sleep睡眠来测试结果。实际使用的时候,我们会在web环境使用,程序并不会关闭,所以实际情况并不需要进行睡眠.

适用场景: 异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。

可能出现的问题: 有可能业务执行完毕了,但是在SendCallback得到的结果是消息存储失败,这种情况我们就需要在业务中记录消息,然后当SendCallback得到结果是存储失败的时候进行消息重发.

同步和异步的理解:

  • 同步: 你自己排队购买iPhone 15时,这是同步操作。你必须亲自等待在排队中,直到轮到你购买。在这个过程中,你不能同时进行其他任务,因为你必须亲自参与购买过程。
  • 异步: 当你雇佣一个人帮你排队,然后自己去做其他事情,等他购买之后再付钱,这是异步操作。你雇佣的人代表你执行购买任务,而你可以自由地进行其他任务,如工作、休息或者做其他事情。你只需在他成功购买后支付费用,而不必等待整个购买过程。这种方式允许你并行执行多个任务,提高了效率。

3.4.3 一次性消息

这种方式主要用在不特别关心发送结果的场景,例如日志发送。

在这里插入图片描述

代码如下:

String topic = "helloTopic";
rocketMQTemplate.sendOneWay(topic,"一次性消息");
// 其他的业务  (无需等待)

3.5 消息刷盘机制

刷盘机制指的是消息中间件接收到生产者发送的消息后,将消息持久化的磁盘的过程.

也就是灰色框框起来的部分,无论是同步发送、异步发送,还是一次性发送,都涉及到消息持久化,千万不要和前面的同步发送和异步发送搞混了。

在这里插入图片描述

3.5.1 同步刷盘

同步刷盘指的是消息中间件接收到消息生产者发送的消息之后,需要将消息持久化到磁盘之后,才会给消息生产者响应消息存储的结果.

在这里插入图片描述

3.5.2 异步刷盘

异步刷盘指的是消息中间件接收到消息生产者发送的消息之后,将消息存储到操作系统的PageCache缓存之后就可以给消息生产者响应存储结果了,然后由操作系统将数据持久化到磁盘中(不由程序控制)

在这里插入图片描述

3.5.3 两种机制对比

  • 同步刷盘更加可靠,异步刷盘速度更快

  • 异步刷盘存在消息丢失的风险(比如消息存储到操作系统PageCache后,通知消息生产者消息已经存储成功了,在操作系统将消息持久化到磁盘之前,此时操作系统奔溃了。消息生产者得到的结果是消息存储成功了,但是消息却没有存到磁盘中.)

  • RocketMQ默认使用的是异步刷盘,可以在broker.conf文件中看到

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

    如果我们需要修改刷盘机制为同步刷盘,可以修改配置文件broker.conf中的flushDiskType

    flushDiskType = SYNC_FLUSH
    

    重启broker即可.

3.6 批量消息发送

在对吞吐率有一定要求的情况下,RocketMQ可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。

在这里插入图片描述

代码实现:

@Test
public void testSendBatchMsg(){
    String topic = "helloTopic";
    List<Message<String>> list = new ArrayList<>();
    for(int i=0;i<10;i++){
        list.add(MessageBuilder.withPayload("批量消息"+i).build());
    }
    SendResult sendResult = rocketMQTemplate.syncSend(topic, list, 3000);
    System.out.println(sendResult.getSendStatus());
}

注意:

这里调用非常简单,将消息打包成 Collection<Message> msgs 传入方法中即可,需要注意的是批量消息的大小不能超过 1MB(否则需要自行分割),最后一个参数表示发送的超时时间。

3.7 消息消费两种方式

指的是在消费者做集群的情况下,消息是如何消费的。

假设现在消息中间件中有5个消息,如果只有一个消费者的情况,那么这个消费者会消费所有的消息

在这里插入图片描述

如果有多个消费者情况,消息如何消费?

在这里插入图片描述

3.7.1 集群模式

默认是集群模式MessageModel.CLUSTERING,消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同。

在这里插入图片描述

我们要启动两个消费者,IDEA中需要启动两个程序,配置如下:

#命令行参数得优先级高于项目中得配置文件
-Dserver.port=8081

在这里插入图片描述

接着我们往这个主题发送10个消息

@Test
public void testSendOnewayMsg(){
    String topic = "helloTopic";
    for(int i=0;i<10;i++){
        rocketMQTemplate.sendOneWay(topic,"测试消息"+i);
    }
}

消息发送完毕之后,在ConsumerDemo8080的程序中看到的输出结果如下:

****

消息发送完毕之后,在ConsumerDemo8081的程序中看到的输出结果如下:

****

可以看到10条消息分别被两个消费者消费(队列中的消息只会给集群中的其中一台机器所消费)

注意: 有些同学做这个实验的时候,可能实验结果会和上面不一样.

  • 情况1: consumer8080消费6个消息 consumer8081消费4个消息
  • 情况2: consumer8080消费5个消息 consumer8081消费5个消息

这个情况,我们在3.7.3章节给大家讲解哈.

应用场景: 比如订单服务支付完毕之后给积分服务发送消息,进行增加积分的业务场景. 比如积分服务部署了2台机器,我们希望一个订单只能增加一次积分的情况,我们就应该使用集群的消费模式.

****

3.7.2 广播模式

消费者采用广播的方式MessageModel.BROADCASTING消费消息,每个消费者消费的消息都是相同的

在这里插入图片描述

我们需要修改监听器注解@RocketMQMessageListener中的消息监听模式MessageModel.BROADCASTING

@RocketMQMessageListener(topic = "helloTopic",consumerGroup = "helloGroup",messageModel = MessageModel.BROADCASTING)

重启consumer8080和consumer8081这两个服务,接着我们往这个主题发送10个消息

消息发送完毕之后,在ConsumerDemo8080的程序中看到的输出结果如下:

****

消息发送完毕之后,在ConsumerDemo8081的程序中看到的输出结果如下:

****

可以看到10条消息同时给两个消费者所消费(队列中的消息只会给集群中的所有机器消费)

应用场景: 比如实现类似Nacos的配置动态刷新,如果Nacos中某个配置修改了,所有用到该配置的服务都同步更新本地的缓存.

****

比如我们有seckill-server的服务,部署了两台的服务器,里面都有StockController,里面的stockLimit字段是从Nacos的远程配置中加载值,而且贴了@RefreshScope注解,如果Nacos更新了数值,本地的程序的字段的值也会同步更新. 这时候我们可以使用到广播的机制,通知到这两个服务,然后同时更新本地的缓存.

****

3.7.3 消息队列结构

我们在3.6.1演示的时候说了,在消费者集群的情况下,往MQ中发送10条消息. 有可能有这两种的情况:

  • 情况1: consumer8080消费6个消息 consumer8081消费4个消息
  • 情况2: consumer8080消费5个消息 consumer8081消费5个消息

出现这样的情况,其实和MQ队列的存储结构有关,我们接下来讲一下消息队列的结构.

假设现在主题下只有一个队列,会存在什么问题呢?

****

生产者可能做集群,每台机器中都会有很多线程会发送消息.

消息中间件使用多线程的方式接收消息,然后往消息队列中存储消息.

在这里插入图片描述

这样的话,我们多线程接收到消息生产者的请求之后,需要将消息都存储到消息队列中,这时候就涉及到线程安全的问题,要保证线程安全的话,我们就需要在往消息队列中写消息的时候进行加锁的处理。

加锁之后,同一时间就只能有一个线程往消息队列中存储数据,这样性能太差了,而且现在的服务器都是多核的处理器,同一时间只有一个线程操作,其他线程都等待的话,没有发挥多核处理器的性能。

RocketMQ每个Topic下默认有四个队列,这样的话,多线程同时可以往四个队列中写入数据,相当于四个线程并行的写入数据了. (这样设计降低了写的并发度,提高消息写入的速度)

这个可以在管控台中看到主题下的队列配置:

在这里插入图片描述

在这里插入图片描述

那么消息生产者往消息中间件发送消息,四个队列是如何存储的呢?

在消息生产者发送消息前,维护了一个index变量,会先计算该消息发送到哪个队列中,具体算法如下:

int queueID = (index++) % 队列长度

每次发送index会进行递增,然后模上队列长度,得到需要发送的队列ID,最终消息就存储到对应的队列中了.

第一次发送:

在这里插入图片描述

第二次发送:

****

那么多个队列的情况,消费者是如何处理这些队列的?

  • 情况1,只有一个消费者的情况

    ****

  • 情况2,有两个消费者集群的情况

    在这里插入图片描述

  • 情况3,有三个消费者集群的情况

    在这里插入图片描述

  • 情况4,有四个消费者集群的情况

    ****

  • 情况5,有五个消费者集群的情况

    在这里插入图片描述

    当消费者个数>读队列个数的话,会有消费者空闲,不会处理消息

理解了队列的结构之后,我们来分析一下之前的情况:

  • 情况1: consumer8080消费6个消息 consumer8081消费4个消息

    当我们发0号消息的时候,int queueId = index++ % 队列长度 计算出来的queueId=0或者queueId=2

    ****

  • 情况2: consumer8080消费5个消息 consumer8081消费5个消息

    当我们发0号消息的时候,int queueId = index++ % 队列长度 计算出来的queueId=1或者queueId=3

    ****

3.8 延迟消息

需求: 假设我们现在要实现订单的超时取消功能

解决方案:

  • 定时调度: 周期性从订单表过滤出超过指定时间的订单,若订单仍未支付,则修改订单状态为超时取消
  • Redis的Key失效事件监听: 创建订单的时候,同步往Redis设置一个Key并设置过期时间,当Key过期的时候,程序端监听到之后,若订单仍未支付,则修改订单状态为超时取消
  • Redis的Zset+定时调度: 创建订单的时候,往Redis的Zset存储订单号和订单时间(转成秒数作为分数),定时调度周期从Zset中过滤出超时的订单,若订单仍未支付,则修改订单状态为超时取消并将该订单号从Zset集合中删除
  • RocketMQ延时消息: 创建订单的时候往RocketMQ发送延时消息,达到指定时间之后,消费者才能消费该消息,若订单仍未支付,则修改订单状态为超时取消

应用场景: 支付订单超时取消,拼团超时取消,好友助力超时取消等.

RocketMQ4.x并不支持任意时间的延时,需要设置几个固定的延时等级,

从1s到2h分别对应着等级1到18

“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”;

可以在管控台集群的配置中看到

在这里插入图片描述

工作原理:

****

当我们发送消息的时候,设置了delayLevel的属性,消息在消息中间件处理的时候,判断到delayLevel>0,会将消息存储到SCHEDULE_TOPIC_XXXX这个Topic下对应的队列中(总共有18个队列,不同delayLevel的消息存储到不同的队列中),消息中间件内部使用Timer,检查18个队列的消息是否达到时间了,如果到达了,则将消息转发到原主题HelloTopic中,从而实现延时消息的功能.

  • 在consume-demo中定义延时消息监听器
@Component
@RocketMQMessageListener(consumerGroup = "delayGroup",topic = "delayTopic")
public class DelayMessageListener implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("消息消费时间:"+new Date()+",消息内容:"+message);
    }
}
  • 在producer-demo中编写发送方法
@Test
public void testSendDelayMsg(){
    String topic = "delayTopic";
    Message<String> msg = MessageBuilder.withPayload("延迟消息,发送时间:"+new Date()).build();
    rocketMQTemplate.syncSend(topic,msg,3000,3);
}

注意: 有可能第一次发送延时消息的时候时间不一定非常准确.后面的话就比较准确了.

- 消费者在启动的时候需要从NameServer中拉取Broker的IP和端口信息,Topic的路由信息(分布在哪台服务器,有几个队列)
- 我们是先启动消费者的,这时候从NameServer中是没有获取到delayTopic的路由信息的
- 当生产者发送消息的时候,是先把消息发送到SCHEDULE_TOPIC_XXXX中
- 消费者每隔30s会从NameServer中更新路由信息,在这30s中间,有可能延时消息已经到时间了,发送到delayTopic,此时消费者还没有该主题的路由信息
- 等到消费者更新路由信息之后,就可以收到消息了.

所以第一次发送的时候,有可能出现上面的问题,所以不是很准确.

3.9 消息过滤

在消息中间件的使用过程中,一个主题对应的消费者想要通过规则只消费这个主题下具备某些特征的消息,过滤掉自己不关心的消息,这个功能就叫消息过滤。

在这里插入图片描述

就如上图所描述的,生产者会向主题中写入形形色色的消息,有橙色的、黄色的、还有灰色的,而这个主题有两个消费者,第一个消费者只想要消费橙色的消息,第二个消费者只想要消费黄色的和灰色的消息,那么这个效果就需要通过消息过滤来实现。

应用场景

我们以常见的电商场景为例,来看看消息过滤在实际应用过程中起到的作用。

在这里插入图片描述

在功能层面,RocketMQ 支持两种过滤方式,Tag 标签过滤和 SQL 属性过滤

3.9.1 Tag标签过滤

Tag 标签过滤方式是 RocketMQ 提供的基础消息过滤能力,基于生产者为消息设置的 Tag 标签进行匹配。生产者在发送消息时,设置消息的 Tag 标签,消费者按需指定已有的 Tag 标签来进行匹配订阅。

在这里插入图片描述

  • 生产者代码
@Test
public void testSendTagMsg(){
    rocketMQTemplate.sendOneWay("tagFilterTopic:TagA","msgA");
    rocketMQTemplate.sendOneWay("tagFilterTopic:TagB","msgB");
    rocketMQTemplate.sendOneWay("tagFilterTopic:TagC","msgC");
}
  • 消费者代码
@Component
@RocketMQMessageListener(consumerGroup = "tagGroup",topic = "tagFilterTopic",selectorExpression = "TagA || TagC")
public class TagFilterMessageListener implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

3.9.2 SQL属性过滤

SQL 属性过滤是 RocketMQ 提供的高级消息过滤方式,通过生产者为消息设置的属性(Key)及属性值(Value)进行匹配。生产者在发送消息时可设置多个属性,消费者订阅时可设置SQL 语法的过滤表达式过滤多个属性。

  • 过滤语法
1. 数值比较:>, >=, <, <=, BETWEEN, =
2. 字符比较:=, <>, IN
3. 判空运算:IS NULL or IS NOT NULL
4. 逻辑运算:AND, OR, NOT

在这里插入图片描述

  • 生产者代码
@Test
public void testSendSQLMsg(){
    Message<String> msg1 = MessageBuilder.withPayload("美女A")
                            .setHeader("age","18").setHeader("weight","45").build();
    Message<String> msg2 = MessageBuilder.withPayload("美女B")
                            .setHeader("age","28").setHeader("weight","55").build();
    Message<String> msg3 = MessageBuilder.withPayload("美女A")
                            .setHeader("age","48").setHeader("weight","75").build();
    String topic = "sqlFilterTopic";
    rocketMQTemplate.sendOneWay(topic,msg1);
    rocketMQTemplate.sendOneWay(topic,msg2);
    rocketMQTemplate.sendOneWay(topic,msg3);
}
  • 消费者代码
@Component
@RocketMQMessageListener(consumerGroup = "sqlGroup",topic = "sqlFilterTopic",selectorExpression = "age>20 and weight>60",selectorType = SelectorType.SQL92)
public class SQLFilterMessageListener implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

运行之后报错信息如下:

Caused by: org.apache.rocketmq.client.exception.MQClientException: CODE: 1  DESC: The broker does not support consumer to filter message by SQL92

这个错误是由于RocketMQ默认是关闭了属性过滤功能的,如果需要使用该功能,需要开启enablePropertyFilter的属性,将该属性置为true才可以。也就是我们需要在RocketMQ的配置文件中/usr/local/rocketmq-4.9/conf/broker.conf添加如下配置:

enablePropertyFilter=true

然后重启Broker即可.

3.10 顺序消息

消息有序指的是,消费者端消费消息时,需按照消息的发送顺序来消费,即先发送的消息,需要先消费(FIFO)。

应用场景:

  • 情景1: 证券、股票交易撮合场景,坚持按照先出价先交易的原则,下游处理订单的系统需要严格按照出价顺序来处理订单。
  • 情景2: 数据实时增量同步场景,以数据库变更增量同步场景为例,上游源端数据库按需执行增删改操作,将二进制操作日志作为消息,通过RocketMQ 传输到下游搜索系统,下游系统按顺序还原消息数据,实现状态数据按序刷新。如果是普通消息则可能会导致状态混乱,和预期操作结果不符,基于顺序消息可以实现下游状态和上游操作结果一致。

RocketMQ普通消息是不具备顺序性

  • 生产者采取轮询的方式往Topic的4个队列中发送消息,无法保证出队列的顺序

  • 消费者使用的是多线程的方式消费消息,有可能后面出来的消息更先执行完,无法保证消费的顺序

    在这里插入图片描述

    在消费者使用的是多线程消费消息,由于CPU时间片的问题,后面取出来的消息可能比前面取出的消息更先执行完

如何保证消息的顺序性

RocketMQ 的消息的顺序性分为两部分,生产顺序性和消费顺序性。

  • 生产顺序性 :将需要顺序消费的消息放入到同一个队列中,保证消息是满足先进先出特性

  • 消费顺序性:在消费端需要采取单线程绑定队列的方式进行消息的消费.

在这里插入图片描述

我们通过代码案例来加深对顺序消息的理解

需求: 通常创建订单后,会经历一系列的操作:【订单创建 -> 订单支付 -> 订单发货 -> 订单配送 -> 订单完成】。在创建完订单后,会发送五条消息到MQ Broker中,消费的时候要按照【订单创建 -> 订单支付 -> 订单发货 -> 订单配送 -> 订单完成】这个顺序去消费,这样的订单才是有效的。

  • 定义OrderStep实体类
public class OrderStep {
    private String orderId;
    private String desc;
    public String getOrderId() {
        return orderId;
    }
    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }
    public String getDesc() {
        return desc;
    }
    public void setDesc(String desc) {
        this.desc = desc;
    }
    @Override
    public String toString() {
        return "OrderStep{" +
                "orderId=" + orderId +
                ", desc='" + desc + '\'' +
                '}';
    }
}
  • 定义OrderUtil工具类
public class OrderUtil {
    /**
     * 生成模拟订单数据
     */
    public static List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();
        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("支付");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("发货");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("配送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
        return orderList;
    }
}

我们先测试普通消息是否可以实现顺序消费呢?

  • 生产者代码
@Test
public void testSendOrderlyMsg(){
    String topic = "orderlyTopic";
    List<OrderStep> orderSteps = OrderUtil.buildOrders();
    for(OrderStep step:orderSteps){
        rocketMQTemplate.sendOneWay(topic,step.toString());
    }
}
  • 消费者代码
@Component
@RocketMQMessageListener(consumerGroup = "orderlyGroup",topic = "orderlyTopic")
public class OrderlyMessageListener implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        System.out.println("线程:"+Thread.currentThread()+",队列ID:"+messageExt.getQueueId()+",消息内容:"+new String(messageExt.getBody()));
    }
}

运行之后结果如下:

在这里插入图片描述

明显可以看到,是不符合顺序消费的定义的.

接着我们就需要在生产者中定义消息队列选择器,需要定义消息发送到哪个队列中.

@Test
public void testSendOrderlyMsg(){
    //定义消息队列选择器
    MessageQueueSelector selector = new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> mqs, org.apache.rocketmq.common.message.Message msg, Object arg) {
            String orderId = (String) arg;
            //使用求模的方式,相同的订单号计算出来的值是一样的,所以相同订单的消息最终会放入到同一个队列中
            int queueId = (int) (Long.parseLong(orderId) % mqs.size());
            return mqs.get(queueId);
        }
    };
    //将消息队列选择器设置到rocketMQTemplate中
    rocketMQTemplate.setMessageQueueSelector(selector);
    String topic = "orderlyTopic";
    List<OrderStep> orderSteps = OrderUtil.buildOrders();
    for(OrderStep step:orderSteps){
        //发送的时候,需要使用Orderly结尾的方法
        rocketMQTemplate.sendOneWayOrderly(topic,step.toString(),step.getOrderId());
    }
}

在消费者端需要使用单线程绑定队列的方式

@Component
@RocketMQMessageListener(consumerGroup = "orderlyGroup",topic = "orderlyTopic",consumeMode = ConsumeMode.ORDERLY)
public class OrderlyMessageListener implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt messageExt) {
        System.out.println("线程:"+Thread.currentThread()+",队列ID:"+messageExt.getQueueId()+",消息内容:"+new String(messageExt.getBody()));
    }
}

执行完消费的结果如下:

在这里插入图片描述

可以看到确实按照我们发送的顺序进行消费了

RocketMQ的顺序消息属于局部有序而非全局有序:

比如我现在有两组订单,这两组订单分别是A1,B1,C1和A2,B2,C2 我们要实现这两组订单是顺序消费的.

在这里插入图片描述

我们发送的时候消息发送顺序如下,订单组1发送顺序是按照A1,B1,C1顺序发送,订单组2发送的顺序也是A2,B2,C2的顺序.

在这里插入图片描述

在发送的时候,根据消息选择器,可能存储的结果如下:

在这里插入图片描述

因为这6个消息被分配到了2个队列中,分别有线程2和线程3进行消费.

这时候有可能A1先执行完,也可能A2先执行完,可能的消费顺序如下:

在这里插入图片描述

这时候可以看到全局的发送顺序和全局的消费顺序是不一致的.

但是在同一个队列中的顺序是有序的.

比如A1先执行,然后才到B1,最后到C1

A2先执行,然后才到B2,最后到C2

提问: 全局有序如何实现?

3.11 其他概念

3.11.1 不同消费组订阅问题

RocketMQ在消费者消费完消息之后,并不是直接删除掉该消息,而是在consumerOffset.json文件中标记该消费者的消费位置. 在集群模式下,消息的消费是根据消费组定的,如下图所示:

在这里插入图片描述

在consumerOffset.json文件中记录了每个消费组的消息消费位置,如下图所示:

在这里插入图片描述

如果此时新增一个消费组XGroup,那么这个组会重新消费A,B,C三个消息.

3.11.2 消息重试

  • 消息发送重试

    - 同步发送失败重试次数,默认为2
    - 异步发送失败重试次数,默认为2
    - 一次性发送,不会进行重试
    
  • 消息消费重试

    对于消费失败的消息,广播模式下仅仅是对于消费失败的消息打印日志,并不会重试。

    针对集群模式,消息重试机制如下:

在这里插入图片描述

  1. 消费者向Broker发送消息重试的请求

  2. Broker找到该消息并拷贝一个新的消息,设置消息主题为%RETRY% + 消费者组名,如果此时该消息的重试次数超过最大重试次数或者延迟级别小于0,那么就把这个新的消息写入到死信队列中

  3. 设置延迟级别= 重试次数 + 3

  4. 设置新的消息重试次数+1,

  5. 这个新消息会进入到延迟主题SCHEDULE_TOPIC_XXXX中,当到达了延迟时间的时候该消息就会再一次被发送到 %RETRY% + 消费者组名 这个主题

  6. 消费者刚启动的时候,会把订阅 %RETRY% + 消费者组名这个主题

  • 消息重试时间间隔
10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

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

3.11.3 幂等性

比如Broker发送Consumer超时(默认15分钟)后重新发送,可能Consumer已经执行完业务了,此时消息重新发送过来的话,我们的业务就会重新执行一次。所以在消费者这段的业务接口实现,我们需要实现幂等性.

3.11.4 死信队列

当一条消息初次消费失败,RocketMQ 会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息。此时,RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

3.11.5 消息的删除

为了避免内存与磁盘的浪费,RocketMQ有专门的删除机制来删除过期的文件。

清理的逻辑是:如果非当前写文件在一 定时间间隔内没有再次被更新 ,则认为是过期文件,可以被删除, RocketMQ 不会关注这个文件上的消息是否全部被消费。

通过在 Broker配置文件中设置 fileReservedTime 来改变过期时间,默认为48,单位为小时

可以通过deleteWhen设置一天执行一次删除文件的操作,默认是凌晨4点。

四、消息积压面试题

  • 如何处理消息队列的消息积压问题

消息积压是因为⽣产者的⽣产速度,⼤于消费者的消费速度。遇到消息积压问题时,我们需要先排查,是不是有bug产⽣了。

如果不是bug,我们看是否可以优化⼀下消费的逻辑。如果还是慢,我们可以考虑⽔平扩容,增加Topic的队列数,和消费组机器的数量,提升整体消费能⼒

如果是bug导致⼏百万消息持续积压⼏⼩时。有如何处理呢?需要解决bug,临时紧急扩容,⼤概思路如下:

1. 先修复consumer消费者的问题,以确保其恢复消费速度,然后将现有consumer 都停掉。
2. 新建⼀个 topic,队列数是原来的 10 倍
3. 然后写⼀个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时
的处理,直接均匀轮询写⼊临时建⽴好的10倍数量的queue。
4.这样处理完之后原来的Topic就没有什么消息了,此时可以把consumer消费者启动起来,此时新的业务就正常了.
5. 接着临时征⽤ 10 倍的机器来部署 consumer消费新Topic的消息。这种做法相当于是临时将 queue 资源和 consumer资源扩⼤10 倍,以正常的 10 倍速度来消费积压数据。

核心: 先保障新的业务能正常运行,然后积压的数据,先放入到新的队列中(不影响原有业务),然后开启10倍的消费者消费积压的消息.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值