RocketMQ学习和使用

RocketMQ 学习总结

为什么要使用MQ?

在这里插入图片描述

看看上如图,在上一次我们学习ElasticSearch时,作为查询的微服务要预先冷启动,创建ElasticSearch的索引和域,之后再进行查询的操作,周所周知,一个门户网站必然存在后台的管理系统,那么,如果说我们的后台系统对商品的信息进行了改变,如审核状态,商品的价格,热度值等,这些都应该去改变在查询端的数据,所以,在修改商品信息之后,我们需要将MySQL的数据进行同步,让ElasticSearch同步数据,这样才合理

  • 解决方案:
1. 在查询的微服务中制定一个定时任务,定期的去检查数据是否有改动(不可取,数据量太大,太耗时,时效性和运行效率得不到保障)
2. 使用MQ进行消息的生产和消费,后台管理系统负责发送消息,微服务这边负责消费消息
  • 上述解释就抛出了我们为什么要学习MQ,以及MQ是如何解决我们的问题的,带着这些问题,进入今天的学习

什么是MQ

MQ 就是生产消费者的设计模式,对于 MQ 来说,其实不管是 RocketMQ、Kafka 还是其他消息队列,它们的本质都是:一发一存一消费
  • 从下图可以明确MQ中的三个组成部分

在这里插入图片描述

  • Producer : 生产者,负责产生消息,并放入到消息队列中
  • Consumer : 消费者,负责消费消息,并返回消费的结果
  • QUEUE : 消息队列,存放消息的地方

MQ的优势

  • MQ 的优势可从三个维度来看:
  1. 异步调用 :微服务之间的调用,如果下游的服务存在耗时操作,那么有可能会阻塞线程,在极端情况下有可能耗尽线程池,触发熔断降级操作,对于这些耗时的操作,我们可以先将消息发送到MQ的消息队列之中,之后由消费方去处理这些消息,即MQ可以实现异步调用操作
  2. 应用解耦 :MQ可以实现应用间的完全解耦
  3. 流量削峰填谷 : 如果一段时间内的请求量很高,而之后的请求量很少,MQ中的消息队列可以保存生产者发送的消息,在接下来的一段时间内去消费这些消息,这样一来就实现了流量的削峰填谷,有效缓解程序压力

主流MQ之间的对比

常见的MQ产品包括Kafka、ActiveMQ、RabbitMQ、RocketMQ。

特性ActiveMQRabbitMQRocketMQ(阿里)kafka(大数据)
开发语言javaerlangjavascala
单机吞吐量qps万级万级10万级10万级
时效性rtms级us级ms级ms级
可用性(高可用)主从架构主从架构分布式架构分布式架构

优劣势总结

ActiveMQ
非常成熟,功能强大,在业内大量的公司以及项目中都有应用,偶尔会有较低概率丢失消息,而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用

RabbitMQ
erlang语言开发,性能极其好,延时很低;
吞吐量到万级,MQ功能比较完备
而且开源提供的管理界面非常棒,用起来很好用
社区相对比较活跃,几乎每个月都发布几个版本分
在国内一些互联网公司近几年用rabbitmq也比较多一些
但是问题也是显而易见的,RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。
而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。
而且rabbitmq集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和掌控。


RocketMQ
接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障
日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景
而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控

kafka
kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量
而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集

RocketMQ介绍

RocketMQ 是分布式的一个服务中间件,从上述的对比中可以看出,其基于java语言开发,吞吐量和Kafka是同一级别,且都适用于分布式架构,由此成了我们必学的不二之选,在阿里内部,RocketMQ承接了例如“双11”等高并发场景的消息流转,能够处理万亿级别的消息

官网:http://rocketmq.apache.org/

中文文档:https://github.com/apache/rocketmq/tree/master/docs/cn

RocketMQ 的安装与启动

1. 安装RocketMQ

  • RocketMQ 解压即安装
  • 不同版本下载地址:http://rocketmq.apache.org/dowloading/releases/

2. 配置RocketMQ之前的准备工作

配置之前先明确RocketMQ中的进程角色,这样才能更好的去理解为何需要这样配置

2.1 RocketMQ进程角色

在这里插入图片描述

上图表现的就是RocketMQ中的所有进程角色,之前提到过RocketMQ是高可用的,所以他的每一个进程都可以进行集群配置

Producer(生产者) : 产生消息,并通过连接NameServer获取 Broker 的地址,之后向 Broker 发送消息

Consumer(消费者): 消费消息,先通过连接NameServer 获取对应 Broker 的地址,之后从 Broker 中获取消息并消费

Broker(消息服务器): 是消息储存中心,接收生产者的消息,并保存,向消费者提供对应Topic和Tag的消息(实际上真正办事的人儿,坏笑~)

NameServer(名称服务器): 类似于Nacos的注册中心,Broker需要注册到NameServer上,Producer 和 Consumer 需要向NameServer获取对应Broker的地址,才能进行推送消息和拉取消息的动作,NameServer通过保存Producer的Topic和Tag信息来让Consumer找到对应的Broker

2.2 相关概念
  • Message(消息)

消息系统传输信息的物理载体,生产和消费数据的最小单元,每一个Message必须从属于一个Topic(主题),就像我们聊天都有一个主题一样,每一个消息都有唯一的MessageID,并且可以携带具有业务表示的key(能通过key解决幂等性的问题),RocketMQ提供了相关的查询方法

  • Topic(主题)

每一个消息都必须从属一个Topic,Topic是RocketMQ中消息订阅的最基本单位

  • Tag(标签)

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

  • Producer Group(生产者组)

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

  • Consumer Group(消费者组)

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

2.3 RocketMQ 执行流程

在这里插入图片描述

  1. 启动NameServer 后,监听端口,等待Producer ,Consumer , Broker 连接上来,相当于一个中心路由的作用,类似于Nacos的作用
  2. Broker 启动后注册到NameServer上,与NameServer建立长连接,会定期发送心跳包,告知Broker的生存状态
  3. Producer 向NameServer 发送请求,先创建 Topic 和 Tag ,NameServer 返回对应的 Broker 地址,并储存这些信息( Topic,Tag等)Proucer得到Broker 的地址后向Broker发送消息,Broker进行储存操作
  4. Consumer 向 NameServer 发送请求,询问 Topic 和 Tag 所在的 Broker,NameServer同样返回对应的Broker地址,Consumer获取到Broker地址后,向Broker获取消息,Consumer消费之后会返回消费结果给Broker
2.4 Broker的储存方式

在这里插入图片描述

如图所示:

Broker 是以分片存储的,Broker与Topic 是多对多的关系,即一个Topic可以在多个Broker中存在,一个Broker可以有多个Topic

Broker是最重要的部分,包括持久化消息、Broker集群一致性(HA)、保存历史消息、保存Consumer消费偏移量、索引创建等

Broker中是以队列的方式储存消息的,全局上来看消息的消费并不是有序的,这个后面详细讲解

明确以上RocketMQ的进程角色和执行流程后再来配置RocketMQ就显得非常容易和易懂了

3. 开始配置RocketMQ

3.1 conf文件
  • 找到如下图所示配置文件,其中nameserver.properties为自己创建的

在这里插入图片描述

  • nameserver.properties配置文件内容

配置nameserver运行的ip地址和端口

# 监听端口,即nameserver的启动端口
listenPort=9876
# 若希望其他ip访问则需要配置真实的ip地址
listenIp=127.0.0.1 
  • broker.conf 配置文件内容

配置broker的运行信息,要告知broker需要注册的nameserver的地址,以及自己的ip和端口

#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割【重点】
namesrvAddr=127.0.0.1:9876
#Broker 对外服务的监听端口
listenPort=10911
#Broker监听的ip【重点】
brokerIP1=127.0.0.1

#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#延迟消息发送的配置,也是消息拉取失败后重发次数和时间间隔的依据
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
3.2 启动nameserver文件说明

在这里插入图片描述

  • 如图,在bin目录下找到对应文件mqnamesrv.cmd 是默认的启动nameserver的文件,mymqbroker.bat 为我们自己创建的批处理文件,来看看文件中的内容
  • mqnamesrv.cmd文件内容
@echo off
rem Licensed to the Apache Software Foundation (ASF) under one or more
rem contributor license agreements.  See the NOTICE file distributed with
rem this work for additional information regarding copyright ownership.
rem The ASF licenses this file to You under the Apache License, Version 2.0
rem (the "License"); you may not use this file except in compliance with
rem the License.  You may obtain a copy of the License at
rem
rem     http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.

if not exist "%ROCKETMQ_HOME%\bin\runserver.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1

call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.namesrv.NamesrvStartup %*

IF %ERRORLEVEL% EQU 0 (
    ECHO "Namesrv starts OK"
)

由此文件可知,需要提前配置%ROCKETMQ_HOME% , 其路径为bin的上一级目录,否则无法启动nameserver

若有配置%ROCKETMQ_HOME%则启动runserver.cmd文件

所以,我们先去配置%ROCKETMQ_HOME%的环境变量,再来看看runserver.cmd中是什么内容

  • 配置%ROCKETMQ_HOME%环境变量

在这里插入图片描述

按上图步骤配置环境变量

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


if not exist "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! & EXIT /B 1
set "JAVA=%JAVA_HOME%\bin\java.exe"

setlocal

set BASE_DIR=%~dp0
set BASE_DIR=%BASE_DIR:~0,-1%
for %%d in (%BASE_DIR%) do set BASE_DIR=%%~dpd

set CLASSPATH=.;%BASE_DIR%conf;%CLASSPATH%

set "JAVA_OPT=%JAVA_OPT% -server -Xms1g -Xmx1g -Xmn500m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
set "JAVA_OPT=%JAVA_OPT% -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"
set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:"%USERPROFILE%\rmq_srv_gc.log" -XX:+PrintGCDetails"
set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow"
set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages"
set "JAVA_OPT=%JAVA_OPT% -Djava.ext.dirs=%BASE_DIR%lib"
set "JAVA_OPT=%JAVA_OPT% -cp "%CLASSPATH%""

"%JAVA%" %JAVA_OPT% %*

如果服务器性能有所欠缺需要改变配置,原配置中内存占据为2g,最小为1g

set “JAVA_OPT=%JAVA_OPT% -server -Xms1g -Xmx1g -Xmn500m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m”

3.3 启动nameserver
  • 自定义的批处理文件mymqnamesrv.bat,目的在于按我们的配置文件启动
start  mqnamesrv.cmd -c  ../conf/nameserver.properties
  • 点击运行看到如下画面,nameserver启动成功

在这里插入图片描述

3.4 启动broker
  • broker的配置文件已经在上面解释完毕
  • mqbroker.cmd启动文件内容
@echo off
rem Licensed to the Apache Software Foundation (ASF) under one or more
rem contributor license agreements.  See the NOTICE file distributed with
rem this work for additional information regarding copyright ownership.
rem The ASF licenses this file to You under the Apache License, Version 2.0
rem (the "License"); you may not use this file except in compliance with
rem the License.  You may obtain a copy of the License at
rem
rem     http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.

if not exist "%ROCKETMQ_HOME%\bin\runbroker.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1

call "%ROCKETMQ_HOME%\bin\runbroker.cmd" org.apache.rocketmq.broker.BrokerStartup %*

IF %ERRORLEVEL% EQU 0 (
   ECHO "Broker starts OK"
)

与nameserver一样,启动也需要配置ROCKETMQ_HOME的环境变量,之后启动的是runbroker.cmd文件

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

if not exist "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! & EXIT /B 1
set "JAVA=%JAVA_HOME%\bin\java.exe"

setlocal

set BASE_DIR=%~dp0
set BASE_DIR=%BASE_DIR:~0,-1%
for %%d in (%BASE_DIR%) do set BASE_DIR=%%~dpd

set CLASSPATH=.;%BASE_DIR%conf;%CLASSPATH%

rem ===========================================================================================
rem  JVM Configuration
rem ===========================================================================================
set "JAVA_OPT=%JAVA_OPT% -server -Xms1g -Xmx1g -Xmn500m"
set "JAVA_OPT=%JAVA_OPT% -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8"
set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:%USERPROFILE%\mq_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"
set "JAVA_OPT=%JAVA_OPT% -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow"
set "JAVA_OPT=%JAVA_OPT% -XX:+AlwaysPreTouch"
set "JAVA_OPT=%JAVA_OPT% -XX:MaxDirectMemorySize=15g"
set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages -XX:-UseBiasedLocking"
set "JAVA_OPT=%JAVA_OPT% -Djava.ext.dirs=%BASE_DIR%lib"
set "JAVA_OPT=%JAVA_OPT% -cp %CLASSPATH%"

"%JAVA%" %JAVA_OPT% %*

同理,服务器性能不足的时候可以改变配置:

set “JAVA_OPT=%JAVA_OPT% -server -Xms1g -Xmx1g -Xmn500m”

原配置为2g

  • 自定义mymqbroker.bat
start  mqbroker.cmd -c  ../conf/broker.conf
  • 看到如下画面,broker启动成功

在这里插入图片描述

4. RocketMQ控制台

4.1:下载控制台源码
# 方式一、git下载,执行如下命令
git clone https://github.com/apache/rocketmq-externals.git

# 方式二、直接下载,访问如下地址即可
https://github.com/apache/rocketmq-externals/archive/master.zip

4.2:切换到rocketmq-console模块

修改端口(rocketmq-console>src>resource>application.properties)

在这里插入图片描述

修改nameserver的地址(rocketmq-console>src>resource>application.properties)

rocketmq.config.namesrvAddr=localhost:9876

在这里插入图片描述

修改Rocketmq的api版本(rocketmq-console>pom.xml)

在这里插入图片描述

添加依赖

<dependency>    
	<groupId>commons-io</groupId>    
	<artifactId>commons-io</artifactId>    
	<version>2.4</version>
</dependency>
4.3:使用maven编译构建
mvn clean package -Dmaven.test.skip=true
4.4:懒人包

我这里直接使用的是老师提供的懒人包~~~

RocketMQ消息的收发

1. POM依赖

<!-- mq java客户端       -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.6.1</version>
</dependency>

2. 生产者消息发送

2.1 同步消息发送–阻塞线程,关心结果

**发送步骤 : **

  1. 创建消息生产者producer (DefaultMQProducer),通过构造器指定group
  2. 指定NameServer地址
  3. 启动producer
  4. 创建Message对象,指定主题Topic,标签Tag,消息体(需要byte)
  5. 发送消息
  6. 关闭生产者
/**
 *  mq 的基本使用: 同步消息发送
 */
public class MqStudyDemo01 {
    public static void main(String[] args) throws Exception{
        // 发送消息的步骤
        // 1.创建消息生产者producer(DefaultMQProducer),并指定生产者group
        DefaultMQProducer producer = new DefaultMQProducer("test_group");
        // 2.指定Nameserver地址
        producer.setNamesrvAddr("localhost:9876");
        // 3.启动producer
        producer.start();
        // 4.创建Message对象,指定主题Topic、消息体
        Message message = new Message("testTopicA","testTagsA","hello MQ ~".getBytes());
        // 5.发送消息
        producer.send(message);
        // 6.关闭生产者producer
        producer.shutdown();
    }
}
2.2 异步消息发送–不阻塞线程,关心结果

**发送步骤 : **

  1. 创建消息生产者producer (DefaultMQProducer),通过构造器指定group
  2. 指定NameServer地址
  3. 启动producer
  4. 创建Message对象,指定主题Topic,标签Tag,消息体(需要byte)
  5. 发送消息
  6. 关闭生产者,生产者的关闭必须在SendCallback的匿名内部类内,因为异步发送不会影响到本地代码的执行,如果像同步发送写到原位置,可能因为网络抖动,消息还没有发送生产者就被关闭了
/**
 *  生产者消息异步发送
 */
public class MqStudyDemo02 {
    public static void main(String[] args) throws Exception{
        // 发送消息的步骤
        // 1.创建消息生产者producer(DefaultMQProducer),并制定生产者group
        DefaultMQProducer producer = new DefaultMQProducer("test_group");
        // 2.指定Nameserver地址
        producer.setNamesrvAddr("localhost:9876");
        // 3.启动producer
        producer.start();
        // 4.创建Message对象,指定主题Topic、消息体
        Message message = new Message("testTopicB","testTagsB","hello 异步消息".getBytes());
        // 5.发送消息
        // 此处与同步消息不同,采用异步消息发送方式
        producer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                // 发送成功后的回调
                System.out.println("异步消息发送成功");
                // 6.关闭生产者producer 因为是异步发送,所以关闭生产者不能再这里,代码会继续往下进行,如果有
                // 网络延迟,可能消息还没有发送,生产者就被关闭了
                producer.shutdown();
            }
            @Override
            public void onException(Throwable throwable) {
                // 发送异常后的回调
                System.out.println("异步消息发送失败");
                producer.shutdown();
            }
        });
        //producer.shutdown();
    }
}
2.3 单向消息发送–不阻塞线程,不关心结果

**发送步骤 : **

  1. 创建生产者producer(DefaultMQProducer),通过构造器指定group
  2. 指定NameServer的地址
  3. 启动producer
  4. 创建Message对象,指定主题Topic,标签Tag,消息体(需要byte)
  5. 发送消息[单向消息]
  6. 关闭producer
/**
 *  发送单向消息
 */
public class MqStudyDemo03 {
    public static void main(String[] args) throws Exception{
        // 1. 创建消息生产者
        DefaultMQProducer producer = new DefaultMQProducer("test_group");
        // 2. 指定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 3. 启动producer
        producer.start();
        // 4. 创建Message对象,指定topic,tag,消息体
        Message message = new Message("testTopic","testTag","hello 单向消息".getBytes());
        // 5. 发送单向消息,先设置消息发送失败之后的重试发送次数
        producer.setRetryTimesWhenSendFailed(2);
        producer.sendOneway(message);
        // 6. 关闭producer
        producer.shutdown();
    }
}

总结:

  1. 同步消息的发送会阻塞线程,关心发送的结果,必须要发送成功之后才会继续接下来的代码,常使用在验证码的发送等场景,必须要收到发送验证码成功的响应后才能继续接下去的步骤,这样能防止验证码不停的发送
  2. 异步消息的发送不会阻塞线程,关心发送的结果,其通过SendCallback的回调函数来处理发送成功和发送失败的结果,经常运用在不能忍受broker长时间响应的场景下
  3. 单向消息的发送不会阻塞线程,且其并不关心发送的结果,是否发送成功我们也不得而知,可使用在日志输出的场景下,部分日志发送失败并不重要

3. 消费者消费消息

分类:

  1. 集群模式: 消息只会被消费一次
  2. 广播模式: 消息会在消费者组中被所有的消费者消费
3.1 集群模式
  • 集群模式下,消费者组订阅同一个Topic时,一条消息只会被消费一次,不会重复消费

**消费步骤 : **

  1. 创建消费者consumer(DefaultMQPushConsumer),通过构造器指定消费者组
  2. 指定NameServer的地址
  3. 通过consumer下的subscribe订阅指定的Topic和Tag
  4. 设置消费者的消费模式MessageModel(CLUSTERING为默认的集群模式)
  5. 注册(registerMessageListener),使用MessageListenerConcurrently匿名内部类马上消费消息
  6. 因为消费者消费消息都要给broker一个响应,所以如果没有异常返回消费成功的枚举ConsumeConcurrentlyStatus.CONSUME_SUCCESS,异常则返回ConsumeConcurrentlyStatus.RECONSUME_LATER,broker接收到响应后会根据响应来做不同的处理
  7. 启动消费者,消费者是一直在线的,不停的去监听端口的Topic
/**
 * 消费者 ——集群模式
 */
public class MqStudyDemo04 {
    public static void main(String[] args) throws Exception {
        // 1.创建消费者Consumer(DefaultMQPushConsumer),指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("testConsumerGroup");
        // 2.指定Nameserver地址
        consumer.setNamesrvAddr("localhost:9876");
        // 3.订阅(subscribe)主题Topic,Tag
        consumer.subscribe("testTopicA", "testTagsA");
        // 4.设置消费模式(MessageModel),默认集群模式
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 5.注册(register)回调函数,处理消息
        consumer.setConsumeMessageBatchMaxSize(2);
        //MessageListenerConcurrently为马上消费
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                try {
                    // 遍历list,获取每一个message
                    list.forEach(messageExt -> {
                        // 获取topic主题
                        String topic = messageExt.getTopic();
                        // 获取tag
                        String tags = messageExt.getTags();
                        // 获取消息体,消息体是byte类型,转成String
                        String messageBody = new String(messageExt.getBody());
                        // 输出
                        System.out.println("topic = " + topic + "tags = " + tags + "message =" + messageBody);
                    });
                    // 没有异常返回给broker
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } catch (Exception e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
        });
        // 6.启动消费者
        consumer.start();
    }
}
3.2 广播模式
  • 当有多个消费者同时监听某个topic时,广播模式 消息会被每个消费者同时消费

**消费步骤: **

  1. 创建消费者consumer(DefaultMQPushConsumer),通过构造器指定消费者组
  2. 指定NameServer的地址
  3. 订阅消息的topic和tag
  4. 指定消费模式为广播模式
  5. 注册消费回调,使用MessageConsumerConcurrently匿名内部类马上消费消息
  6. 消费成功或消费失败返回对应的响应
  7. 开启消费者
/**
 * 消费者 —— 广播模式
 */
public class MqStudyDemo05 {
    public static void main(String[] args) throws Exception{
        // 1. 创建消费者(DefaultMQPushConsumer),通过构造器指定消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("testConsumerGroup2");
        // 2. 指定NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 3. 订阅主题topic和标签tag,必须要与发送者的topic和tag一致
        consumer.subscribe("testTopicB","testTagsB");
        // 4. 设置消费者模式为广播模式,默认是集群模式
        consumer.setMessageModel(MessageModel.BROADCASTING);
        // 5. 注册,消费回调
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                try {
                    // 遍历list,获取每一个message
                    list.forEach(messageExt -> {
                        // 获取消息的topic
                        String topic = messageExt.getTopic();
                        // 获取消息的tag
                        String tags = messageExt.getTags();
                        // 获取消息体,消息体是byte[],要转成String
                        String messageBody = new String(messageExt.getBody());
                        // 输出
                        System.out.println("topic = " + topic + "tags = " + tags + "message =" + messageBody);
                    });
                    // 没有异常,返回消费成功的枚举
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } catch (Exception e) {
                    e.printStackTrace();
                    // 有异常返回消费失败的枚举
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
        });
        // 6. 启动消费者
        consumer.start();
    }
}

4. 消息的收发顺序

4.1 验证全局消息默认无序
  • 根据我们之前分析过的RocketMQ的消息存储方式,Topic是分片的,且一个 Topic 中默认有4个queue,那么在储存的时候有可能是不会将消息储存在同一个queue中的,这就造成了RocketMQ默认情况下全局消息是无序的
  • 代码实现:
/**
 *  测试 RocketMQ 全局消息无序
 */
public class OrderlyDemo01 {
    public static void main(String[] args) throws Exception{
        // 1. 创建消息生产者
        DefaultMQProducer producer = new DefaultMQProducer("testOrderlyGroup");
        // 2. 指定NameServer
        producer.setNamesrvAddr("localhost:9876");
        // 3. 启动producer
        producer.start();
        // 4. 创建Message对象,指定Topic和Tag
        for (int i = 0; i < 100; i++) {
            Message message = new Message("testOrderlyTopic","testOrderlyTag",("message"+i).getBytes());
            // 5. 发送消息,这里就用同步消息
            SendResult send = producer.send(message);
            System.out.println(send);
        }
        // 5. 关闭生产者
        producer.shutdown();
    }
}

上述代码,我们在发消息时构建了一个循环,以此发送多条同步消息,通过发送后的结果打印,来验证消息的全局无序,下图为消费者消费后的打印,证明了全局上我们的RocketMQ的消息是无序的

Connected to the target VM, address: '127.0.0.1:55041', transport: 'socket'
topic = testOrderlyTopictags = testOrderlyTagmessage =message8
topic = testOrderlyTopictags = testOrderlyTagmessage =message33
topic = testOrderlyTopictags = testOrderlyTagmessage =message37
topic = testOrderlyTopictags = testOrderlyTagmessage =message17
topic = testOrderlyTopictags = testOrderlyTagmessage =message21
topic = testOrderlyTopictags = testOrderlyTagmessage =message24
topic = testOrderlyTopictags = testOrderlyTagmessage =message28
topic = testOrderlyTopictags = testOrderlyTagmessage =message9
topic = testOrderlyTopictags = testOrderlyTagmessage =message13
topic = testOrderlyTopictags = testOrderlyTagmessage =message0
topic = testOrderlyTopictags = testOrderlyTagmessage =message4
topic = testOrderlyTopictags = testOrderlyTagmessage =message25
topic = testOrderlyTopictags = testOrderlyTagmessage =message29
topic = testOrderlyTopictags = testOrderlyTagmessage =message1
topic = testOrderlyTopictags = testOrderlyTagmessage =message16
topic = testOrderlyTopictags = testOrderlyTagmessage =message5
topic = testOrderlyTopictags = testOrderlyTagmessage =message12
4.2 特殊场景——模拟全局消息有序

**解决方案: **

  1. 当然,从设置中将默认的queue的个数由4改为1,可以解决这个问题,但会大大降低我们RocketMQ的效率,此方法不可取
  2. 既然消息发送到了不同的queue中,那么我们只要保证一个topic的消息往一个queue中发送即可
  • 代码实现:
/**
 *  特殊场景 —— topic消息有序
 *  思路: 同一个topic的消息往一个queue中发送
 */
public class OrderlyDemo02 {
    public static void main(String[] args) throws Exception{
        // 1. 创建producer生产者
        DefaultMQProducer producer = new DefaultMQProducer("testOrderlyGroup");
        // 2. 指定NameServer
        producer.setNamesrvAddr("localhost:9876");
        // 3. 启动producer
        producer.start();
        // 设置Message消息,循环发送,发到同一个topic里
        for (int i = 0; i < 100; i++) {
            // 4. 构造Message
            Message message = new Message("testOrderlyTopic2","testOrderlyTag2",("message"+i).getBytes());
            // 5. 发送消息,通过消息队列选择器,将消息发送到指定的queue中
            producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    // list 即是 Message 的队列,参数 o 即是后面的arg
                    Integer i = (Integer) o;
                    // 选择第一个queue作为存储队列
                    return list.get(i);
                }
            },0);
        }
        // 6. 关闭producer
        producer.shutdown();
    }
}

上述代码中,通过MessageQueueSelector消息队列选择器选择了第一个queue进行消息发送的目标队列,所以消费时的消息在全局上就保证了有序性,以下为部分消费消息

在这里插入图片描述

4.3 延时消息

在这里插入图片描述

顾名思义,就是将消息延期发送,在我们的Broker中设立一个临时区域,储存消息,等到设定的时间到了之后再将消息发送到目标Topic中

延迟消息的等级根据我们在broker.conf中的配置项相关

延迟消息发送的配置,也是消息拉取失败后重发次数和时间间隔的依据
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

  • 代码实现:
/**
 * 延迟消息发送
 */
public class OrderlyDemo03 {
    public static void main(String[] args) throws Exception{
        // 1. 创建消息生产者
        DefaultMQProducer producer = new DefaultMQProducer("testOrderlyGroup");
        // 2. 指定NameServer
        producer.setNamesrvAddr("localhost:9876");
        // 3. 启动生产者
        producer.start();
        // 4. 创建消息 Message
        Message message = new Message("testTopicB","testTagsB","hello 异步消息".getBytes());
        // 5. 设置延迟消息等级,发送延迟消息
        //#延迟消息发送的配置,也是消息拉取失败后重发次数和时间间隔的依据
        //messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        message.setDelayTimeLevel(3);
        producer.send(message);
        // 6. 关闭生产者
        producer.shutdown();
    }
}

延迟消息应用场景:

在我们的分布式订单下单后,给与顾客一定的时间付款,这时可以运用延迟任务,只有付款之后才会发送消息,设定时间到后去检查订单状态,若未付款则取消订单,释放库存

5. 重试队列

RocketMQ的消费者方默认有重试机制:

  1. **异常重试: ** 因为程序代码中有异常,返回了RECONSUMER_LATER,之后发生重发
  2. 超时重试: Consumer端有耗时操作,或者Consumer端消费信息超时,导致长时间Broker得不到Consumer的回复,在设置的超时时间后,Broker会尝试重发消息
  • 重试的设置:

    • 依然是 broker.conf 中的配置项
    延迟消息发送的配置,也是消息拉取失败后重发次数和时间间隔的依据
    messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
    
    • broker会根据设置的次数,其中时间为发送重试消息的间隔时间

注意1:只有在消息模式为MessageModel.CLUSTERING集群模式时,Broker才会自动进行重试,广播消息是不会重试的

注意2:由于MQ的重试机制,难免会引起消息的重复消费问题。比如一个ConsumerGroup中有两个,Consumer1和Consumer2,以集群方式消费。假设一条消息发往ConsumerGroup,由Consumer1消费,但是由于Consumer1消费过慢导致超时,如果Broker将消息发送给Consumer2去消费,这样就产生了重复消费问题。因此,使用MQ时应该对一些关键消息进行幂等去重的处理

6. 死信队列

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

在消息队列 RocketMQ 版中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)


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


死信队列具有以下特性:
1:一个死信队列对应一个 Group ID, 而不是对应单个消费者实例
2:如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 版不会为其创建相应的死信队列
3:一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic
4:消息队列 RocketMQ 版控制台提供对死信消息的查询、重发的功能

7. SpringBoot整合RocketMQ

7.1 POM依赖
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <!--2.1.0对应的mq是4.6.x-->
    <version>2.1.0</version>
</dependency>
7.2 Producer生产者
@RestController
public class ProducerController {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @RequestMapping("syncSend")
    public SendResult syncSend() {
        // 同步发送消息
        return rocketMQTemplate.syncSend("test-topic1:test-tag1","你好mq");
    }

    @RequestMapping("asyncSend")
    public void asyncSend() {

        // 异步发送消息
        rocketMQTemplate.asyncSend("test-topic2:test-tag2", "asyncSend", new SendCallback() {
            public void onSuccess(SendResult sendResult) {
                System.out.println(sendResult);
            }

            public void onException(Throwable throwable) {
                throwable.printStackTrace();
            }
        });
    }

    @RequestMapping("onewaySend")
    public void onewaySend() {
        // oneway 发送单向消息
        rocketMQTemplate.sendOneWay("test-topic3:test-tag3", "oneway");
    }

    @GetMapping("delay")
    public String delay(){
        //延时消息,使用同步消息的方法重载
        rocketMQTemplate.syncSend("test-topic4:test-tag4", MessageBuilder.withPayload("delay").build(),2000,2);

        return "delay";
    }
}
7.3 Consumer消息的消费者

@RocketMQMessageListener注解说明:

consumerGroup: 消费者组名

topic:订阅的主题topic名称,务必与发送者保持一致,否则无法订阅成功

selectorExpression: 订阅的topic下的tag名称,同样务必与发送者保持一致,否则无法订阅成功

consumerMode : 枚举类型,消费者模式,包含两种模式,一种是默认的即时消费,一种是有序消费ORDERLY

messageModel : 枚举类型,消息消费模式,广播和集群模式,默认是集群模式

  • 注意:这里的类实现的接口RocketMQListener中的泛型就是生产者发送的消息体的类型
@Component
@RocketMQMessageListener(consumerGroup = "bootConsumerGroup",
                        topic = "test-topic1",
                        selectorExpression = "test-tag1",
                        consumeMode = ConsumeMode.CONCURRENTLY,
                        messageModel = MessageModel.CLUSTERING)
public class ConsumerListenerController implements RocketMQListener<String> {
    @Override
    public void onMessage(String msg) {
        System.out.println(msg);
    }
}

8. 解决最开始的问题

RocketMQ 学习到这里我们就可以根据以上所学的知识来解决最开始同步ES的问题了

这里我们模拟在一个分布式项目中处理ES同步的问题

思路:
1. 商品微服务进行商品的审核操作,操作完毕之后发送消息到我们的RocketMQ中
2. 发送的是修改之后的商品信息
3. 商品搜索的微服务进行监听,消息的消费,实现同步ES的操作
  • 明确以上流程,我们开始执行:
8.1 商品微服务整合RocketMQ(生产者)
  • 商品微服务加入pom依赖
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>
  • 模拟商品审核情景:通过传入商品id进行审核,改变商品审核状态信息,之后发送消息,将新的商品信息发送到MQ
  • 操作的表:商品表mall_goods,其中有品牌外键和一级,二级,三级分类的外键

在这里插入图片描述

  • mapper层,使用的是MybatisPlus
public interface MallGoodsMapper extends BaseMapper<MallGoods> {
}
  • service层实现类,其中注释的方法部分为原直接调用同步索引的方法,由此也能看出使用MQ还能实现解耦
@Service
public class MallGoodsServiceImpl extends ServiceImpl<MallGoodsMapper, MallGoods> implements IMallGoodsService {
	// 注入rocketMq
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
	// 注入商品品牌mapper层,用于查询品牌信息
    @Autowired(required = false)
    private MallGoodsBrandMapper mallGoodsBrandMapper;
    
	// 注入商品分类mapper层,用于查询商品分类信息
    @Autowired(required = false)
    private MallGoodsCatMapper mallGoodsCatMapper;
	
    // 认证操作方法
    @Override
    public ResultVO auditGoods(String spuId) {
        // 非空判断
        if (StringUtils.isEmpty(spuId)) {
            return new ResultVO(false, "参数不合法");
        }
        // 通过id查询商品
        MallGoods mallGoods = this.baseMapper.selectById(spuId);

        // 非空判断
        if (mallGoods == null) {
            return new ResultVO(false, "商品不存在");
        }

        // 查询品牌信息
        MallGoodsBrand mallGoodsBrand = mallGoodsBrandMapper.selectById(mallGoods.getBrandId());
        mallGoods.setMallGoodsBrand(mallGoodsBrand);

        // 查询一级分类信息
        MallGoodsCat mallGoodsCat1 = mallGoodsCatMapper.selectById(mallGoods.getCategory1Id());
        mallGoods.setCat1(mallGoodsCat1);

        // 查询二级分类信息
        MallGoodsCat mallGoodsCat2 = mallGoodsCatMapper.selectById(mallGoods.getCategory2Id());
        mallGoods.setCat2(mallGoodsCat2);

        // 查询三级分类信息
        MallGoodsCat mallGoodsCat3 = mallGoodsCatMapper.selectById(mallGoods.getCategory3Id());
        mallGoods.setCat3(mallGoodsCat3);

        // 商品存在,更改商品审核信息,1为审核通过,0为审核不通过,这里直接设置审核成功,方便后续检测
        mallGoods.setAuditStatus("1");
        // 修改商品信息
        this.baseMapper.updateById(mallGoods);

        //try {
        //    // 远程调用search进行同步操作
        //    searchApi.synchronousESWhenGoodsUpdate(mallGoods);
        //} catch (Exception e) {
        //    e.printStackTrace();
        //    // 发生异常反向补偿
        //    mallGoods.setAuditStatus("0");
        //    // 修改商品信息
        //    this.baseMapper.updateById(mallGoods);
        //}

        // 使用RocketMQ发送消息
        SendResult sendResult = rocketMQTemplate.syncSend("goods2ESTopic:goods", mallGoods);

        // 判断消息是否发送成功
        if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
            // 发送成功
            return new ResultVO(true, "商品审核成功");
        } else {
            // 消息未发送成功,反向补偿
            mallGoods.setAuditStatus("0");
            // 修改商品信息
            this.baseMapper.updateById(mallGoods);
            return new ResultVO(false, "同步失败");
        }
    }
}
  • 商品审核controller层
@RestController
@RequestMapping("/goods")
public class MallGoodsController {
	// 审核商品信息
    @RequestMapping("auditGoods/{spuId}")
    public ResultVO auditGoods(@PathVariable String spuId) {
        return goodsService.auditGoods(spuId);
    }
}

至此,商品审核的接口开发完毕,生产者端完成,接下来开发消费者端

8.2 搜索微服务整合RocketMQ(消费者)
  • 上来就是pom依赖
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>
  • 商品同步逻辑,传入新的商品信息,通过值传递给索引对象ESGoods,之后通过ES模板对象save方法同步索引库
	
    public ResultVO synchronousESWhenGoodsUpdate(MallGoods spu) {
        // 非空判断
        if (spu == null){
            return new ResultVO(false,"参数不合法");
        }
        // 进行同步操作,将MallGoods转为ESGoods
        ESGoods esGoods = new ESGoods();
        // id
        esGoods.setSpuId(spu.getSpuId());
        // brand 品牌信息
        esGoods.setBrandId(spu.getMallGoodsBrand().getId());
        esGoods.setBrandName(spu.getMallGoodsBrand().getName());
        // 分类信息
        esGoods.setCid1id(spu.getCat1().getId());
        esGoods.setCat1name(spu.getCat1().getName());
        esGoods.setCid2id(spu.getCat2().getId());
        esGoods.setCat2name(spu.getCat2().getName());
        esGoods.setCid3id(spu.getCat3().getId());
        esGoods.setCat3name(spu.getCat3().getName());
        // 维护创建时间,后面可以做排序处理
        esGoods.setCreateTime(new Date());
        // 商品名称
        esGoods.setGoodsName(spu.getGoodsName());
        esGoods.setPrice(spu.getPrice().doubleValue());
        // 图片是字符串形式,需要做处理
        String albumPics = spu.getAlbumPics();
        // 对字符串做处理要先进行非空判断
        if (!StringUtils.isEmpty(albumPics)) {
            String[] split = albumPics.split(",");
            // 非空判断
            if (split != null && split.length > 0) {
                esGoods.setSmallPic(spu.getAlbumPics());
            }
        }
        // 所有字段赋值完毕之后,同步
        restTemplate.save(esGoods);

        return new ResultVO(true,"同步ES操作成功");
    }
  • 消费者方的代码就比较简单了,只需定义一个类去实现RocketMQListener接口即可,然后通过注解**@RocketMQMessageListener定义消费者组,Topic,Tag,消费模式,消息模式**
@Component
@RocketMQMessageListener(consumerGroup = "search-consumer-group",
        topic = "goods2ESTopic", selectorExpression = "goods",
        consumeMode = ConsumeMode.CONCURRENTLY, messageModel = MessageModel.CLUSTERING)
public class SearchConsumer implements RocketMQListener<MallGoods> {

    @Autowired
    private ISearchService searchService;

    @Override
    public void onMessage(MallGoods mallGoods) {
        System.out.println(mallGoods);
        ResultVO resultVO = searchService.synchronousESWhenGoodsUpdate(mallGoods);
    }
}
8.3 测试
  • 改变数据库中的审核数据,此处我们将8号商品的审核状态改为0

在这里插入图片描述

  • 初始化后可以看到ES索引库中只有7条数据了

在这里插入图片描述

  • 接下来进行商品审核,访问接口即可,实际上是通过后台系统去发送消息的,这里不好演示才写到一起的

在这里插入图片描述

  • 再查询ES索引库,商品审核成功,ES同步索引库成功

在这里插入图片描述

在这里插入图片描述

至此,我们的审核和同步都完成,MQ基本使用开发完毕

9. 解决幂等性问题

9.1 什么是幂等性
  • 幂等性的概念是:任意多次执行所产生的影响均与一次执行的影响相同,即无论你请求了多少次,对数据库的影响都只能有一次,不能重复处理
  • 图解:

在这里插入图片描述

如上图所示,再消费者消费的时候,如逻辑中有插入数据库操作,或更改值操作(更改值只能是update field = field + num 时),下游逻辑如果有耗时操作,或此时发生了网络抖动,更或者发生了异常,那么都会是的Consumer的返回信息超时,Broker接收不到Consumer的响应,就会去重发消息,Consumer继续消费同样的消息,又做了一次插入操作,如此就不符合逻辑,这就是幂等性问题

由幂等性的概念和上图所知:

  1. 查询,删除操作不会产生幂等性问题,因为多次执行的结果是一致的
  2. 更改操作只能是增加值或减少值会产生幂等性问题,直接赋值也是不会产生幂等性问题的
9.2 解决思路

最开始解释RocketMQ的进程角色时,其中的Message是有MessageID的这是区分Message的唯一标识,其也可以携带key,这个key可以作为业务标识,我们解决幂等性问题可以根据这个key作为业务标识,加上Redis配合

在这里插入图片描述

图解:

生产者在发送消息的时候将业务id一般来说就是操作的对象的主键id发送到redis中,设置状态,0为未修改,1为修改,在上述的案例中,就是将商品id设置为Message的key作为业务id,状态设为0,存储到Redis中

Consumer消费之前,从Redis中获取信息,查看商品修改状态是否是0,是0才进行修改的逻辑,之后将状态改为1

如此一来,就算发生了网络抖动等因素,也不会发生幂等性问题,幂等性问题只能通过代码去解决

  • 商品审核代码改造
@Override
    public ResultVO auditGoods(String spuId) {
        // 非空判断
        if (StringUtils.isEmpty(spuId)) {
            return new ResultVO(false, "参数不合法");
        }
        // 通过id查询商品
        MallGoods mallGoods = this.baseMapper.selectById(spuId);

        // 非空判断
        if (mallGoods == null) {
            return new ResultVO(false, "商品不存在");
        }

        // 查询品牌信息
        MallGoodsBrand mallGoodsBrand = mallGoodsBrandMapper.selectById(mallGoods.getBrandId());
        mallGoods.setMallGoodsBrand(mallGoodsBrand);

        // 查询一级分类信息
        MallGoodsCat mallGoodsCat1 = mallGoodsCatMapper.selectById(mallGoods.getCategory1Id());
        mallGoods.setCat1(mallGoodsCat1);

        // 查询二级分类信息
        MallGoodsCat mallGoodsCat2 = mallGoodsCatMapper.selectById(mallGoods.getCategory2Id());
        mallGoods.setCat2(mallGoodsCat2);

        // 查询三级分类信息
        MallGoodsCat mallGoodsCat3 = mallGoodsCatMapper.selectById(mallGoods.getCategory3Id());
        mallGoods.setCat3(mallGoodsCat3);

        // 商品存在,更改商品审核信息,1为审核通过,0为审核不通过,这里直接设置审核成功,方便后续检测
        mallGoods.setAuditStatus("1");
        // 修改商品信息
        this.baseMapper.updateById(mallGoods);

        //try {
        //    // 远程调用search进行同步操作
        //    searchApi.synchronousESWhenGoodsUpdate(mallGoods);
        //} catch (Exception e) {
        //    e.printStackTrace();
        //    // 发生异常反向补偿
        //    mallGoods.setAuditStatus("0");
        //    // 修改商品信息
        //    this.baseMapper.updateById(mallGoods);
        //}

        // 发送消息之前储存到redis中
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps("menhu:goods:goods2es");
        // field为商品id , value为0:未修改,1已修改
        operations.put(String.valueOf(mallGoods.getSpuId()), "0");

        // 使用RocketMQ发送消息
        SendResult sendResult = rocketMQTemplate.syncSend("goods2ESTopic:goods", mallGoods);

        // 判断消息是否发送成功
        if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
            // 发送成功
            return new ResultVO(true, "商品审核成功");
        } else {
            // 消息未发送成功,反向补偿
            mallGoods.setAuditStatus("0");
            // 修改商品信息
            this.baseMapper.updateById(mallGoods);
            return new ResultVO(false, "同步失败");
        }
    }
  • 同步ES代码改造
@Override
    public ResultVO synchronousESWhenGoodsUpdate(MallGoods spu) {
        // 非空判断
        if (spu == null){
            return new ResultVO(false,"参数不合法");
        }

        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps("menhu:goods:goods2es");

        // 取出值
        String o = (String) operations.get(String.valueOf(spu.getSpuId()));
        //判断值是否为0
        if ("0".equals(o)){
            // 进行同步操作,将MallGoods转为ESGoods
            ESGoods esGoods = new ESGoods();
            // id
            esGoods.setSpuId(spu.getSpuId());
            // brand 品牌信息
            esGoods.setBrandId(spu.getMallGoodsBrand().getId());
            esGoods.setBrandName(spu.getMallGoodsBrand().getName());
            // 分类信息
            esGoods.setCid1id(spu.getCat1().getId());
            esGoods.setCat1name(spu.getCat1().getName());
            esGoods.setCid2id(spu.getCat2().getId());
            esGoods.setCat2name(spu.getCat2().getName());
            esGoods.setCid3id(spu.getCat3().getId());
            esGoods.setCat3name(spu.getCat3().getName());
            // 维护创建时间,后面可以做排序处理
            esGoods.setCreateTime(new Date());
            // 商品名称
            esGoods.setGoodsName(spu.getGoodsName());
            esGoods.setPrice(spu.getPrice().doubleValue());
            // 图片是字符串形式,需要做处理
            String albumPics = spu.getAlbumPics();
            // 对字符串做处理要先进行非空判断
            if (!StringUtils.isEmpty(albumPics)) {
                String[] split = albumPics.split(",");
                // 非空判断
                if (split != null && split.length > 0) {
                    esGoods.setSmallPic(spu.getAlbumPics());
                }
            }
            // 所有字段赋值完毕之后,同步
            restTemplate.save(esGoods);
            // 改变原状态值
            operations.put(String.valueOf(spu.getSpuId()),"1");
        }
        return new ResultVO(true,"同步ES操作成功");
    }
  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
一、rocketmq入门到精通视频教程目录大纲 001-001_RocketMQ_简介 002-002_RocketMQ_核心概念详解 003-003_RocketMQ_集群构建模型详解(一) 004-004_RocketMQ_集群构建模型详解(二) 005-005_RocketMQ_双主模式集群环境搭建 006-006_RocketMQ_控制台使用讲解 007-007_RocketMQ_Broker配置文件详解 008-008_RocketMQ_helloworld示例讲解 009-009_RocketMQ_整体架构概述详解 010-010_RocketMQ_Producer_API详解 011-011_RocketMQ_Producer_顺序消费机制详解 012-012_RocketMQ_Producer_事务消息机制详解 013-013_RocketMQ_Consumer_Push和Pull模式及使用详解 014-014_RocketMQ_Consumer_配置参数详解 015-015_RocketMQ_Consumer_重试策略详解 016-016_RocketMQ_Consumer_幂等去重策略详解 017-017_RocketMQ_消息模式及使用讲解 018-018_RocketMQ_双主双从集群环境搭建与使用详解 019-019_RocketMQ_FilterServer机制及使用详解 020-020_RocketMQ_管理员命令 二、rocketmq实战视频教程目录大纲 01_rocketmq_实战项目介绍 02_rocketMQ实战项目设计(一) 03_rocketMQ实战项目设计(二) 04_rocketMQ实战-环境搭建(一) 05_rocketMQ实战-环境搭建(二) 06_rocketMQ实战-生产者与spring结合 07_rocketMQ实战-消费者与spring结合 08_rocketMQ实战-数据库模型设计 09_rocketMQ实战-数据库DAO代码生成 10_rocketMQ实战-远程RPC接口设计与实现(一) 11_rocketMQ实战-远程RPC接口设计与实现(二) 12_rocketMQ实战-远程RPC接口设计与实现(三) 13_rocketMQ实战-下单流程(一) 14_rocketMQ实战-下单流程(二) 15_rocketMQ实战-下单流程(三) 16_rocketMQ实战-下单流程(四) 17_rocketMQ实战-下单流程(五) 18_rocketMQ实战-下单流程(六) 19_rocketMQ实战-下单流程(七) 20_rocketMQ实战-下单流程(八)-商品库存 21_rocketMQ实战-下单流程(九)-商品库存 22_rocketMQ实战-下单流程(十)-支付模块 23_rocketMQ实战-整体联调

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值