微服务实战系列之SpringCloud Alibaba学习(五)

微服务实战系列之SpringCloud Alibaba:

  • 微服务实战系列之SpringCloud Alibaba学习(一)
  • 微服务实战系列之SpringCloud Alibaba学习(二)
  • 微服务实战系列之SpringCloud Alibaba学习(三)
  • 微服务实战系列之SpringCloud Alibaba学习(四)
  • 微服务实战系列之SpringCloud Alibaba学习(五)

在微服务实战系列之SpringCloud Alibaba学习(一)中,学习了搭建三大微服务并完成交互开发与测试。

在微服务实战系列之SpringCloud Alibaba学习(二)中,针对微服务会遇到的一些问题,使用了一些微服务组件来解决,实现了用户微服务、商品微服务和订单微服务之间的远程调用,并且实现了服务调用的负载均衡。也基于阿里开源的Sentinel实现了服务的限流与容错,并详细介绍了Sentinel的核心技术与配置规则 。如下:

  • 使用Nacos来实现服务的注册与发现
  • 使用Ribbon来实现服务调用的负载均衡
  • 使用Sentinel实现接口限流

在微服务实战系列之SpringCloud Alibaba学习(三)中,简单介绍了服务网关,并对SpringCloud Gateway的核心架构进行了简要说明,也在项目中整合了SpringCloud Gateway网关实现了通过网关访问后端微服务,另外,也基于SpringCloud Gateway整合Sentinel实现了网关的限流功能。

  • 使用SpringCloud Gateway网关实现了通过网关访问后端微服务以及限流功能

在微服务实战系列之SpringCloud Alibaba学习(四)中,学习分布式链路追踪技术与解决方案。随后在项目中整合Sleuth实现了链路追踪,并使用Sleuth整合ZipKin实现了分布式链路追踪的可视化

在微服务实战系列之SpringCloud Alibaba学习(五)中,学习消息服务MQ以及项目整合RocketMQ

文章目录

      • 1. 消息服务MQ
        • 1.1 MQ的使用场景
          • 1.1.1 异步解耦
          • 1.1.2 流量削峰
        • 1.2 引入MQ后的注意事项
        • 1.3 MQ选型对比
      • 2. 项目整合RocketMQ
        • 2.1 RocketMQ环境准备
          • 2.1.1 源码编译安装RocketMQ
          • 2.1.2 测试RocketMQ环境
          • 2.1.3 源码编译RocketMQ控制台
          • 2.1.4 验证RocketMQ控制台
        • 2.2 编码测试RocketMQ
          • 2.2.1 导入RocketMQ依赖
          • 2.2.2 编写生产者代码
          • 2.2.3 编写消费者代码
          • 2.2.4 测试消息的生产与消费
        • 2.3 项目整合RocketMQ
          • 2.3.1 用户微服务整合RocketMQ
          • 2.3.2 订单微服务整合RocketMQ
          • 2.3.3 测试项目整合的RocketMQ
      • 3. RocketMQ核心技术
        • 3.1 RocketMQ的核心技术
        • 3.2 RocketMQ基本概念
        • 3.3 RocketMQ特性

1. 消息服务MQ

1.1 MQ的使用场景

MQ的英文全称是Message Queue,翻译成中文就是消息队列,队列实现了先进先出(FIFO)的消息模型。通过消息队列,我们可以实现多个进程之间的通信,例如,可以实现多个微服务之间的消息通信。MQ的最简模型就是生产者生产消息,将消息发送到MQ,消息消费者订阅MQ,消费消息。
在这里插入图片描述
MQ的使用场景通常包含:异步解耦流量削峰

1.1.1 异步解耦

关于异步的场景,我们这里举一个用户下单成功后,向用户发送通知消息,为用户增加积分和优惠券的场景。

同步耦合场景分析

如果是同步调用的场景,则具体业务为:当用户提交订单成功后,订单系统会调用通知系统为用户发送消息通知,告知用户下单成功,订单系统调用积分系统为用户增加积分,订单系统调用优惠券系统为用户增加优惠券。整个调用流程如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第1张图片
通过上图的分析,可以看到,用户调用订单系统下单时,总共会经过8个步骤。并且每个步骤都是紧密耦合在一起串行执行,如下所示。

微服务实战系列之SpringCloud Alibaba学习(五)_第2张图片
此时,订单系统、通知系统、积分系统和优惠券系统是紧密耦合在一起的,订单系统下单、通知系统发通知、积分系统发积分和优惠券系统发优惠券,四个任务全部完成后,才会给用户返回提交订单的结果信息。

用户提交订单花费的总时间为调用订单系统下单的时间+订单系统调用通知系统发送通知的时间+订单系统调用积分系统发放积分的时间+订单系统调用优惠券系统发放优惠券的时间。

注意:这里为了更好的说明系统之间串行执行的问题,忽略了网络的延迟时间。

这种串行化的系统执行方式,在高并发、大流量场景下是不可取的。另外,如果其中一个系统异常或者宕机,势必会影响到订单系统的可用性。在系统维护上,只要任意一个系统的接口发生变动,订单系统的逻辑也要跟着发生变动。

异步解耦场景分析

既然在高并发、大流量场景下使用订单系统直接串行调用通知系统、积分系统和优惠券系统的方式不可取。那我们是否能够使用异步解耦的方式呢。

其实,在用户提交订单的场景中,用户最关心的是自己的订单是否提交成功,由于下单时,订单系统会直接返回是否下单成功的提示。

对于通知、积分和优惠券可以以异步的方式延后一小段时间执行。并且通知系统、积分系统和优惠券系统之间不存在必然的业务关联逻辑,它们之间可以以并行的方式执行。

所以,可以使用MQ将订单系统与通知系统、积分系统和优惠券系统进行解耦,用户调用订单系统的接口下单时,订单系统向数据库写入订单数据后,向MQ写入消息,就可以直接返回给用户下单成功的提示,此时通知系统、积分系统和优惠券系统都订阅MQ中的消息,收到消息后各自执行自身的业务逻辑即可。

微服务实战系列之SpringCloud Alibaba学习(五)_第3张图片
当引入MQ进行异步解耦之后,用户调用订单系统的接口下单,订单系统执行完业务逻辑将订单数据入口,会向MQ发送一条消息,随后便直接返回用户下单成功的提示。通知系统、积分系统和优惠券系统会同时订阅MQ中的消息,当收到消息时,它们各自会执行自身的业务逻辑,并且它们是以并行的方式执行各自的业务逻辑。
微服务实战系列之SpringCloud Alibaba学习(五)_第4张图片
从执行的时间线上可以看出,当引入MQ进行异步解耦之后,通知系统、积分系统、优惠券系统和订单系统回复响应都是并行执行的,大大提高系统的执行性能。

并且解耦后,任意一个系统异常或者宕机,都不会影响到订单系统的可用性。只要订单系统与其他系统提前约定好发送的消息格式和消息内容,后续任意一个系统的业务逻辑变动,几乎都不会影响到订单系统的逻辑。

1.1.2 流量削峰

MQ在高并发、大流量的场景下可以用作削峰填谷的利器,例如,12306的春运抢票场景、高并发秒杀场景、双十一和618的大促场景等。

在高并发、大流量业务场景下,瞬间会有大量用户的请求涌入系统,如果不对这些流量做处理的话,直接让这些流量进入下游系统,则很可能由于下游系统无法支撑如此高的并发而导致系统崩溃或宕机。为了解决这些问题,可以引入MQ进行流量的削峰填谷
微服务实战系列之SpringCloud Alibaba学习(五)_第5张图片
将流量发送到MQ中后,下游系统根据自身的处理能力进行消费即可。保证了下游系统的高可用性。

1.2 引入MQ后的注意事项

引入MQ最大的优点就是异步解耦流量削峰,但是引入MQ后也有很多需要注意的事项和问题,主要包括:系统的整体可用性降低、系统的复杂度变高、引入了消息一致性的问题。

系统的整体可用性降低

在对一个系统进行架构设计时,引入的外部依赖越多,系统的稳定性和可用性就会降低。系统中引入了MQ,部分业务就会出现强依赖MQ的现象,此时,如果MQ宕机,则部分业务就会变得不可用。所以,引入MQ时,我们就要考虑如何实现MQ的高可用。

系统的复杂度变高

引入MQ后,会使之前的同步接口调用变成通过MQ的异步调用,在实际的开发过程中,异步调用会比同步调用复杂的多。并且异步调用出现问题后,重现问题,定位问题和解决问题都会比同步调用复杂的多。

并且引入MQ后,还要考虑如何保证消息的顺序等问题。

消息一致性问题

引入MQ后,不得不考虑的一个问题就是消息的一致性问题。这期间就要考虑如何保证消息不丢失,消息幂等和消息数据处理的幂等性问题。

1.3 MQ选型对比

目前,在行业内使用的比较多的MQ包含RabbitMQ、Kafka和RocketMQ。这里,我将三者的对比简单整理了个表格,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第6张图片

2. 项目整合RocketMQ

2.1 RocketMQ环境准备

RocketMQ是阿里开源的消息中间件,目前是Apache下的顶级项目。正式在项目中接入RocketMQ之前,我们需要搭建RocketMQ的环境。这里呢,我把搭建RocketMQ的基础环境分为两个部分:搭建RocketMQ环境和搭建RocketMQ控制台。

「注意:这里都是先下载RocketMQ的源码和RocketMQ控制台的源码,然后对源码进行编译后,再搭建的。目的也是让小伙伴们能够跟着实现手动编译RocketMQ的源码,另外,编译RocketMQ源码和控制台源码需要JDK1.8+Maven。」

2.1.1 源码编译安装RocketMQ

(1)到链接 https://github.com/apache/rocketmq/releases/tag/rocketmq-all-4.9.3下载RocketMQ 4.9.3版本的源码。下载并解压后的源码如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第7张图片(2)打开cmd命令行,进入RocketMQ的解压目录,我这里是E:\Application\RocketMQ\rocketmq-rocketmq-all-4.9.3目录,然后在cmd命令行输入如下命令开始编译打包。

mvn clean install -Dmaven.test.skip=true -Prelease-all

编译过程如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第8张图片
编译打包成功后,如下图所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第9张图片
(3)编译成功后,会在RocketMQ解压目录下的distribution目录下的target目录下生成RocketMQ的安装包,在我电脑上的目录就是:E:\Application\RocketMQ\rocketmq-rocketmq-all-4.9.3\distribution\target。如下所示。

微服务实战系列之SpringCloud Alibaba学习(五)_第10张图片
这样,我们就自己下载RocketMQ的源码,并打包成功了。

「注意:这里,为了方便,我还是将RocketMQ部署到我本机Windows操作系统上,小伙伴们也可以将之前的Nacos、Sentinel和这次的RocketMQ都部署在Linux操作系统上,部署方式几乎与在Windows操作系统一样。」

(4)将编译出的安装包,解压到电脑的某个目录下,例如我解压后的目录为:E:\Application\microservices\RocketMQ\rocketmq-4.9.3。

(5)在RocketMQ的解压目录下的conf目录下修改broker.conf文件,修改后的文件内容如下所示。

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

# 自动创建Topic
autoCreateTopicEnable=true
# nameServ地址
namesrvAddr=127.0.0.1:9876
# 存储路径
storePathRootDir=E:/RocketMQ/data/rocketmq/dataDir
# commitLog路径
storePathCommitLog=E:/RocketMQ/data/rocketmq/dataDir/commitlog
# 消息队列存储路径
storePathConsumeQueue=E:/RocketMQ/data/rocketmq/dataDir/consumequeue
# 消息索引存储路径
storePathIndex=E:/RocketMQ/data/rocketmq/dataDir/index
# checkpoint文件路径
storeCheckpoint=E:/RocketMQ/data/rocketmq/dataDir/checkpoint
# abort文件存储路径
abortFile=E:/RocketMQ/data/rocketmq/dataDir/abort

小伙伴们可以根据自己的实际情况,自行修改上述文件中配置的目录地址。

(6)非常重要的一步,在启动RocketMQ之前,需要配置下ROCKETMQ_HOME环境变量,否则在启动RocketMQ的时候,会提示如下错误信息。

E:\Application\microservices\RocketMQ\rocketmq-4.9.3\bin>mqnamesrv.cmd
Please set the ROCKETMQ_HOME variable in your environment!

「提示:设置ROCKETMQ_HOME环境变量。」

接下来,就在系统环境变量中,设置下ROCKETMQ_HOME的环境变量,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第11张图片
(7)配置完RocketMQ的环境变量后,打开cmd命令行,进入RocketMQ的bin目录,例如,我电脑的目录是:E:\Application\microservices\RocketMQ\rocketmq-4.9.3\bin。执行mqnamesrv.cmd命令启动NameServer,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第12张图片(8)重新打开一个cmd命令行,进入RocketMQ的bin目录,输入mqbroker.cmd -n localhost:9876命令启动RocketMQ的Broker服务,如下所示。
在这里插入图片描述

2.1.2 测试RocketMQ环境

RocketMQ内置了大量的测试案例,并且这些测试案例可以通过RocketMQ的bin目录下的tools.cmd命令进行测试。接下来,我们就使用RocketMQ自带的tools.cmd命令测试RocketMQ的环境。

(1)启动生产者程序向RocketMQ发送消息。

重新打开cmd命令行,进入RocketMQ的bin目录,在命令行输入如下命令调用RocketMQ自带的生产者程序向RocketMQ发送消息。

set NAMESRV_ADDR=localhost:9876
tools.cmd org.apache.rocketmq.example.quickstart.Producer

可以看到,执行完上述两条命令后,生产者程序开始向RocketMQ发送消息。
微服务实战系列之SpringCloud Alibaba学习(五)_第13张图片(2)启动消费者程序消费RocketMQ中的消息。

重新打开cmd命令行,进入RocketMQ的bin目录,在命令行输入如下命令调用RocketMQ自带的消费者程序消费RocketMQ中的消息。

set NAMESRV_ADDR=localhost:9876
tools.cmd org.apache.rocketmq.example.quickstart.Consumer

可以看到,执行完上述两条命令后,消费者程序开始消费RocketMQ中的消息。
微服务实战系列之SpringCloud Alibaba学习(五)_第14张图片说明我们使用源码编译搭建RocketMQ环境成功了。

2.1.3 源码编译RocketMQ控制台

这里需要注意的是:RocketMQ控制台本质上是一个SpringBoot程序,启动后默认监听的端口是8080。RocketMQ的新版控制台已经从RocketMQ的rocketmq-externals项目中分离出来了。也就是说,新版的RocketMQ控制台已经从https://github.com/apache/rocketmq-externals链接所示的项目中分离出来,新版控制台的链接地址为:https://github.com/apache/rocketmq-dashboard。

(1)从链接https://github.com/apache/rocketmq-dashboard下载新版的RocketMQ控制台源码。下载后解压。

(2)进入到RocketMQ控制台源码解压目录的src/main/resources目录下,编辑application.yml文件,修改namesrvAddrs地址,去掉多余的namesrvAddrs地址。

application.yml文件中原来的配置如下所示。

rocketmq:
  config:
    # if this value is empty,use env value rocketmq.config.namesrvAddr  NAMESRV_ADDR | now, default localhost:9876
    # configure multiple namesrv addresses to manage multiple different clusters
    namesrvAddrs:
      - 127.0.0.1:9876
      - 127.0.0.2:9876

将127.0.0.2:9876删除或者注释掉,如下所示。

rocketmq:
  config:
    # if this value is empty,use env value rocketmq.config.namesrvAddr  NAMESRV_ADDR | now, default localhost:9876
    # configure multiple namesrv addresses to manage multiple different clusters
    namesrvAddrs:
      - 127.0.0.1:9876
    #  - 127.0.0.2:9876

RocketMQ控制台启动时默认监听的端口是8080,由于我们项目中订单微服务监听的端口也是8080,所以,将RocketMQ控制台监听的端口修改为10003,修改前的配置如下所示。

server:
  port: 8080

修改后的配置如下所示。

server:
  port: 10003

(3)修改完application.yml文件后,打开cmd命令行,进入RocketMQ控制台源码的根目录,输入如下Maven命令开始编译RocketMQ控制台的源码。

mvn clean install -Dmaven.test.skip=true

编译过程如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第15张图片

(4)编译完成后,会在RocketMQ控制台源码的根目录下生成target目录,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第16张图片
进入target目录后,可以看到生成了rocketmq-dashboard-1.0.1-SNAPSHOT.jar文件,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第17张图片这个jar文件就是RocketMQ控制台的运行文件。

(5)重新打开cmd命令行,进入rocketmq-dashboard-1.0.1-SNAPSHOT.jar文件所在的命令,在命令行直接输入如下命令启动RocketMQ控制台程序。

java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar
2.1.4 验证RocketMQ控制台

在浏览器中输入http://localhost:10003后,出现如下画面说明RocketMQ启动成功。
微服务实战系列之SpringCloud Alibaba学习(五)_第18张图片
界面默认是英文,我们也可以点击右上角的changeLanguage切换语言,切换成中文显示,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第19张图片
选择主题菜单想后如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第20张图片可以看到目前RocketMQ中存在一个名称为TopicTest的主题,点击TopicTest主题的状态按钮,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第21张图片
会显示TopicTest主题的消息队列信息,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第22张图片可以看到,正确显示出了TopicTest主题的消息队列信息,说明RocketMQ控制台启动成功了。

2.2 编码测试RocketMQ

我们使用RocketMQ自带的生产者和消费者程序实现了消息的生成与消费,为了让小伙伴们能够更加直观的感受到消息中间件在项目中的作用,接下来,我们自己编码测试下RocketMQ。

2.2.1 导入RocketMQ依赖

在用户微服务shop-user的pom.xml中,添加RocketMQ相关的依赖,如下所示。

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.3</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.5.2</version>
</dependency>
2.2.2 编写生产者代码

在用户微服务的sec/test/java目录下新建io.binghe.shop.rocketmq.test包,在包下创建RocketMQProducer类,作为RocketMQ的生产者,代码如下所示。

/**
 * @author binghe
 * @version 1.0.0
 * @description RocketMQ生产者
 */
public class RocketMQProducer {

    public static void main(String[] args) throws Exception {
        //创建消息生产者
        DefaultMQProducer producer = new DefaultMQProducer("bingheProducerGroup");
        //设置NameServer地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        //启动生产者
        producer.start();
        //构建消息对象
        Message message = new Message("bingheTopic", "bingheTag", "Hello RocketMQ".getBytes());
        System.out.println("生产者发出的消息为:" + JSONObject.toJSONString(message));
        //发送消息并接收结果
        SendResult sendResult = producer.send(message);
        //打印结果信息
        System.out.println("生产者收到的发送结果信息为:" + JSONObject.toJSONString(sendResult));
        //关闭生产者
        producer.shutdown();
    }
}

生产者的代码比较简单,这里就不再赘述了。

2.2.3 编写消费者代码

io.binghe.shop.rocketmq.test包下新建RocketMQConsumer类,作为RocketMQ的消费者,代码如下所示。

/**
 * @author binghe
 * @version 1.0.0
 * @description RocketMQ消费者
 */
public class RocketMQConsumer {

   public static void main(String[] args) throws Exception {
       try{
           //创建消息消费者
           DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("bingheConsumerGroup");
           //设置NameServer地址
           consumer.setNamesrvAddr("127.0.0.1:9876");
           //订阅bingheTopic主题
           consumer.subscribe("bingheTopic", "*");
           //设置消息监听,当收到消息时RocketMQ会回调消息监听
           consumer.registerMessageListener(new MessageListenerConcurrently() {
               @Override
               public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list,
                                                               ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                   //打印消息消费者收到的RocketMQ消息
                   System.out.println("消费者收到的消息为:" + list);
                   //返回消息消费成功的标识
                   return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
               }
           });
           //启动消费者
           consumer.start();
           System.out.println("消费者启动成功");
       }catch (Exception e){
           e.printStackTrace();
       }
   }
}
2.2.4 测试消息的生产与消费

(1)为了便于观察,这里我们先启动消费者程序RocketMQConsumer,启动RocketMQConsumer后会在IDEA的控制台打印如下信息。

消费者启动成功

说明消费者启动成功了。

(2)运行生产者程序RocketMQProducer,运行后RocketMQProducer程序控制台会输出如下信息。

生产者发出的消息为:{"body":"SGVsbG8gUm9ja2V0TVE=","delayTimeLevel":0,"flag":0,"properties":{"WAIT":"true","TAGS":"bingheTag"},"tags":"bingheTag","topic":"bingheTopic","waitStoreMsgOK":true}
生产者收到的发送结果信息为:{"messageQueue":{"brokerName":"DESKTOP-PSKC7T1","queueId":1,"topic":"bingheTopic"},"msgId":"C0A8006F538418B4AAC25B9EDDAC0000","offsetMsgId":"C0A8B80100002A9F0000000000036B16","queueOffset":2,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}

说明生产者程序RocketMQProducer成功将消息发送到RocketMQ。

(3)接下来,再看下消费者程序RocketMQConsumer的控制台,如下所示。

消费者收到的消息为:[MessageExt [queueId=1, storeSize=206, queueOffset=2, sysFlag=0, bornTimestamp=1652871538093, bornHost=/192.168.184.1:52915, storeTimestamp=1652871538099, storeHost=/192.168.184.1:10911, msgId=C0A8B80100002A9F0000000000036B16, commitLogOffset=224022, bodyCRC=1774740973, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='bingheTopic', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=3, CONSUME_START_TIME=1652871538103, UNIQ_KEY=C0A8006F538418B4AAC25B9EDDAC0000, CLUSTER=DefaultCluster, WAIT=true, TAGS=bingheTag}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81], transactionId='null'}]]

说明生成者发送到RocketMQ的消息,被消费者成功消费到了。

2.3 项目整合RocketMQ

我们在项目中模拟一个用户成功下单后,为用户发送通知,通知用户下单成功的逻辑,具体的流程就是下单成功后将订单的信息发送到RocketMQ,然后用户微服务订阅RocketMQ的消息,接收到消息后进行打印。

2.3.1 用户微服务整合RocketMQ

(1)编码测试RocketMQ时,导入了RocketMQ的依赖,这里就不用再次导入了。

(2)在用户微服务shop-user的application.yml文件中添加如下RocketMQ的配置。

rocketmq:
  name-server: 127.0.0.1:9876

(3)在用户微服务shop-user中创建io.binghe.shop.user.rocketmq包,在包下创建RocketConsumeListener,实现org.apache.rocketmq.spring.core.RocketMQListener接口,具体代码如下所示。

/**
 * @author binghe
 * @version 1.0.0
 * @description 监听消费
 */
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "user-group", topic = "order-topic")
public class RocketConsumeListener implements RocketMQListener<Order> {
    @Override
    public void onMessage(Order order) {
        log.info("用户微服务收到了订单信息:{}", JSONObject.toJSONString(order));
    }
}

其中,RocketConsumeListener类上的@RocketMQMessageListener注解,表示当前类是一个RocketMQ的消费者,在@RocketMQMessageListener注解中配置了消费者组为user-group,主题为order-topic。

至此,用户微服务整合RocketMQ完毕。

2.3.2 订单微服务整合RocketMQ

(1)在订单微服务shop-order的pom.xml文件中添加RocketMQ的依赖,如下所示。

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.3</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.5.2</version>
</dependency>

(2)在订单微服务shop-order的application.yml文件中添加如下配置。

rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: order-group

(3)将io.binghe.shop.order.service.impl.OrderServiceV6Impl类,复制一份成io.binghe.shop.order.service.impl.OrderServiceV7Impl类,接下来,在io.binghe.shop.order.service.impl.OrderServiceV7Impl类中操作。

io.binghe.shop.order.service.impl.OrderServiceV7Impl类上的@Service注解中的名称修改为orderServiceV7,如下所示。

@Slf4j
@Service("orderServiceV7")
public class OrderServiceV7Impl implements OrderService {
    //省略具体代码
}

(4)在io.binghe.shop.order.service.impl.OrderServiceV7Impl类中,注入RocketMQTemplate对象,如下所示。

@Autowired
private RocketMQTemplate rocketMQTemplate;

(5)在io.binghe.shop.order.service.impl.OrderServiceV7Impl#saveOrder()方法中,提交订单成功后将订单信息写入RocketMQ,如下所示。

@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrder(OrderParams orderParams) {
    //省略上面所有代码
    rocketMQTemplate.convertAndSend("order-topic", order);
}

(6)在io.binghe.shop.order.controller.OrderController中,将注入的OrderService的名称修改成orderServiceV7,如下所示。

@Autowired
@Qualifier(value = "orderServiceV7")
private OrderService orderService;

「注意:订单微服务shop-order中,修改后的代码见源码工程,这里不再粘贴完整的源代码。」

2.3.3 测试项目整合的RocketMQ

(1)分别启动Nacos,Sentinel,ZipKin和RocketMQ。

(2)分别启动用户微服务、商品微服务、订单微服务和网关服务。

(3)在浏览器中输入localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。

图片

(4)查看用户微服务shop-user的控制台,发现会输出订单的信息,如下所示。

2022-05-18 20:37:26.440  INFO [server-user,,,] 18064 --- [MessageThread_1] i.b.s.u.rocketmq.RocketConsumeListener   : 用户微服务收到了订单信息:{"address":"北京","id":13176882400989185,"phone":"13212345678","totalPrice":2399.00,"userId":1001,"username":"binghe"}

说明项目中成功集成了RocketMQ。

3. RocketMQ核心技术

3.1 RocketMQ的核心技术

RocketMQ是使用Java语言开发的,我们可以将RocketMQ的源码导入到IDEA中,使用IDEA来导入并启动RocketMQ的源码,接下来就可以在IDEA中调试RocketMQ的源码。

下载RocketMQ源码

到链接https://github.com/apache/rocketmq/releases/tag/rocketmq-all-4.9.3下载RocketMQ源码,这里下载的是RocketMQ 4.9.3版本。

将源码导入IDEA

将RocketMQ下载的本地后,就可以将RocketMQ的源码导入到IDEA中了。导入后的项目结构如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第23张图片

编译RocketMQ源码

在IDEA编译RocketMQ源码,需要在IDEA中配置Maven编译,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第24张图片

如图所示,需要输入名称、在Working directory对应的地方选择要使用Maven编译的项目目录,这里,选择RocketMQ项目的根目录,表示编译整个RocketMQ项目。在Command line对应的文本框中输入clean install -Dmaven.test.skip=true来编译RocketMQ源码。其中-Dmaven.test.skip=true参数表示在编译的过程中忽略测试。

配置好之后点击IDEA右上角的运行按钮开始编译RocketMQ的源码,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第25张图片

在编译的过程中可能会下载一些RocketMQ源码依赖的Jar包和插件,等待一会即可。

启动RocketMQ

在IDEA中启动RocketMQ的源码需要进行简单的配置,具体的步骤如下所示。

(1)在RocketMQ源码的根目录下创建conf目录,并将distribution模块下的conf目录的broker.conf文件、logback_namesrv.xml文件和logback_broker.xml文件复制到RocketMQ源码的根目录下创建conf目录中,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第26张图片

(2)文件复制完成后,修改broker.conf文件的内容,修改后的内容如下所示。

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

# 自动创建Topic
autoCreateTopicEnable=true
# nameServ地址
namesrvAddr=127.0.0.1:9876
# 存储路径
storePathRootDir=E:/RocketMQ/data/rocketmq/dataDir
# commitLog路径
storePathCommitLog=E:/RocketMQ/data/rocketmq/dataDir/commitlog
# 消息队列存储路径
storePathConsumeQueue=E:/RocketMQ/data/rocketmq/dataDir/consumequeue
# 消息索引存储路径
storePathIndex=E:/RocketMQ/data/rocketmq/dataDir/index
# checkpoint文件路径
storeCheckpoint=E:/RocketMQ/data/rocketmq/dataDir/checkpoint
# abort文件存储路径
abortFile=E:/RocketMQ/data/rocketmq/dataDir/abort

(3)配置NameServer启动项,NameServer的启动类,在RocketMQ源码的namesrv模块中,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第27张图片

启动前先要在IDEA中配置下NameServer的启动类,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第28张图片

启动NameServer前重点是要在IDEA的Environment variables中配置ROCKETMQ_HOME环境变量,这里,ROCKETMQ_HOME环境变量的值是RocketMQ源码的根目录。

(4)配置Broker启动项,Broker的启动类,在RocketMQ源码中的broker模块中,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第29张图片

启动前先要在IDEA中配置下Broker的启动类,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第30张图片

配置Broker的启动类时,除了要在Environment variables后面配置ROCKETMQ_HOME环境变量外,还要在Program arguments后面配置启动RocketMQ时加载的配置文件,这里的配置文件使用-c参数指向RocketMQ源码根目录下的conf目录中的broker.conf文件,如下所示。

-c D:\Workspaces\myself\rocketmq\src\rocketmq-rocketmq-all-4.9.3\conf\broker.conf

(5)在IDEA中分别启动RocketMQ的NameServer和Broker,可以发现在启动NameServer的控制台输出了如下日志信息。

The Name Server boot success. serializeType=JSON

在启动Broker的控制台输出了如下日志信息。

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

此时,我们到在broker.conf文件中配置的RocketMQ存储数据的根目录E:/RocketMQ/data/rocketmq/dataDir下查看具体信息,如下所示。
微服务实战系列之SpringCloud Alibaba学习(五)_第31张图片

可以看到,在E:/RocketMQ/data/rocketmq/dataDir目录下生成了RocketMQ相关的存储信息。

综上,我们成功在IDEA中导入并启动了RocketMQ的源码。接下来,小伙伴们就可以在IDEA中随心所欲的调试RocketMQ的源码了。

3.2 RocketMQ基本概念

消息模型

RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。

Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。

消息生产者

负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。

消息消费者

负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。

主题

表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。

代理服务器

消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

名字服务

名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。

拉取式消费

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

推动式消费

Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。

生产者组

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

消费者组

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

集群消费

集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。

广播消费

广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

普通顺序消息

普通顺序消费模式下,消费者通过同一个消息队列( Topic 分区,称作 Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。

严格顺序消息

严格顺序消息模式下,消费者收到的所有消息均是有顺序的。

消息

消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。

标签

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

3.3 RocketMQ特性

订阅与发布

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

消息顺序

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

顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。

  • 全局顺序 对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。适用场景:性能要求不高,所有的消息严格按照 FIFO原则进行消息发布和消费的场景
  • 分区顺序 对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。同一个分区内的消息按照严格的 FIFO顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key是完全不同的概念。适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO原则进行消息发布和消费的场景。

消息过滤

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

消息可靠性

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

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

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

5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。

至少一次

至少一次(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。注意,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重试消息的压力。

RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。

考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。

消息重投

生产者在发送消息时,同步消息失败会重投,异步消息有重试,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控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。

参考:

本文是参考冰河技术得SpringCloud Alibaba实战专栏内容整理的,只是方便学习使用(侵权删),具体想要了解或想要获取源码的,请参考下面冰河技术的链接,关注公众号获取源码。

SpringCloud Alibaba

冰河技术:《SpringCloud Alibaba实战》

你可能感兴趣的:(SpringCloud,Allibaba,spring,cloud,微服务)