RocketMQ笔记
一、基础模块
1、MQ介绍
1.1 为什么要用MQ
消息队列是一种“先进先出”的数据结构
其应用场景主要包含以下3个方面
* (1)应用解耦
系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
使用消息队列解耦合,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中,用户的下单操作正常完成。当物流系统恢复后,补充处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。
* (2)流量削峰
应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。
一般情况,为了保证系统的稳定性,如果系统负载超过阈值,就会阻止用户请求,这会影响用户体验,而如果使用消息队列将请求缓存起来,等待系统处理完毕后通知用户下单完毕,这样总不能下单体验要好。
处于经济考量目的:
业务系统正常时段的QPS如果是1000,流量最高峰是10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰
* (3)数据分发
通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可
1.2 MQ的优点和缺点
优点:解耦、削峰、数据分发
缺点包含以下几点:
* (1)系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
如何保证MQ的高可用?
* (2)系统复杂度提高
MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?
* (3)一致性问题
A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。
如何保证消息数据处理的一致性?
1.3 各种MQ产品的比较
常见的MQ产品包括Kafka、ActiveMQ、RabbitMQ、RocketMQ。
2、RocketMQ快速入门
RocketMQ是阿里巴巴2016年MQ中间件,使用Java语言开发,在阿里内部,RocketMQ承接了例如“双11”等高并发场景的消息流转,能够处理万亿级别的消息。
2.1 准备工作
2.1.1 下载 RocketMQ
RocketMQ最新版本:4.5.1
[下载地址](https://www.apache.org/dyn/closer.cgi?path=rocketmq/4.5.1/rocketmq-all-4.5.1-bin-release.zip)
2.1.2 环境要求
* Linux64位系统
* JDK1.8(64位)
* 源码安装需要安装Maven 3.2.x
2.2 安装JDK
如果你已经安装了JDK,跳过此步骤
(1)安装JDK之前,一定要确保提前删除了虚拟机自带的JDK
rpm -qa | grep -i java | xargs -n1 rpm -e --nodeps rpm -qa:查询所安装的所有rpm软件包 grep -i:忽略大小写 xargs -n1:表示每次只传递一个参数 rpm -e –nodeps:强制卸载软件
最后重启虚拟机
reboot
(2) 用XShell传输工具将JDK导入到opt目录下面的software文件夹下面
切换到/opt/software目录,查看是否上传成功
(3)解压JDK到/opt/module目录下
tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/
解压完毕后切换目录到/opt/module/jdk1.8.0_212下
(4)配置JDK环境变量
(a)新建/etc/profile.d/my_env.sh文件
sudo vim /etc/profile.d/my_env.sh
注意截图里面是已经切换到/etc/profile.d目录了
添加如下内容
#JAVA_HOME export JAVA_HOME=/opt/module/jdk1.8.0_212 export PATH=$PATH:$JAVA_HOME/bin
(b)保存退出
:wq
(c)source一下/etc/profile文件,让新的环境变量PATH生效
source /etc/profile
(5) 测试JDK是否安装成功
java -version
如果能看到以下结果,则代表Java安装成功。
java version "1.8.0_212"
2.3 安装RocketMQ
2.3.1 安装步骤
本教程以二进制包方式安装
(1)下载 RocketMQ 安装包 并上传到 /opt/software 目录下
(2) 解压安装包到 /opt/module 目录下
unzip rocketmq-all-4.4.0-bin-release.zip -d /opt/module/
解压完后切换到目录中
2.3.2 目录介绍
* bin:启动脚本,包括shell脚本和CMD脚本
* conf:实例配置文件 ,包括broker配置文件、logback配置文件等
* lib:依赖jar包,包括Netty、commons-lang、FastJSON等
2.3.3 修改安装目录
cd /usr/local/ ls mkdir rocketmq ls cd /opt/module/ ls mv rocketmq-all-4.4.0-bin-release/ /usr/local/rocketmq/ ls cd /usr/local/rocketmq/ ls
2.4 修改初始内存
cd /opt/module/rocketmq-all-4.9.0-bin-release/bin/
RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小
(1)修改runserver.sh
vim runserver.sh
原先的默认值如下:
将其修改如下:
(2)修改runbroker.sh
vim runbroker.sh
原先的默认值如下:
将其修改如下:
2.5 启动RocketMQ
(1)启动NameServer
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release
#1、启动NameServer nohup sh bin/mqnamesrv & #2、查看启动日志 tail -f ~/logs/rocketmqlogs/namesrv.log
(2)启动Broker
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release #1、启动Broker nohup sh bin/mqbroker -n localhost:9876 & #2、查看启动日志 tail -f ~/logs/rocketmqlogs/broker.log
2.6 测试RocketMQ
启动完成之后
2.6.1 发送消息
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release
# 1.设置环境变量 export NAMESRV_ADDR=localhost:9876
# 2.使用安装包的Demo发送消息 sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
2.6.2 接收消息
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release # 1.设置环境变量 export NAMESRV_ADDR=localhost:9876 # 2.使用安装包的Demo发送消息 sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
2.7 关闭RocketMQ
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release # 1.关闭NameServer sh bin/mqshutdown namesrv # 2.关闭Broker sh bin/mqshutdown broker
2.8 控制台的安装与启动
RocketMQ有一个可视化的dashboard,通过该控制台可以直观的查看到很多数据。
(1)下载
(3)添加依赖
在解压目录rocketmq-console的pom.xml中添加如下JAXB依赖。
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
(4)cmd命令打包
mvn clean package -Dmaven.test.skip=true
打包完毕后
(5)启动
注意:一定要将防火墙关闭或者开放相应的端口
java -jar rocketmq-console-ng-1.0.0.jar
(6)访问
2.9 Docker 安装 RocketMQ
关于docker的安装这里我就不再多做介绍了,不清楚的可以看我之前的docker的笔记或者mysql高级的笔记。这里我就只介绍如何使用docker安装rocketmq
(1)搜索镜像
docker search rocketmq
(2)拉取镜像
docker pull rocketmqinc/rocketmq:latest
(3)创建RocketMQ配置文件
#创建目录 mkdir -p /mydata/rocketmq/data mkdir -p /mydata/rocketmq/conf # 创建文件 touch /mydata/rocketmq/conf/broker.conf
配置文件 broker.conf 填入以下内容 ,注意最后一项ip改成你的Linux的ip
brokerClusterName = DefaultCluster brokerName = broker-a brokerId = 0 deleteWhen = 04 fileReservedTime = 48 brokerRole = ASYNC_MASTER flushDiskType = ASYNC_FLUSH brokerIP1 = 192.168.184.204
(4)创建namesrv服务实例
执行下面一条长指令启动容器,注意,长指令不要用我的,修改成你自己的,详解长指令看下面
docker run -d -p 9876:9876 -v /mydata/rocketmq/data/namesrv/logs:/root/logs -v /mydata/rocketmq/data/namesrv/store:/root/store --name rmqnamesrv -e "MAX_POSSIBLE_HEAP=100000000" rocketmqinc/rocketmq:latest sh mqnamesrv
详解
-d # 后台运行 -p #设置默认端口,这里rocketmq默认9876端口 -v #设置映射本地目录到容器内的目录,这个注意我都是把本地的/usr/local/docker/rocketmq/**映射到容器内的对应目录的,这个可以改成你本地的linux目录,当然也可以和我一样。我理解的就是MQ的数据和日志什么的不能放在容器中啊,因为容器毕竟占用的空间有限,就映射一下放在本地目录中。
(5)创建 broker服务实例
docker run -d -p 10911:10911 -p 10909:10909 -v /mydata/rocketmq/data/broker/logs:/root/logs -v /mydata/rocketmq/data/broker/store:/root/store -v /mydata/rocketmq/conf/broker.conf:/opt/rocketmq-latest/conf/broker.conf --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" rocketmqinc/rocketmq:latest sh mqbroker -c /opt/rocketmq-latest/conf/broker.conf
![](https://img2020.cnblogs.com/blog/1211355/202111/1211355-20211116194743823-415768813.png)
注意的地方还是上面需要映射的目录,还有就是linux要开启相应的防火墙端口
(6)安装 rocketmq 控制台
拉取rocketmq控制台的镜像
docker pull pangliang/rocketmq-console-ng
![](https://img2020.cnblogs.com/blog/1211355/202111/1211355-20211116194959667-191826068.png)
后台启动rocketmq的控制台镜像,映射到18080端口
docker run -d -e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.184.204:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" -p 18080:8080 -t pangliang/rocketmq-console-ng
全部启动这3个容器后可通过下面命令查看容器运行状态
![](https://img2020.cnblogs.com/blog/1211355/202111/1211355-20211116195900200-671983152.png)
注意注意:一定要开启防火墙的相应端口
通过看上面的配置需要开启9876、10911、10909、18080四个端口号
#开启9876端口 firewall-cmd --zone=public --add-port=9876/tcp --permanent #开启10911端口 firewall-cmd --zone=public --add-port=10911/tcp --permanent #开启10909端口 firewall-cmd --zone=public --add-port=10909/tcp --permanent #开启18080端口 firewall-cmd --zone=public --add-port=18080/tcp --permanent
重新载入防火墙配置
firewall-cmd --reload
检查成果
打开浏览器访问192.168.184.204:18080 注意访问你linux的ip和上面rocketmq控制台映射的18080端口
![](https://img2020.cnblogs.com/blog/1211355/202111/1211355-20211116195927026-1602153484.png)
3、RocketMQ集群搭建
3.1 各角色介绍
* Producer:消息的发送者;举例:发信者
* Consumer:消息接收者;举例:收信者
* Broker:暂存和传输消息;举例:邮局
* NameServer:管理Broker;举例:各个邮局的管理机构
* Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息
* Message Queue:相当于是Topic的分区;用于并行发送和接收消息
3.2 集群搭建方式
3.2.1 集群特点
(1)- NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
(2)- Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。
(3)- Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
(4)- Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
3.2.3 集群模式
(1)单Master模式
这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用。不建议线上环境使用,可以用于本地测试。
(2)多Master模式
一个集群无Slave,全是Master,例如2个Master或者3个Master,这种模式的优缺点如下:
- 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
- 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
(3)多Master多Slave模式(异步)
每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下:
- 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样;
- 缺点:Master宕机,磁盘损坏情况下会丢失少量消息。
(4)多Master多Slave模式(同步)
每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功,这种模式的优缺点如下:
- 优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;
- 缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。
3.3 主从主双集群搭建
3.3.1 总体架构
消息高可用采用2m-2s(同步双写)方式
3.3.2 集群工作流程
1. 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
2. Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
3. 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
4. Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
5. Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
3.3.3 服务器环境
| **序号** | **IP** | **角色** | **架构模式** |
| -------- | -------------- | ------------------------ | --------------- |
| 1 | 192.168.184.206 | nameserver、brokerserver | Master1、Slave2 |
| 2 | 192.168.184.207 | nameserver、brokerserver | Master2、Slave1
(1)克隆两台虚拟机分别为rocketmq1和rocketmq2
(2)分别修改rocketmq1和rocketmq2的ip信息和主机名称,然后重启服务和虚拟机
#修改ip
vi /etc/sysconfig/network-scripts/ifcfg-ens33
#修改主机名称
vi /etc/hostname
#重启服务
service network restart
#重启虚拟机
reboot
(3)修改Windows系统的host文件,添加以下内容,方便后面xshell直接以主机名连接
192.168.184.206 rocketmq1 192.168.184.207 rocketmq1
(4)测试两台虚拟机之间是否能相互ping通
#rocketmq1 ping rocketmq2 的ip ping 192.168.184.207 #rocketmq2 ping rocketmq1 的ip ping 192.168.184.206
3.3.4 Host添加信息
#rocketmq1修改主机配置
vim /etc/hosts
配置如下:
# nameserver
192.168.184.206 rocketmq-nameserver1
192.168.184.207 rocketmq-nameserver2
# broker
192.168.184.206 rocketmq-master1
192.168.184.206 rocketmq-slave2
192.168.184.207 rocketmq-master2
192.168.184.207 rocketmq-slave1
配置完成后,重启网卡让域名有效
systemctl restart network
3.3.5 防火墙配置
宿主机需要远程访问虚拟机的rocketmq服务和web服务,需要开放相关的端口号,简单粗暴的方式是直接关闭防火墙
# 关闭防火墙 systemctl stop firewalld.service # 查看防火墙的状态 firewall-cmd --state # 禁止firewall开机启动 systemctl disable firewalld.service
或者为了安全,只开放特定的端口号,RocketMQ默认使用3个端口:9876 、10911 、11011 。如果防火墙没有关闭的话,那么防火墙就必须开放这些端口:
* `nameserver` 默认使用 9876 端口
* `master` 默认使用 10911 端口
* `slave` 默认使用11011 端口
执行以下命令:
# 开放name server默认端口 firewall-cmd --remove-port=9876/tcp --permanent # 开放master默认端口 firewall-cmd --remove-port=10911/tcp --permanent # 开放slave默认端口 (当前集群模式可不开启) firewall-cmd --remove-port=11011/tcp --permanent # 重启防火墙 firewall-cmd --reload
3.3.6 环境变量配置
配置环境变量是为了在任何目录下都可以执行对应的命令,不用切换到bin目录
vim /etc/profile.d/my_env.sh
在文件末尾加入如下命令
#set rocketmq ROCKETMQ_HOME=/usr/local/rocketmq/rocketmq-all-4.4.0-bin-release PATH=$PATH:$ROCKETMQ_HOME/bin export ROCKETMQ_HOME PATH
输入:wq! 保存并退出, 并使得配置立刻生效:
#重新加载配置文件使其生效
source /etc/profile
3.3.7 创建消息存储路径
1号服务器(192.168.184.206)
mkdir /usr/local/rocketmq/store/broker-a mkdir /usr/local/rocketmq/store/broker-a/commitlog mkdir /usr/local/rocketmq/store/broker-a/consumequeue mkdir /usr/local/rocketmq/store/broker-a/index mkdir /usr/local/rocketmq/store/broker-b-s mkdir /usr/local/rocketmq/store/broker-b-s/commitlog mkdir /usr/local/rocketmq/store/broker-b-s/consumequeue mkdir /usr/local/rocketmq/store/broker-b-s/index
2号服务器(192.168.184.207)
mkdir /usr/local/rocketmq/store/broker-b mkdir /usr/local/rocketmq/store/broker-b/commitlog mkdir /usr/local/rocketmq/store/broker-b/consumequeue mkdir /usr/local/rocketmq/store/broker-b/index mkdir /usr/local/rocketmq/store/broker-a-s mkdir /usr/local/rocketmq/store/broker-a-s/commitlog mkdir /usr/local/rocketmq/store/broker-a-s/consumequeue mkdir /usr/local/rocketmq/store/broker-a-s/index
注意:以上步骤两台服务器都要设置
3.3.8 broker 配置文件
(1)master1
1号服务器:192.168.184.206
vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a.properties
修改配置如下:
#暴露的外网IP brokerIP1=192.168.184.206 brokerIP2=192.168.184.206 #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 brokerName=broker-a #0 表示 Master,>0 表示 Slave brokerId=0 #nameServer地址,分号分割 namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876 #在发送消息时,自动创建服务器不存在的topic,默认创建的队列数 defaultTopicQueueNums=4 #是否允许 Broker 自动创建Topic,建议线下开启,线上关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭 autoCreateSubscriptionGroup=true #Broker 对外服务的监听端口 listenPort=10911 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=120 #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000 #destroyMapedFileIntervalForcibly=120000 #redeleteHangedFileInterval=120000 #检测物理文件磁盘空间 diskMaxUsedSpaceRatio=88 #存储路径 storePathRootDir=/usr/local/rocketmq/store/broker-a #commitLog 存储路径 storePathCommitLog=/usr/local/rocketmq/store/broker-a/commitlog #消费队列存储路径存储路径 storePathConsumeQueue=/usr/local/rocketmq/store/broker-a/consumequeue #消息索引存储路径 storePathIndex=/usr/local/rocketmq/store/broker-a/index #checkpoint 文件存储路径 storeCheckpoint=/usr/local/rocketmq/store/broker-a/checkpoint #abort 文件存储路径 abortFile=/usr/local/rocketmq/store/broker-a/abort #限制的消息大小 maxMessageSize=65536 #flushCommitLogLeastPages=4 #flushConsumeQueueLeastPages=2 #flushCommitLogThoroughInterval=10000 #flushConsumeQueueThoroughInterval=60000 #Broker 的角色 #- ASYNC_MASTER 异步复制Master #- SYNC_MASTER 同步双写Master #- SLAVE brokerRole=SYNC_MASTER #刷盘方式 #- ASYNC_FLUSH 异步刷盘 #- SYNC_FLUSH 同步刷盘 flushDiskType=SYNC_FLUSH #checkTransactionMessageEnable=false #发消息线程池数量 #sendMessageThreadPoolNums=128 #拉消息线程池数量 #pullMessageThreadPoolNums=128
(2) slave2
1号服务器:192.168.184.206
vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b-s.properties
修改而配置如下:
#暴露的外网IP brokerIP1=192.168.184.206 brokerIP2=192.168.184.206 #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 brokerName=broker-b #0 表示 Master,>0 表示 Slave brokerId=1 #nameServer地址,分号分割 namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876 #在发送消息时,自动创建服务器不存在的topic,默认创建的队列数 defaultTopicQueueNums=4 #是否允许 Broker 自动创建Topic,建议线下开启,线上关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭 autoCreateSubscriptionGroup=true #Broker 对外服务的监听端口 listenPort=11011 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=120 #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000 #destroyMapedFileIntervalForcibly=120000 #redeleteHangedFileInterval=120000 #检测物理文件磁盘空间 diskMaxUsedSpaceRatio=88 #存储路径 storePathRootDir=/usr/local/rocketmq/store/broker-b-s #commitLog 存储路径 storePathCommitLog=/usr/local/rocketmq/store/broker-b-s/commitlog #消费队列存储路径存储路径 storePathConsumeQueue=/usr/local/rocketmq/store/broker-b-s/consumequeue #消息索引存储路径 storePathIndex=/usr/local/rocketmq/store/broker-b-s/index #checkpoint 文件存储路径 storeCheckpoint=/usr/local/rocketmq/store/broker-b-s/checkpoint #abort 文件存储路径 abortFile=/usr/local/rocketmq/store/broker-b-s/abort #限制的消息大小 maxMessageSize=65536 #flushCommitLogLeastPages=4 #flushConsumeQueueLeastPages=2 #flushCommitLogThoroughInterval=10000 #flushConsumeQueueThoroughInterval=60000 #Broker 的角色 #- ASYNC_MASTER 异步复制Master #- SYNC_MASTER 同步双写Master #- SLAVE brokerRole=SLAVE #刷盘方式 #- ASYNC_FLUSH 异步刷盘 #- SYNC_FLUSH 同步刷盘 flushDiskType=ASYNC_FLUSH #checkTransactionMessageEnable=false #发消息线程池数量 #sendMessageThreadPoolNums=128 #拉消息线程池数量 #pullMessageThreadPoolNums=128
(3)master2
2号服务器 192.168.184.207
vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b.properties
修改配置如下:
#暴露的外网IP brokerIP1=192.168.184.207 brokerIP2=192.168.184.207 #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 brokerName=broker-b #0 表示 Master,>0 表示 Slave brokerId=0 #nameServer地址,分号分割 namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876 #在发送消息时,自动创建服务器不存在的topic,默认创建的队列数 defaultTopicQueueNums=4 #是否允许 Broker 自动创建Topic,建议线下开启,线上关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭 autoCreateSubscriptionGroup=true #Broker 对外服务的监听端口 listenPort=10911 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=120 #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000 #destroyMapedFileIntervalForcibly=120000 #redeleteHangedFileInterval=120000 #检测物理文件磁盘空间 diskMaxUsedSpaceRatio=88 #存储路径 storePathRootDir=/usr/local/rocketmq/store/broker-b #commitLog 存储路径 storePathCommitLog=/usr/local/rocketmq/store/broker-b/commitlog #消费队列存储路径存储路径 storePathConsumeQueue=/usr/local/rocketmq/store/broker-b/consumequeue #消息索引存储路径 storePathIndex=/usr/local/rocketmq/store/broker-b/index #checkpoint 文件存储路径 storeCheckpoint=/usr/local/rocketmq/store/broker-b/checkpoint #abort 文件存储路径 abortFile=/usr/local/rocketmq/store/broker-b/abort #限制的消息大小 maxMessageSize=65536 #flushCommitLogLeastPages=4 #flushConsumeQueueLeastPages=2 #flushCommitLogThoroughInterval=10000 #flushConsumeQueueThoroughInterval=60000 #Broker 的角色 #- ASYNC_MASTER 异步复制Master #- SYNC_MASTER 同步双写Master #- SLAVE brokerRole=SYNC_MASTER #刷盘方式 #- ASYNC_FLUSH 异步刷盘 #- SYNC_FLUSH 同步刷盘 flushDiskType=SYNC_FLUSH #checkTransactionMessageEnable=false #发消息线程池数量 #sendMessageThreadPoolNums=128 #拉消息线程池数量 #pullMessageThreadPoolNums=128
(4) slave1
2号服务器:192.168.184.207
vi /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a-s.properties
修改配置如下:
#暴露的外网IP brokerIP1=192.168.184.207 brokerIP2=192.168.184.207 #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 brokerName=broker-a #0 表示 Master,>0 表示 Slave brokerId=1 #nameServer地址,分号分割 namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876 #在发送消息时,自动创建服务器不存在的topic,默认创建的队列数 defaultTopicQueueNums=4 #是否允许 Broker 自动创建Topic,建议线下开启,线上关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭 autoCreateSubscriptionGroup=true #Broker 对外服务的监听端口 listenPort=11011 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=120 #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000 #destroyMapedFileIntervalForcibly=120000 #redeleteHangedFileInterval=120000 #检测物理文件磁盘空间 diskMaxUsedSpaceRatio=88 #存储路径 storePathRootDir=/usr/local/rocketmq/store/broker-a-s #commitLog 存储路径 storePathCommitLog=/usr/local/rocketmq/store/broker-a-s/commitlog #消费队列存储路径存储路径 storePathConsumeQueue=/usr/local/rocketmq/store/broker-a-s/consumequeue #消息索引存储路径 storePathIndex=/usr/local/rocketmq/store/broker-a-s/index #checkpoint 文件存储路径 storeCheckpoint=/usr/local/rocketmq/store/broker-a-s/checkpoint #abort 文件存储路径 abortFile=/usr/local/rocketmq/store/broker-a-s/abort #限制的消息大小 maxMessageSize=65536 #flushCommitLogLeastPages=4 #flushConsumeQueueLeastPages=2 #flushCommitLogThoroughInterval=10000 #flushConsumeQueueThoroughInterval=60000 #Broker 的角色 #- ASYNC_MASTER 异步复制Master #- SYNC_MASTER 同步双写Master #- SLAVE brokerRole=SLAVE #刷盘方式 #- ASYNC_FLUSH 异步刷盘 #- SYNC_FLUSH 同步刷盘 flushDiskType=ASYNC_FLUSH #checkTransactionMessageEnable=false #发消息线程池数量 #sendMessageThreadPoolNums=128 #拉消息线程池数量 #pullMessageThreadPoolNums=128
3.3.9 修改启动脚本文件
(1)runbroker.sh
vim /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin/runbroker.sh
需要根据内存大小进行适当的对JVM参数进行调整:
(2)runserver.sh
vim /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin/runserver.sh
注意:两台服务器这里都要修改更新
3.3.10 服务启动
(1)启动NameServe集群
分别在 192.168.184.206 和 192.168.184.207 启动NameServe
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin nohup sh mqnamesrv &
jps
(2)启动Broker集群
1、在 1号服务器(192.168.184.206 )上启动master1和slave2
(a)master1
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a.properties &
jps
(b)slave2
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b-s.properties & jps
2、在 1号服务器(192.168.184.207 )上启动master1和slave2
(a)master2
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-b.properties & jps
(b)slave1
cd /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/bin nohup sh mqbroker -c /usr/local/rocketmq/rocketmq-all-4.4.0-bin-release/conf/2m-2s-sync/broker-a-s.properties & jps
3.3.11 查看进程状态
启动后通过JPS查看启动进程
3.3.12 查看日志
# 查看nameServer日志 tail -500f ~/logs/rocketmqlogs/namesrv.log # 查看broker日志 tail -500f ~/logs/rocketmqlogs/broker.log
3.4 mqadmin管理工具
3.4.1 使用方式
进入RocketMQ安装位置,在bin目录下执行
./mqadmin {command} {args}
3.4.2 命令介绍
(1)topic相关
名称 | 含义 | 命令选项 | 说明 |
updateTopic | 创建更新Topic配置 | -b | Broker 地址,表示 topic 所在 Broker,只支持单台Broker,地址为ip:port |
-c | cluster 名称,表示 topic 所在集群(集群可通过 clusterList 查询) | ||
-h- | 打印帮助 | ||
-n | NameServer服务地址,格式 ip:port | ||
-p | 指定新topic的读写权限( W=2|R=4|WR=6 ) | ||
-r | 可读队列数(默认为 8) | ||
-w | 可写队列数(默认为 8) | ||
-t | topic 名称(名称只能使用字符 ^[a-zA-Z0-9_-]+$ ) | ||
deleteTopic | 删除Topic | -c | cluster 名称,表示删除某集群下的某个 topic (集群 可通过 clusterList 查询) |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic 名称(名称只能使用字符 ^[a-zA-Z0-9_-]+$ ) | ||
topicList | 查看 Topic 列表信息 | -h | 打印帮助 |
-c | 不配置-c只返回topic列表,增加-c返回clusterName, topic, consumerGroup信息,即topic的所属集群和订阅关系,没有参数 | ||
-n | NameServer 服务地址,格式 ip:port | ||
topicRoute | 查看 Topic 路由信息 | -t | topic 名称 |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
topicStatus | 查看 Topic 消息队列offset | -t | topic 名称 |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
topicClusterList | 查看 Topic 所在集群列表 | -t | topic 名称 |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
updateTopicPerm | 更新 Topic 读写权限 | -t | topic 名称 |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
-b | Broker 地址,表示 topic 所在 Broker,只支持单台Broker,地址为ip:port | ||
-p | 指定新 topic 的读写权限( W=2|R=4|WR=6 ) | ||
-c | cluster 名称,表示 topic 所在集群(集群可通过 clusterList 查询),-b优先,如果没有-b,则对集群中所有Broker执行命令 | ||
updateOrderConf | 从NameServer上创建、删除、获取特定命名空间的kv配置,目前还未启用 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic,键 | ||
-v | orderConf,值 | ||
-m | method,可选get、put、delete | ||
allocateMQ | 以平均负载算法计算消费者列表负载消息队列的负载结果 | -t | topic 名称 |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
-i | ipList,用逗号分隔,计算这些ip去负载Topic的消息队列 | ||
statsAll | 打印Topic订阅关系、TPS、积累量、24h读写总量等信息 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-a | 是否只打印活跃topic | ||
-t | 指定topic |
(2)集群相关
名称 | 含义 | 命令选项 | 说明 |
clusterList | 查看集群信息,集群、BrokerName、BrokerId、TPS等信息 | -m | 打印更多信息 (增加打印出如下信息 #InTotalYest, #OutTotalYest, #InTotalToday ,#OutTotalToday) |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
-i | 打印间隔,单位秒 | ||
clusterRT | 发送消息检测集群各Broker RT。消息发往${BrokerName} Topic。 | -a | amount,每次探测的总数,RT = 总时间 / amount |
-s | 消息大小,单位B | ||
-c | 探测哪个集群 | ||
-p | 是否打印格式化日志,以|分割,默认不打印 | ||
-h | 打印帮助 | ||
-m | 所属机房,打印使用 | ||
-i | 发送间隔,单位秒 | ||
-n | NameServer 服务地址,格式 ip:port |
(3)Broker相关
名称 | 含义 | 命令选项 | 说明 |
updateBrokerConfig | 更新 Broker 配置文件,会修改Broker.conf | -b | Broker 地址,格式为ip:port |
-c | cluster 名称 | ||
-k | key 值 | ||
-v | value 值 | ||
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
brokerStatus | 查看 Broker 统计信息、运行状态(你想要的信息几乎都在里面) | -b | Broker 地址,地址为ip:port |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
brokerConsumeStats | Broker中各个消费者的消费情况,按Message Queue维度返回Consume Offset,Broker Offset,Diff,TImestamp等信息 | -b | Broker 地址,地址为ip:port |
-t | 请求超时时间 | ||
-l | diff阈值,超过阈值才打印 | ||
-o | 是否为顺序topic,一般为false | ||
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
getBrokerConfig | 获取Broker配置 | -b | Broker 地址,地址为ip:port |
-n | NameServer 服务地址,格式 ip:port | ||
wipeWritePerm | 从NameServer上清除 Broker写权限 | -b | Broker 地址,地址为ip:port |
-n | NameServer 服务地址,格式 ip:port | ||
-h | 打印帮助 | ||
cleanExpiredCQ | 清理Broker上过期的Consume Queue,如果手动减少对列数可能产生过期队列 | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-b | Broker 地址,地址为ip:port | ||
-c | 集群名称 | ||
cleanUnusedTopic | 清理Broker上不使用的Topic,从内存中释放Topic的Consume Queue,如果手动删除Topic会产生不使用的Topic | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-b | Broker 地址,地址为ip:port | ||
-c | 集群名称 | ||
sendMsgStatus | 向Broker发消息,返回发送状态和RT | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-b | BrokerName,注意不同于Broker地址 | ||
-s | 消息大小,单位B | ||
-c | 发送次数 |
(4)消息相关
名称 | 含义 | 命令选项 | 说明 |
queryMsgById | 根据offsetMsgId查询msg,如果使用开源控制台,应使用offsetMsgId,此命令还有其他参数,具体作用请阅读QueryMsgByIdSubCommand。 | -i | msgId |
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
queryMsgByKey | 根据消息 Key 查询消息 | -k | msgKey |
-t | Topic 名称 | ||
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
queryMsgByOffset | 根据 Offset 查询消息 | -b | Broker 名称,(这里需要注意 填写的是 Broker 的名称,不是 Broker 的地址,Broker 名称可以在 clusterList 查到) |
-i | query 队列 id | ||
-o | offset 值 | ||
-t | topic 名称 | ||
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
queryMsgByUniqueKey | 根据msgId查询,msgId不同于offsetMsgId,区别详见常见运维问题。-g,-d配合使用,查到消息后尝试让特定的消费者消费消息并返回消费结果 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-i | uniqe msg id | ||
-g | consumerGroup | ||
-d | clientId | ||
-t | topic名称 | ||
checkMsgSendRT | 检测向topic发消息的RT,功能类似clusterRT | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic名称 | ||
-a | 探测次数 | ||
-s | 消息大小 | ||
sendMessage | 发送一条消息,可以根据配置发往特定Message Queue,或普通发送。 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic名称 | ||
-p | body,消息体 | ||
-k | keys | ||
-c | tags | ||
-b | BrokerName | ||
-i | queueId | ||
consumeMessage | 消费消息。可以根据offset、开始&结束时间戳、消息队列消费消息,配置不同执行不同消费逻辑,详见ConsumeMessageCommand。 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic名称 | ||
-b | BrokerName | ||
-o | 从offset开始消费 | ||
-i | queueId | ||
-g | 消费者分组 | ||
-s | 开始时间戳,格式详见-h | ||
-d | 结束时间戳 | ||
-c | 消费多少条消息 | ||
printMsg | 从Broker消费消息并打印,可选时间段 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic名称 | ||
-c | 字符集,例如UTF-8 | ||
-s | subExpress,过滤表达式 | ||
-b | 开始时间戳,格式参见-h | ||
-e | 结束时间戳 | ||
-d | 是否打印消息体 | ||
printMsgByQueue | 类似printMsg,但指定Message Queue | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-t | topic名称 | ||
-i | queueId | ||
-a | BrokerName | ||
-c | 字符集,例如UTF-8 | ||
-s | subExpress,过滤表达式 | ||
-b | 开始时间戳,格式参见-h | ||
-e | 结束时间戳 | ||
-p | 是否打印消息 | ||
-d | 是否打印消息体 | ||
-f | 是否统计tag数量并打印 | ||
resetOffsetByTime | 按时间戳重置offset,Broker和consumer都会重置 | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-g | 消费者分组 | ||
-t | topic名称 | ||
-s | 重置为此时间戳对应的offset | ||
-f | 是否强制重置,如果false,只支持回溯offset,如果true,不管时间戳对应offset与consumeOffset关系 | ||
-c | 是否重置c++客户端offset |
(5)消费者、消费组相关
名称 | 含义 | 命令选项 | 说明 |
consumerProgress | 查看订阅组消费状态,可以查看具体的client IP的消息积累量 | -g | 消费者所属组名 |
-s | 是否打印client IP | ||
-h | 打印帮助 | ||
-n | NameServer 服务地址,格式 ip:port | ||
consumerStatus | 查看消费者状态,包括同一个分组中是否都是相同的订阅,分析Process Queue是否堆积,返回消费者jstack结果,内容较多,使用者参见ConsumerStatusSubCommand | -h | 打印帮助 |
-n | NameServer 服务地址,格式 ip:port | ||
-g | consumer group | ||
-i | clientId | ||
-s | 是否执行jstack | ||
getConsumerStatus | 获取 Consumer 消费进度 | -g | 消费者所属组名 |
-t | 查询主题 | ||
-i | Consumer 客户端 ip | ||
-n | NameServer 服务地址,格式 ip:port | ||
-h | 打印帮助 | ||
updateSubGroup | 更新或创建订阅关系 | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-b | Broker地址 | ||
-c | 集群名称 | ||
-g | 消费者分组名称 | ||
-s | 分组是否允许消费 | ||
-m | 是否从最小offset开始消费 | ||
-d | 是否是广播模式 | ||
-q | 重试队列数量 | ||
-r | 最大重试次数 | ||
-i | 当slaveReadEnable开启时有效,且还未达到从slave消费时建议从哪个BrokerId消费,可以配置备机id,主动从备机消费 | ||
-w | 如果Broker建议从slave消费,配置决定从哪个slave消费,配置BrokerId,例如1 | ||
-a | 当消费者数量变化时是否通知其他消费者负载均衡 | ||
deleteSubGroup | 从Broker删除订阅关系 | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-b | Broker地址 | ||
-c | 集群名称 | ||
-g | 消费者分组名称 | ||
cloneGroupOffset | 在目标群组中使用源群组的offset | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-s | 源消费者组 | ||
-d | 目标消费者组 | ||
-t | topic名称 | ||
-o | 暂未使用 |
(6)连接相关
名称 | 含义 | 命令选项 | 说明 |
consumerConnec tion | 查询 Consumer 的网络连接 | -g | 消费者所属组名 |
-n | NameServer 服务地址,格式 ip:port | ||
-h | 打印帮助 | ||
producerConnec tion | 查询 Producer 的网络连接 | -g | 生产者所属组名 |
-t | 主题名称 | ||
-n | NameServer 服务地址,格式 ip:port | ||
-h | 打印帮助 |
(7)NameServer相关
名称 | 含义 | 命令选项 | 说明 |
updateKvConfig | 更新NameServer的kv配置,目前还未使用 | -s | 命名空间 |
-k | key | ||
-v | value | ||
-n | NameServer 服务地址,格式 ip:port | ||
-h | 打印帮助 | ||
deleteKvConfig | 删除NameServer的kv配置 | -s | 命名空间 |
-k | key | ||
-n | NameServer 服务地址,格式 ip:port | ||
-h | 打印帮助 | ||
getNamesrvConfig | 获取NameServer配置 | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
updateNamesrvConfig | 修改NameServer配置 | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 | ||
-k | key | ||
-v | value |
(8)其他
名称 | 含义 | 命令选项 | 说明 |
startMonitoring | 开启监控进程,监控消息误删、重试队列消息数等 | -n | NameServer 服务地址,格式 ip:port |
-h | 打印帮助 |
3.4.2 注意事项
* 几乎所有命令都需要配置-n表示NameServer地址,格式为ip:port
* 几乎所有命令都可以通过-h获取帮助
* 如果既有Broker地址(-b)配置项又有clusterName(-c)配置项,则优先以Broker地址执行命令;如果不配置Broker地址,则对集群中所有主机执行命令
3.5 集群监控平台搭建
3.5.1 概述
RocketMQ有一个对其扩展的开源项目incubator-rocketmq-externals,这个项目中有一个子模块叫rocketmq-console,这个便是管理控制台项目了,先将incubator-rocketmq-externals拉到本地,因为我们需要自己对rocketmq-console进行编译打包运行,本身就是一个springboot项目,因此修改好内容就可以直接打包。
3.5.2 下载并编译打包
git clone https://github.com/apache/rocketmq-externals cd rocketmq-console //切换到目录
注意:打包前在rocketmq-console
中的application.properties配置namesrv
集群地址:
打包
mvn clean package -Dmaven.test.skip=true//跳过测试打包
启动rocketmq-console:(随便在哪一台服务器上运行都可以,我在一号服务器上运行的)
先上传到服务器1的 /opt/module目录下
启动成功后,我们就可以通过浏览器访问http://一号服务器公网IP:8080
进入控制台界面了,如下图:
集群状态
3.6 docker搭建rocketmq双主双从模式(同步双写)
(1)准备两台已安装docker的服务器
1号服务器:192.168.184.208 2号服务器:192.168.184.209
部署内容如下图所示
(2)创建存储数据的目录
分别在 192.168.184.208、192.168.184.209 服务器上执行下面命令创建目录:
# 创建 Broker-Master 持久化目录 mkdir -p /mydata/rocketmq-cluster/broker-master/conf && \ mkdir -p /mydata/rocketmq-cluster/broker-master/logs && \ mkdir -p /mydata/rocketmq-cluster/broker-master/store # 创建 Broker-Slave 持久化目录 mkdir -p /mydata/rocketmq-cluster/broker-slave/conf && \ mkdir -p /mydata/rocketmq-cluster/broker-slave/logs && \ mkdir -p /mydata/rocketmq-cluster/broker-slave/store #创建 NameServer 持久化目录 mkdir -p /mydata/rocketmq-cluster/server/logs
(3)创建 Broker 配置文件
在各个服务器上分别创建如下文件:
- 服务器 192.168.184.208: 创建 master-a、slave-b 配置文件
- 服务器 192.168.184.209: 创建 master-b、slave-a 配置文件
第一台服务器创建 master-a 配置:
cd /mydata/rocketmq-cluster/broker-master/conf/
vim broker.conf
listenPort=10911 brokerIP1=192.168.184.208 brokerClusterName=DefaultCluster brokerName=broker-a brokerRole=SYNC_MASTER brokerId=0 deleteWhen=04 fileReservedTime=48 flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.184.208:9876;192.168.184.209:9876 enablePropertyFilter=true
第一台服务器创建 slave-b 配置:
cd /mydata/rocketmq-cluster/broker-slave/conf/
vim broker.conf
listenPort=11911 brokerIP1=192.168.184.208 brokerClusterName=DefaultCluster brokerName=broker-b brokerRole=SLAVE brokerId=1 deleteWhen=04 fileReservedTime=48 flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.184.208:9876;192.168.184.209:9876 enablePropertyFilter=true
第二台服务器创建 master-b 配置:
cd /mydata/rocketmq-cluster/broker-master/conf/
vim broker.conf
listenPort=10911 brokerIP1=192.168.184.209 brokerClusterName=DefaultCluster brokerName=broker-b brokerRole=SYNC_MASTER brokerId=0 deleteWhen=04 fileReservedTime=48 flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.184.209:9876;192.168.184.209:9876 enablePropertyFilter=true
第二台服务器创建 slave-a 配置:
cd /mydata/rocketmq-cluster/broker-slave/conf/
vim broker.conf
listenPort=11911 brokerIP1=192.168.184.209 brokerClusterName=DefaultCluster brokerName=broker-a brokerRole=SLAVE brokerId=1 deleteWhen=04 fileReservedTime=48 flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.184.208:9876;192.168.184.209:9876 enablePropertyFilter=true
(4)下载相关镜像
在两台服务器上,分别创建拉取对应的镜像
- 服务器一 192.168.184.208: 拉取 rocketmq 与 rocketmq-console 镜像。
- 服务器二 192.168.184.209: 拉取 rocketmq 镜像。
两台服务器分别拉取 RocketMQ 镜像:
# 下载 RocketMQ 镜像 docker pull foxiswho/rocketmq:4.7.0
第一台服务器拉取 RocketMQ Console 镜像:
# 下载 RocketMQ 控制台 docker pull apacherocketmq/rocketmq-console:2.0.0
(5)查看镜像设置的用户与组的配置
任意一台服务器上查看镜像 rocketmq 的组成,分析其中用户与组的信息:
docker history foxiswho/rocketmq:4.7.0
可以观察到:
- 组名:rocketmq,组ID:3000
- 用户名:rocketmq,用户ID:3000
(6) 更改目录归属组与用户与容器的配置一致
两台服务器上分别创建组与用户:
# 创建组 groupadd rocketmq # 增加用户并加入组 useradd -g rocketmq rocketmq # 设置用户密码 passwd rocketmq # 更改组的 gid groupmod -g 3000 rocketmq # 更改用户的 uid usermod -u 3000 rocketmq #查看是否更改成功 id rocketmq
两台服务器上分别更改上面创建的目录的权限为上面创建的组与用户:
chown -R rocketmq:rocketmq /mydata/rocketmq-cluster/
(7)安装 RocketMQ NameServer
两台服务器上分别部署 NameServer:
docker run -d --name rmqnamesr --net host \ --restart=always \ -v /mydata/rocketmq-cluster/server/logs:/home/rocketmq/logs \ -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \ -p 9876:9876 \ foxiswho/rocketmq:4.7.0 \ sh mqnamesrv
(8)安装 RocketMQ Broker
分别在两台服务器上执行下面命令,操作 Docker 启动 Broker Master 与 Broker Slave 节点:
- 服务器 192.168.184.208: Broker master-a、Broker slave-b
- 服务器 192.168.184.209: Broker master-b、Broker slave-a
服务器一部署 Broker master-a,服务器二部署 Broker master-b 的 Docker 命令:
docker run -d --name rmqbroker-master \ --restart=always \ --net host \ -v /mydata/rocketmq-cluster/broker-master/logs:/home/rocketmq/logs \ -v /mydata/rocketmq-cluster/broker-master/store:/home/rocketmq/store \ -v /mydata/rocketmq-cluster/broker-master/conf:/home/rocketmq/conf \ -p 10909:10909 -p 10911:10911 -p 10912:10912 \ -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \ foxiswho/rocketmq:4.7.0 \ sh mqbroker -c /home/rocketmq/conf/broker.conf
服务器一部署 Broker slave-b,服务器二部署 Broker slave-a 的 Docker 命令:
docker run -d --name rmqbroker-slave \ --restart=always \ --net host \ -v /mydata/rocketmq-cluster/broker-slave/logs:/home/rocketmq/logs \ -v /mydata/rocketmq-cluster/broker-slave/store:/home/rocketmq/store \ -v /mydata/rocketmq-cluster/broker-slave/conf:/home/rocketmq/conf \ -p 11909:11909 -p 11911:11911 -p 11912:11912 \ -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \ foxiswho/rocketmq:4.7.0 \ sh mqbroker -c /home/rocketmq/conf/broker.conf
(9)部署控制台
服务器一部署 RocketMQ 控制台:
docker run -d --name rmqconsole \ --restart=always \ -p 8088:8080 \ -e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.184.208:9876;192.168.184.209:9876 \ -Dcom.rocketmq.sendMessageWithVIPChannel=false" \ apacherocketmq/rocketmq-console:2.0.0
(10)访问控制台
输入 http://192.168.184.208:8088 访问在服务器一部署的 RocketMQ 控制台:
4、消息发送样例
准备工作
创建 SpringBoot 工程
(1)导入MQ客户端依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.7.0</version> </dependency>
注意:这里的版本必须与远程的版本一样,我用docker搭建的集群版本是4.7的,否则会报以下错误:
Exception in thread main org.apache.rocketmq.client.exception.MQClientException: No route info of this topic
(2)消息发送者步骤分析
1.创建消息生产者producer,并制定生产者组名 2.指定Nameserver地址 3.启动producer 4.创建消息对象,指定主题Topic、Tag和消息体 5.发送消息 6.关闭生产者producer
(3)消息消费者步骤分析
1.创建消费者Consumer,制定消费者组名 2.指定Nameserver地址 3.订阅主题Topic和Tag 4.设置回调函数,处理消息 5.启动消费者consumer
4.1 基本样例
4.1.1 消息发送
(1)发送同步消息
这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
package com.itheima.mq.rocket.base.producer; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; /** * 发送同步消息 */ public class SyncProducer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); for (int i=0; i<10; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("base","Tag1", ("hello world"+i).getBytes()); //5.发送消息 SendResult result = producer.send(msg); //发送状态 SendStatus sendStatus = result.getSendStatus(); //消息ID String msgId = result.getMsgId(); //消息接收队列ID int queueId = result.getMessageQueue().getQueueId(); System.out.println("发送状态:"+sendStatus+",消息ID"+msgId+",队列"+queueId+",条数"+i); TimeUnit.SECONDS.sleep(1); } //6.关闭生产者producer producer.shutdown(); } }
运行结果
(2)发送异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
package com.itheima.mq.rocket.base.producer; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; /** * 发送异步消息 */ public class AsyncProducer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); for (int i=0; i<10; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("base","Tag2", ("hello world"+i).getBytes()); //5.发送异步消息 producer.send(msg, new SendCallback() { /** * 发送成功回调函数 * @param sendResult */ @Override public void onSuccess(SendResult sendResult) { System.out.println("发送结果"+sendResult); } /** * 发送失败回调函数 * @param throwable */ @Override public void onException(Throwable e) { System.out.println("发送异常:"+e); } }); //线程睡眠1秒 TimeUnit.SECONDS.sleep(1); } //6.关闭生产者producer producer.shutdown(); } }
发送结果
(3)单向发送消息
这种方式主要用在不特别关心发送结果的场景,例如日志发送。
package com.itheima.mq.rocket.base.producer; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; /** * 发送单向消息 */ public class OneWayProducer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); for (int i=0; i<10; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("base","Tag3", ("hello world,单向消息"+i).getBytes()); //5.发送消息 producer.sendOneway(msg); //线程睡眠1秒 TimeUnit.SECONDS.sleep(1); } //6.关闭生产者producer producer.shutdown(); } }
4.1.2 消费消息
(1)负载均衡模式(默认)
消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同
package com.itheima.mq.rocket.base.consumer; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; /** * 消息的接收者 */ public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("base","Tag2"); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println(new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); } }
消费信息
(2)广播模式
指的是不同的comsumer接收到的消息是一样的
package com.itheima.mq.rocket.base.consumer; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.List; /** * 消息的接收者 */ public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("base","Tag1"); //设定消费模式:负载均衡/广播模式 consumer.setMessageModel(MessageModel.BROADCASTING); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println(new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); } }
4.2 顺序消息
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。
顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
创建订单生成类
package com.itheima.mq.rocket.order; import java.util.ArrayList; import java.util.List; /** * 订单的步骤 */ public class OrderStep { private long orderId; private String desc; public long getOrderId() { return orderId; } public void setOrderId(long 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 + '\'' + '}'; } /** * 生成模拟订单数据 */ public static List<OrderStep> buildOrders() { List<OrderStep> orderList = new ArrayList<OrderStep>(); //15103111039L:创建、付款、推送、完成 //15103111065L:创建、付款、完成 //15103117235L:创建、付款、完成 OrderStep orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("创建"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111065L); orderDemo.setDesc("创建"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("付款"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103117235L); orderDemo.setDesc("创建"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111065L); orderDemo.setDesc("付款"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103117235L); orderDemo.setDesc("付款"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111065L); orderDemo.setDesc("完成"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("推送"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103117235L); orderDemo.setDesc("完成"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("完成"); orderList.add(orderDemo); return orderList; } }
4.2.1 顺序消息生产
package com.itheima.mq.rocket.order; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.MessageQueueSelector; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.List; public class Producer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); //构建消息集合 List<OrderStep> orderSteps = OrderStep.buildOrders(); //发送消息 for (int i=0; i<orderSteps.size(); i++) { String body = orderSteps.get(i)+""; Message message = new Message("orderTopic","order","i"+i, body.getBytes()); /** * 参数一:消息对象 * 参数二:消息队列的选择器 * 参数三:选择队列的业务标识(订单ID) */ SendResult sendResult = producer.send(message, new MessageQueueSelector() { /** * 参数一:队列集合 * 参数二:消息对象 * 参数三:业务标识参数 * @param mqs * @param message * @param arg * @return */ @Override public MessageQueue select(List<MessageQueue> mqs, Message message, Object arg) { long orderId = (long) arg; long index = orderId % mqs.size(); return mqs.get((int) index); } }, orderSteps.get(i).getOrderId()); System.out.println("发送结果:"+sendResult); } } }
4.2.2 顺序消费消息
package com.itheima.mq.rocket.order; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("orderTopic","*"); //4、注册消息监听器 consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext consumeOrderlyContext) { for (MessageExt msg : msgs) { System.out.println("线程名称:【"+Thread.currentThread().getName()+"】消费消息:"+new String(msg.getBody())); } return ConsumeOrderlyStatus.SUCCESS; } }); //5、启动消费者 consumer.start(); System.out.println("消费者启动"); } }
消费结果
从上图的结果我们可以看出来实现了分区有序,即一个线程只完成唯一标识的订单消息
4.3 延时消息
比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
4.3.1 启动消息消费者
package com.itheima.mq.rocket.delay; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.List; public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("DelayTopic","*"); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println("消息ID:【" + msg.getMsgId() + "】,延迟时间:" + (System.currentTimeMillis() - msg.getStoreTimestamp())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); System.out.println("消费者启动"); } }
4.3.2 发送延时消息
package com.itheima.mq.rocket.delay; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; public class Producer { public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); for (int i=0; i<10; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("DelayTopic","Tag1", ("hello world"+i).getBytes()); //设定延迟时间 msg.setDelayTimeLevel(2); //5.发送消息 SendResult result = producer.send(msg); //发送状态 SendStatus sendStatus = result.getSendStatus(); //消息ID String msgId = result.getMsgId(); //消息接收队列ID int queueId = result.getMessageQueue().getQueueId(); System.out.println("发送状态:"+sendStatus+",消息ID"+msgId+",队列"+queueId+",条数"+i); TimeUnit.SECONDS.sleep(1); } //6.关闭生产者producer producer.shutdown(); } }
4.3.3 验证
4.3.4 使用限制
现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18
// org/apache/rocketmq/store/config/MessageStoreConfig.java private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
4.4 批量消息
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB。
4.1.1 发送批量消息
如果您每次只发送不超过4MB的消息,则很容易使用批处理,样例如下:
生产者代码
package com.itheima.mq.rocket.batch; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class Producer { public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); List<Message> msgs = new ArrayList<Message>(); //4、创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg1 = new Message("BatchTopic","Tag1",("Hello World"+1).getBytes()); Message msg2 = new Message("BatchTopic","Tag1",("Hello World"+2).getBytes()); Message msg3 = new Message("BatchTopic","Tag1",("Hello World"+3).getBytes()); msgs.add(msg1); msgs.add(msg2); msgs.add(msg3); //5、发送消息 SendResult result = producer.send(msgs); //发送状态 SendStatus status = result.getSendStatus(); System.out.println("发送结果:" + result); //线程睡眠1秒 TimeUnit.SECONDS.sleep(1); //6.关闭生产者producer producer.shutdown(); } }
消费者代码
package com.itheima.mq.rocket.batch; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.List; /** * 消息的接收者 */ public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("BatchTopic","*"); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println(new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); System.out.println("消费者启动"); } }
发送结果
消息分割
如果消息的总长度可能大于4MB时,这时候最好把消息进行分割
package com.itheima.mq.rocket.batch; import org.apache.rocketmq.common.message.Message; import java.util.Iterator; import java.util.List; import java.util.Map; public class ListSplitter implements Iterator<List<Message>> { private final int SIZE_LIMIT = 1024 * 1024 * 4; private final List<Message> messages; private int currIndex; public ListSplitter(List<Message> messages) { this.messages = messages; } @Override public boolean hasNext() { return currIndex < messages.size(); } @Override public List<Message> next() { int nextIndex = currIndex; int totalSize = 0; for (; nextIndex < messages.size(); nextIndex++) { Message message = messages.get(nextIndex); int tmpSize = message.getTopic().length() + message.getBody().length; Map<String, String> properties = message.getProperties(); for (Map.Entry<String, String> entry : properties.entrySet()) { tmpSize += entry.getKey().length() + entry.getValue().length(); } tmpSize = tmpSize + 20; // 增加日志的开销20字节 if (tmpSize > SIZE_LIMIT) { //单个消息超过了最大的限制 //忽略,否则会阻塞分裂的进程 if (nextIndex - currIndex == 0) { //假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环 nextIndex++; } break; } if (tmpSize + totalSize > SIZE_LIMIT) { break; } else { totalSize += tmpSize; } } List<Message> subList = messages.subList(currIndex, nextIndex); currIndex = nextIndex; return subList; } }
4.5 过滤消息
4.5.1 Tag 过滤
在大多数情况下,TAG是一个简单而有用的设计,其可以来选择您想要的消息。例如:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
消费者将接收包含TAGA或TAGB或TAGC的消息。但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息。SQL特性可以通过发送消息时的属性来进行计算。在RocketMQ定义的语法下,可以实现一些简单的逻辑。下面是一个例子:
------------ | message | |----------| a > 5 AND b = 'abc' | a = 10 | --------------------> Gotten | b = 'abc'| | c = true | ------------ ------------ | message | |----------| a > 5 AND b = 'abc' | a = 1 | --------------------> Missed | b = 'abc'| | c = true | ------------
生产者代码
package com.itheima.mq.rocket.filter.tag; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; public class Producer { public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); for (int i=0; i<3; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("FilterTagTopic","Tag2", ("hello world"+i).getBytes()); //5.发送消息 SendResult result = producer.send(msg); //发送状态 SendStatus sendStatus = result.getSendStatus(); //消息ID String msgId = result.getMsgId(); //消息接收队列ID int queueId = result.getMessageQueue().getQueueId(); System.out.println("发送状态:"+sendStatus+",消息ID"+msgId+",队列"+queueId+",条数"+i); TimeUnit.SECONDS.sleep(1); } //6.关闭生产者producer producer.shutdown(); } }
消费者代码
package com.itheima.mq.rocket.filter.tag; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.List; public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("FilterTagTopic","Tag1 || Tag2"); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println(new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); System.out.println("消费者启动"); } }
4.5.2 SQL 基本语法
只有使用push模式的消费者才能用使用SQL92标准的sql语句,接口如下:
public void subscribe(finalString topic, final MessageSelector messageSelector)
发送消息时,你能通过putUserProperty
来设置消息的属性
生产者代码
package com.itheima.mq.rocket.filter.sql; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; public class Producer { public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 DefaultMQProducer producer = new DefaultMQProducer("group1"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //3.启动producer producer.start(); for (int i=0; i<10; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("FilterSQLTopic","Tag2", ("hello world"+i).getBytes()); msg.putUserProperty("i",String.valueOf(i)); //5.发送消息 SendResult result = producer.send(msg); //发送状态 SendStatus sendStatus = result.getSendStatus(); //消息ID String msgId = result.getMsgId(); //消息接收队列ID int queueId = result.getMessageQueue().getQueueId(); System.out.println("发送状态:"+sendStatus+",消息ID"+msgId+",队列"+queueId+",条数"+i); TimeUnit.SECONDS.sleep(2); } //6.关闭生产者producer producer.shutdown(); } }
用MessageSelector.bySql来使用sql筛选消息
消费者代码
package com.itheima.mq.rocket.filter.sql; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.MessageSelector; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("FilterSQLTopic", MessageSelector.bySql("i>5")); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println(new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); System.out.println("消费者启动"); } }
4.6 事务消息
4.6.1 流程分析
上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
(1)事务消息发送及提交
(1) 发送消息(half消息)。 (2) 服务端响应消息写入结果。 (3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。 (4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
(2)事务补偿
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查” (2) Producer收到回查消息,检查回查消息对应的本地事务的状态 (3) 根据本地事务状态,重新Commit或者Rollback 其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
(3)事务消息状态
事务消息共有三种状态,提交状态、回滚状态、中间状态: * TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。 * TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。 * TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
4.6.2 发送事务消息
(1)创建事务性生产者
使用 TransactionMQProducer
类创建生产者,并指定唯一的 ProducerGroup
,就可以设置自定义线程池来处理这些检查请求。执行本地事务后、需要根据执行结果对消息队列进行回复。回传的事务状态在请参考前一节。
package com.itheima.mq.rocket.transaction; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.*; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.remoting.exception.RemotingException; import java.util.concurrent.TimeUnit; /** * 发送同步消息 */ public class Producer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { //1.创建消息生产者producer,并制定生产者组名 TransactionMQProducer producer = new TransactionMQProducer("group5"); //2.指定Nameserver地址 producer.setNamesrvAddr("192.168.184.208:9876;192.168.184.209:9876"); //添加事务监听器 producer.setTransactionListener(new TransactionListener() { /** * 在该方法中执行本地事务 * @param msg * @param arg * @return */ @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { if(StringUtils.equals("TAGA",msg.getTags())){ return LocalTransactionState.COMMIT_MESSAGE; }else if(StringUtils.equals("TAGB",msg.getTags())){ return LocalTransactionState.ROLLBACK_MESSAGE; }else if(StringUtils.equals("TAGC",msg.getTags())){ return LocalTransactionState.UNKNOW; } return LocalTransactionState.UNKNOW; } /** * 该方法是MQ进行消息事务状态回查 * @param msg * @return */ @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { System.out.println("消息的Tag:"+msg.getTags()); return LocalTransactionState.COMMIT_MESSAGE; } }); //3.启动producer producer.start(); String[] tag = {"TAGA", "TAGB", "TAGC"}; for (int i=0; i<3; i++){ //4.创建消息对象,指定主题Topic、Tag和消息体 /** * 参数一:消息主题Topic * 参数二:消息Tag * 参数三:消息内容 */ Message msg = new Message("TransactionTopic",tag[i], ("hello world"+i).getBytes()); //5.发送消息 SendResult result = producer.sendMessageInTransaction(msg,null); //发送状态 SendStatus sendStatus = result.getSendStatus(); //消息ID String msgId = result.getMsgId(); //消息接收队列ID int queueId = result.getMessageQueue().getQueueId(); System.out.println("发送状态:"+sendStatus+",消息ID"+msgId+",队列"+queueId+",条数"+i); TimeUnit.SECONDS.sleep(1); } //6.关闭生产者producer //producer.shutdown(); } }
(2)实现事务的监听接口
当发送半消息成功时,我们使用 executeLocalTransaction
方法来执行本地事务。它返回前一节中提到的三个事务状态之一。checkLocalTranscation
方法用于检查本地事务状态,并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。
(3)消费者代码
package com.itheima.mq.rocket.transaction; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.List; /** * 消息的接收者 */ public class Consumer { public static void main(String[] args) throws MQClientException { //1.创建消费者Consumer,制定消费者组名 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group5"); //2.指定Nameserver地址 consumer.setNamesrvAddr("192.168.184.208:9876;192.168.184.208:9876"); //3.订阅主题Topic和Tag consumer.subscribe("TransactionTopic","*"); //设定消费模式:负载均衡/广播模式 consumer.setMessageModel(MessageModel.BROADCASTING); //4.设置回调函数,处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * 接收消息内容 * @param msgs * @param consumeConcurrentlyContext * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt msg : msgs){ System.out.println(new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者consumer consumer.start(); System.out.println("消费者启动"); } }
4.6.3 使用限制
1. 事务消息不支持延时消息和批量消息。 2. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 `transactionCheckMax`参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = `transactionCheckMax` ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 `AbstractTransactionCheckListener` 类来修改这个行为。 3. 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 `transactionMsgTimeout` 参数。 4. 事务性消息可能不止一次被检查或消费。 5. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。 6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
二、案例模块
1、案例介绍
1.1 业务分析
模拟电商网站购物场景中的【下单】和【支付】业务
-
用户请求订单系统下单
-
订单系统通过RPC调用订单服务下单
-
订单服务调用优惠券服务,扣减优惠券
-
订单服务调用调用库存服务,校验并扣减库存
-
订单服务调用用户服务,扣减用户余额
-
订单服务完成确认订单
-
用户请求支付系统
-
支付系统调用第三方支付平台API进行发起支付流程
-
用户通过第三方支付平台支付成功后,第三方支付平台回调通知支付系统
-
支付系统调用订单服务修改订单状态
-
支付系统调用积分服务添加积分
-
支付系统调用日志服务记录日志
1.2 问题分析
###问题1
用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。
使用MQ保证在下单失败后系统数据的完整性
###问题2
商家支付系统如何保证在收到第三方支付平台的异步通知时,如何快速给第三方支付凭条做出回应?
通过MQ进行数据分发,提高系统处理性能
2、技术分析
2.1 技术选型
-
SpringBoot
-
Dubbo
-
Zookeeper
-
RocketMQ
-
Mysql
2.2 SpringBoot整合RocketMQ
项目
mvn install -Dmaven.skip.test=true
2.2.1 消息生产者
(1)添加依赖(pom.xml)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.itheima.shop</groupId> <artifactId>springboot-rocketmq-producer</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <properties> <rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot-starter-version> </properties> <dependencies> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>${rocketmq-spring-boot-starter-version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
(2)配置文件
# nameserver rocketmq.name-server=192.168.184.208:9876;192.168.184.209:9876 #发送者组名 rocketmq.producer.group=my-group
(3)启动类
package com.itheima.shop.springboot.rocketmq; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MQProduceApplication { public static void main(String[] args) { SpringApplication.run(MQProduceApplication.class, args); } }
(4)测试类
package com.itheima.shop.test; import com.itheima.shop.springboot.rocketmq.MQProduceApplication; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest(classes = {MQProduceApplication.class}) public class ProducerTest { @Autowired private RocketMQTemplate rocketMQTemplate; @Test public void testSendMessage(){ rocketMQTemplate.convertAndSend("springboot-rocketmq","hello springboot rocketmq"); } }
测试结果
2.2.2 消息消费者
(1)添加依赖 pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.itheima.shop</groupId> <artifactId>springboot-rocketmq-consumer</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <properties> <rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot-starter-version> </properties> <dependencies> <!--添加web依赖--> <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>${rocketmq-spring-boot-starter-version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
(2)配置文件
# nameserver rocketmq.name-server=192.168.184.208:9876;192.168.184.209:9876 #消费者组名 rocketmq.consumer.group=my-group
(3)启动类
package com.itheima.shop.springboot.rocketmq; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MQConsumerApplication { public static void main(String[] args) { SpringApplication.run(MQConsumerApplication.class, args); } }
(4)消息监听器
package com.itheima.shop.springboot.rocketmq.listener; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.stereotype.Component; @RocketMQMessageListener(topic="springboot-rocketmq" ,consumerGroup ="${rocketmq.consumer.group}" ) @Component public class Consumer implements RocketMQListener<String> { @Override public void onMessage(String s) { System.out.println("接收到消息"+s); } }
2.3 SpringBoot整合Dubbo
依赖包
将dubbo-spring-boot-starter
安装到本地仓库
mvn install -Dmaven.skip.test=true
2.3.1 搭建Zookeeper集群
(1)准备工作
(a)安装JDK
(b)建立/user/local/zookeeper-cluster,将ZooKeeper上传到服务器
(c)解压ZooKeeper,并创建data目录,将conf下的zoo_sample.cfg文件改名为zoo.cfg
tar -zxvf zookeeper-3.4.6.tar.gz
ls
cd zookeeper-3.4.6/conf
ls
mv zoo_sample.cfg zoo.cfg
ls
(4)创建三个集群文件夹,并将解压后的ZooKeeper复制到这三个目录下
cd /usr/local/zookeeper-cluster/ mkdir zookeeper-1 mkdir zookeeper-2 mkdir zookeeper-3 ls cp -rf zookeeper-3.4.6 zookeeper-1 cp -rf zookeeper-3.4.6 zookeeper-2 cp -rf zookeeper-3.4.6 zookeeper-3 cd zookeeper-1 ls
(5)配置每一个 ZooKeeper 的dataDir(zoo.cfg) clientPort 分别为 2181 2182 2183
clientPort=2181 dataDir=/usr/local/zookeeper-cluster/zookeeper-1/data
修改/usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
clientPort=2182 dataDir=/usr/local/zookeeper-cluster/zookeeper-2/data
修改/usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
clientPort=2183 dataDir=/usr/local/zookeeper-cluster/zookeeper-3/data
(2) 配置集群
(a)在每一个 zookeeper 的data 目录下创建一个myid文件,内容分别是1、2、3。这个文件就是记录每个服务器的ID
(b)在每一个 zookeeper 的zoo.cfg 配置客户端访问端口(clientPort)和集群服务器IP列表
集群服务器ip列表如下
server.1=192.168.184.205:2881:3881 server.2=192.168.184.205:2882:3882 server.3=192.168.184.205:2883:3883
(3)启动集群
启动集群就是分别启动每个实例
/usr/local/zookeeper-cluster/zookeeper-1/zookeeper-3.4.6/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-2/zookeeper-3.4.6/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-3/zookeeper-3.4.6/bin/zkServer.sh start
查看主从关系
2.3.2 RPC服务接口
public interface IUserService { public String sayHello(String name); }
2.3.3 服务提供者
(1)添加依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itheima.shop</groupId> <artifactId>springboot-dubbo-provider</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <!--dubbo--> <dependency> <groupId>com.alibaba.spring.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!--spring-boot-stater--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <artifactId>log4j-to-slf4j</artifactId> <groupId>org.apache.logging.log4j</groupId> </exclusion> </exclusions> </dependency> <!--zookeeper--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.10</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.9</version> <exclusions> <exclusion> <artifactId>slf4j-log4j12</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.itheima.shop</groupId> <artifactId>springboot-dubbo-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
(2)配置文件
# application.properties spring.application.name=dubbo-demo-provider spring.dubbo.application.id=dubbo-demo-provider spring.dubbo.application.name=dubbo-demo-provider spring.dubbo.registry.address=zookeeper://192.168.184.205:2181;zookeeper://192.168.184.205:2182;zookeeper://192.168.184.205:2183 spring.dubbo.server=true spring.dubbo.protocol.name=dubbo spring.dubbo.protocol.port=20880
(3)启动类
package com.itheima.shop.springboot.dubbo; import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableDubboConfiguration @SpringBootApplication public class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class,args); } }
(4)服务实现
package com.itheima.shop.springboot.dubbo.service.impl; import com.alibaba.dubbo.config.annotation.Service; import com.itheima.shop.service.IUserService; import org.springframework.stereotype.Component;
@Component @Service(interfaceClass = IUserService.class) public class UserServiceImpl implements IUserService { @Override public String sayHello(String name) { return "hello:"+name; } }
2.3.4 dubbo-admin 管理平台搭建
(1)上传tomcat和dubbo-admin到服务器
(2)移动tomcat安装包到 /usr/local/tomcat 下
(3)解压tomcat安装包
tar -zxvf apache-tomcat-7.0.52.tar.gz
(4)移动dubbo-admin.war包tomcat的webapps下
(5) 启动tomcat并查看日志
(6)访问dubbo-admin平台
http://192.168.184.205:8080/dubbo-admin/
用户名:root 密码:root
注意:如果你的dubbo-admin管控台不能显示服务,则如下操作
(1)dubbo.properties添加分组信息:/usr/local/src/tomcat7/webapps/dubbo-admin/WEB-INF
dubbo.registry.address=zookeeper://127.0.0.1:2181 dubbo.registry.group=dubbo dubbo.admin.root.password=root dubbo.admin.guest.password=guest
注意:一定要添加dubbo.registry.group=xxx;其中xxx就是dubbo的分组信息,也就是配置文件里面的这个name信息。
(2) dubbo-admin.xml添加分组信息:/usr/local/src/tomcat7/webapps/dubbo-admin/WEB-INF/classes/META-INF/spring
<dubbo:registry group="${dubbo.registry.group}" address="${dubbo.registry.address}" check="false" file="false" />
重新启动tomcat即可
2.3.5 服务消费者
(1)添加依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itheima.shop</groupId> <artifactId>springboot-dubbo-consumer</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--dubbo--> <dependency> <groupId>com.alibaba.spring.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <artifactId>log4j-to-slf4j</artifactId> <groupId>org.apache.logging.log4j</groupId> </exclusion> </exclusions> </dependency> <!--zookeeper--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.10</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.9</version> <exclusions> <exclusion> <artifactId>slf4j-log4j12</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.itheima.shop</groupId> <artifactId>springboot-dubbo-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
(2)配置文件
# application.properties spring.application.name=dubbo-demo-consumer spring.dubbo.application.name=dubbo-demo-consumer spring.dubbo.application.id=dubbo-demo-consumer spring.dubbo.registry.address=zookeeper://192.168.184.205:2181;zookeeper://192.168.184.205:2182;zookeeper://192.168.184.205:2183
(3)启动类
package com.itheima.shop.springboot.dubbo; import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableDubboConfiguration @SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class,args); } }
(4)Controller
package com.itheima.shop.springboot.dubbo.controller; import com.alibaba.dubbo.config.annotation.Reference; import com.itheima.shop.service.IUserService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @Reference private IUserService userService; @RequestMapping("sayHello") public String sayHello(String name) { return userService.sayHello(name); } }
3、环境搭建
3.1 数据库
(1)优惠券表
(2)商品表
(3)订单表
(4)订单商品日志表
(5)用户表
(6)用户余额日志表
(7)订单支付表
(8)MQ消息表
(9)MQ消息消费表
3.2 项目初始化
shop系统基于Maven进行项目管理
3.2.1 工程浏览
共12个系统
3.2.2 工程关系
3.3 Mybatis逆向工程使用
(1)代码生成
使用Mybatis逆向工程针对数据表生成CURD持久层代码
###2)代码导入
-
将实体类导入到shop-pojo工程
-
在服务层工程中导入对应的Mapper类和对应配置文件
3.4 公共类介绍
-
IDWorker:Twitter雪花算法
-
异常处理类
CustomerException:自定义异常类
CastException:异常抛出类
-
常量类
ShopCode:系统状态类
-
响应实体类
4、下单业务
4.1 下单基本流程
(1)接口定义
public interface IOrderService { /** * 确认订单 * @param order * @return Result */ Result confirmOrder(TradeOrder order); }
(2)业务类实现
@Slf4j @Component @Service(interfaceClass = IOrderService.class) public class OrderServiceImpl implements IOrderService { @Override public Result confirmOrder(TradeOrder order) { //1.校验订单 //2.生成预订单 try { //3.扣减库存 //4.扣减优惠券 //5.使用余额 //6.确认订单 //7.返回成功状态 } catch (Exception e) { //1.确认订单失败,发送消息 //2.返回失败状态 } } }
(3)校验订单
/** * 校验订单 * @param tradeOrder */ private void checkOrder(TradeOrder tradeOrder) { //1、校验订单是否存在 if(tradeOrder==null){ CastException.cast(ShopCode.SHOP_ORDER_INVALID); } //2、校验订单中的商品是否存在 TradeGoods tradeGoods = iGoodsService.findOne(tradeOrder.getGoodsId()); if(tradeGoods==null){ CastException.cast(ShopCode.SHOP_GOODS_NO_EXIST); } //3、校验下单用户是否存在 TradeUser tradeUser = iUserService.findOne(tradeOrder.getUserId()); if(tradeUser==null){ CastException.cast(ShopCode.SHOP_USER_NO_EXIST); } //4、校验订单金额是否合法 if(tradeOrder.getGoodsPrice().compareTo(tradeGoods.getGoodsPrice())!=0){ CastException.cast(ShopCode.SHOP_GOODS_PRICE_INVALID); } //5、校验订单商品数量是否合法 if(tradeOrder.getGoodsNumber() >= tradeGoods.getGoodsNumber()){ CastException.cast(ShopCode.SHOP_GOODS_NUM_NOT_ENOUGH); } log.info("校验订单通过"); }
(4) 生成预订单
/** * 生成预订单 * @param tradeOrder */ private Long savePreOrder(TradeOrder tradeOrder) { //1、设置订单状态为不可见 tradeOrder.setOrderStatus(ShopCode.SHOP_ORDER_NO_CONFIRM.getCode()); //2、设置订单ID long orderId = idWorker.nextId(); tradeOrder.setOrderId(orderId); //3、核算订单运费 BigDecimal shippingFee = calculateShippingFee(tradeOrder.getOrderAmount()); if(tradeOrder.getShippingFee().compareTo(shippingFee)!=0){ CastException.cast(ShopCode.SHOP_ORDER_SHIPPINGFEE_INVALID); } //4、核算订单总价格是否正确 BigDecimal orderAmount = tradeOrder.getGoodsPrice().multiply(new BigDecimal(tradeOrder.getGoodsNumber())); if(tradeOrder.getOrderAmount().compareTo(orderAmount)!=0){ CastException.cast(ShopCode.SHOP_ORDERAMOUNT_INVALID); } //5、判断用户是否使用余额 BigDecimal moneyPaid = tradeOrder.getMoneyPaid(); if(moneyPaid != null){ //5.1 订单中余额是否合法 int r = moneyPaid.compareTo(BigDecimal.ZERO); //余额小于0 if(r==-1){ CastException.cast(ShopCode.SHOP_MONEY_PAID_LESS_ZERO); } //余额大于0 if(r==1){ TradeUser user = iUserService.findOne(tradeOrder.getUserId()); if (user == null) { CastException.cast(ShopCode.SHOP_USER_NO_EXIST); } //比较余额是否大于用户账户余额 if(moneyPaid.compareTo(new BigDecimal(user.getUserMoney()))==1){ CastException.cast(ShopCode.SHOP_MONEY_PAID_INVALID); } } tradeOrder.setMoneyPaid(tradeOrder.getMoneyPaid()); }else{ tradeOrder.setMoneyPaid(BigDecimal.ZERO); } //6、判断用户是否使用优惠券 Long couponId = tradeOrder.getCouponId(); if(couponId!=null){ //6.1 判断优惠券是否存在 TradeCoupon tradeCoupon = iCouponService.findOne(couponId); if(tradeCoupon==null){ CastException.cast(ShopCode.SHOP_COUPON_NO_EXIST); } //6.2 判断优惠券是否已经使用 if(tradeCoupon.getIsUsed().intValue()==ShopCode.SHOP_COUPON_ISUSED.getCode().intValue()){ CastException.cast(ShopCode.SHOP_COUPON_ISUSED); } tradeOrder.setCouponPaid(tradeCoupon.getCouponPrice()); }else{ tradeOrder.setCouponPaid(BigDecimal.ZERO); } //7、核算订单支付金额 订单总金额-余额-优惠券金额 BigDecimal payAmount = tradeOrder.getOrderAmount().subtract(tradeOrder.getMoneyPaid()).subtract(tradeOrder.getCouponPaid()); tradeOrder.setPayAmount(payAmount); //8、设置下单时间 tradeOrder.setAddTime(new Date()); //9、保存订单到数据库 int r = tradeOrderMapper.insert(tradeOrder); if(ShopCode.SHOP_SUCCESS.getCode() != r) { CastException.cast(ShopCode.SHOP_ORDER_SAVE_ERROR); } log.info("订单:["+tradeOrder.getOrderId()+"]预订单生成成功"); return tradeOrder.getOrderId(); } /** * 核算运费 * @param orderAmount * @return */ private BigDecimal calculateShippingFee(BigDecimal orderAmount) { if(orderAmount.compareTo(new BigDecimal(100))==1){ return BigDecimal.ZERO; }else{ return new BigDecimal(10); } }
(5)扣减库存
/** * 扣减库存 * @param tradeOrder */ private void reduceGoodsNum(TradeOrder tradeOrder) { //订单ID、商品ID、商品数量 TradeGoodsNumberLog tradeGoodsNumberLog = new TradeGoodsNumberLog(); tradeGoodsNumberLog.setOrderId(tradeOrder.getOrderId()); tradeGoodsNumberLog.setGoodsId(tradeOrder.getGoodsId()); tradeGoodsNumberLog.setGoodsNumber(tradeOrder.getGoodsNumber()); Result result = iGoodsService.reduceGoodsNum(tradeGoodsNumberLog); if(result.getSuccess().equals(ShopCode.SHOP_FAIL.getSuccess())){ CastException.cast(ShopCode.SHOP_REDUCE_GOODS_NUM_FAIL); } log.info("订单:"+tradeOrder.getOrderId()+"扣减库存成功"); }
@Override public Result reduceGoodsNum(TradeGoodsNumberLog goodsNumberLog) { if (goodsNumberLog == null || goodsNumberLog.getGoodsNumber() == null || goodsNumberLog.getOrderId() == null || goodsNumberLog.getGoodsNumber() == null || goodsNumberLog.getGoodsNumber().intValue() <= 0) { CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID); } TradeGoods goods = goodsMapper.selectByPrimaryKey(goodsNumberLog.getGoodsId()); if(goods.getGoodsNumber()<goodsNumberLog.getGoodsNumber()){ //库存不足 CastException.cast(ShopCode.SHOP_GOODS_NUM_NOT_ENOUGH); } //减库存 goods.setGoodsNumber(goods.getGoodsNumber()-goodsNumberLog.getGoodsNumber()); goodsMapper.updateByPrimaryKey(goods); //记录库存操作日志 goodsNumberLog.setGoodsNumber(-(goodsNumberLog.getGoodsNumber())); goodsNumberLog.setLogTime(new Date()); goodsNumberLogMapper.insert(goodsNumberLog); return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage()); }
(6)扣减优惠券
/** * 使用优惠券 * @param tradeOrder */ private void updateCouponStatus(TradeOrder tradeOrder) { if(tradeOrder.getCouponId()!=null){ TradeCoupon tradeCoupon = iCouponService.findOne(tradeOrder.getCouponId()); tradeCoupon.setOrderId(tradeOrder.getOrderId()); tradeCoupon.setUserId(tradeOrder.getUserId()); tradeCoupon.setIsUsed(ShopCode.SHOP_COUPON_ISUSED.getCode()); tradeCoupon.setUsedTime(new Date()); //更新优惠券状态 Result result = iCouponService.updateCouponStatus(tradeCoupon); if(result.getSuccess().equals(ShopCode.SHOP_FAIL.getSuccess())){ CastException.cast(ShopCode.SHOP_COUPON_USE_FAIL); } log.info("订单:"+tradeOrder.getOrderId()+"使用优惠券成功"); } }
/** * 更新优惠券状态 * @param tradeCoupon * @return */ @Override public Result updateCouponStatus(TradeCoupon tradeCoupon) { if(tradeCoupon==null || tradeCoupon.getCouponId()==null){ CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID); } tradeCouponMapper.updateByPrimaryKey(tradeCoupon); return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage()); }
(7)扣减用户余额
/** * 扣减余额 * @param tradeOrder */ private void reduceMoneyPaid(TradeOrder tradeOrder) { if(tradeOrder.getMoneyPaid()!=null && tradeOrder.getMoneyPaid().compareTo(BigDecimal.ZERO)==1) { TradeUserMoneyLog tradeUserMoneyLog = new TradeUserMoneyLog(); tradeUserMoneyLog.setOrderId(tradeOrder.getOrderId()); tradeUserMoneyLog.setUserId(tradeOrder.getUserId()); tradeUserMoneyLog.setUseMoney(tradeOrder.getMoneyPaid()); tradeUserMoneyLog.setMoneyLogType(ShopCode.SHOP_USER_MONEY_PAID.getCode()); Result result = iUserService.updateMoneyPaid(tradeUserMoneyLog); if (result.getSuccess().equals(ShopCode.SHOP_FAIL.getSuccess())) { CastException.cast(ShopCode.SHOP_COUPON_USE_FAIL); } log.info("订单:[" + tradeOrder.getOrderId() + "扣减余额[" + tradeOrder.getMoneyPaid() + "元]成功]"); } }
/** * 扣减余额 * @param tradeUserMoneyLog * @return */ @Override public Result updateMoneyPaid(TradeUserMoneyLog tradeUserMoneyLog) { //1、校验参数是否合法 if (tradeUserMoneyLog == null || tradeUserMoneyLog.getUserId() == null || tradeUserMoneyLog.getUseMoney() == null || tradeUserMoneyLog.getOrderId() == null || tradeUserMoneyLog.getUseMoney().compareTo(BigDecimal.ZERO) <= 0) { CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID); } //2、查询订单余额使用日志 TradeUserMoneyLogExample tradeUserMoneyLogExample = new TradeUserMoneyLogExample(); tradeUserMoneyLogExample.createCriteria() .andUserIdEqualTo(tradeUserMoneyLog.getUserId()) .andOrderIdEqualTo(tradeUserMoneyLog.getOrderId()); int r = tradeUserMoneyLogMapper.countByExample(tradeUserMoneyLogExample); TradeUser tradeUser = tradeUserMapper.selectByPrimaryKey(tradeUserMoneyLog.getUserId()); //判断余额操作行为 //3、扣减余额 if(tradeUserMoneyLog.getMoneyLogType().intValue()== ShopCode.SHOP_USER_MONEY_PAID.getCode().intValue()){ if(r>0){ //已经付款 CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY); } //减余额 log.info("用户余额:"+tradeUser.getUserMoney()); log.info("订单金额:"+tradeUserMoneyLog.getUseMoney()); tradeUser.setUserMoney(new BigDecimal(tradeUser.getUserMoney()).subtract(tradeUserMoneyLog.getUseMoney()).longValue()); tradeUserMapper.updateByPrimaryKey(tradeUser); } //4、回退余额 if(tradeUserMoneyLog.getMoneyLogType().intValue()== ShopCode.SHOP_USER_MONEY_REFUND.getCode().intValue()){ //如果订单未付款,则不能退款,抛异常 if (r == 0) { CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY); } //防止多次退款 tradeUserMoneyLogExample = new TradeUserMoneyLogExample(); tradeUserMoneyLogExample.createCriteria() .andUserIdEqualTo(tradeUserMoneyLog.getUserId()) .andOrderIdEqualTo(tradeUserMoneyLog.getOrderId()) .andMoneyLogTypeEqualTo(ShopCode.SHOP_USER_MONEY_REFUND.getCode()); int r2 = tradeUserMoneyLogMapper.countByExample(tradeUserMoneyLogExample); if (r2 > 0) { CastException.cast(ShopCode.SHOP_USER_MONEY_REFUND_ALREADY); } //退款 tradeUser.setUserMoney(new BigDecimal(tradeUser.getUserMoney()).add(tradeUserMoneyLog.getUseMoney()).longValue()); tradeUserMapper.updateByPrimaryKey(tradeUser); } //5、记录订单余额使用日志 //记录用户使用余额日志 tradeUserMoneyLog.setCreateTime(new Date()); tradeUserMoneyLogMapper.insert(tradeUserMoneyLog); return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage()); }
(8)确认订单
/** * 确认订单 * @param tradeOrder */ private void updateOrderStatus(TradeOrder tradeOrder) { tradeOrder.setOrderStatus(ShopCode.SHOP_ORDER_CONFIRM.getCode()); tradeOrder.setPayStatus(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY.getCode()); tradeOrder.setConfirmTime(new Date()); int r = tradeOrderMapper.updateByPrimaryKey(tradeOrder); if (r <= 0) { CastException.cast(ShopCode.SHOP_ORDER_CONFIRM_FAIL); } log.info("订单:["+tradeOrder.getOrderId()+"]状态修改成功"); }
(9)小结
@Override public Result confirmOrder(TradeOrder order) { //1.校验订单 checkOrder(order); //2.生成预订单 Long orderId = savePreOrder(order); order.setOrderId(orderId); try { //3.扣减库存 reduceGoodsNum(order); //4.扣减优惠券 updateCoponStatus(order); //5.使用余额 reduceMoneyPaid(order); //6.确认订单 updateOrderStatus(order); log.info("订单:["+orderId+"]确认成功"); return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage()); } catch (Exception e) { //确认订单失败,发送消息 ... return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage()); } }
4.2 失败补偿机制
4.2.1 消息发送方
# 下单失败消息发送组 rocketmq.name-server=192.168.184.208:9876;192.168.184.209:9876 rocketmq.producer.group=orderProducerGroup mq.order.consumer.group.name=order_orderTopic_cancel_group mq.order.topic=orderTopic mq.order.tag.cancel=order_cancel
@Autowired RocketMQTemplate rocketMQTemplate; @Value("${mq.order.topic}") private String topic; @Value("${mq.order.tag.cancel}") private String tag;
@Override public Result ConfirmOrder(TradeOrder tradeOrder) { //1.校验订单 checkOrder(tradeOrder); //2.生成预订单 savePreOrder(tradeOrder); try { //3.扣减库存 reduceGoodsNum(tradeOrder); //4.扣减优惠券 updateCouponStatus(tradeOrder); //5.使用余额 reduceMoneyPaid(tradeOrder); //模拟异常抛出 CastException.cast(ShopCode.SHOP_FAIL); //6.确认订单 updateOrderStatus(tradeOrder); //7.返回成功状态 return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage()); } catch (Exception e) { //1.确认订单失败,发送消息 //订单ID、优惠券ID、用户ID、余额、商品ID、商品数量 MQEntity mqEntity = new MQEntity(); mqEntity.setOrderId(tradeOrder.getOrderId()); mqEntity.setUserId(tradeOrder.getUserId()); mqEntity.setUserMoney(tradeOrder.getMoneyPaid()); mqEntity.setGoodsId(tradeOrder.getGoodsId()); mqEntity.setGoodsNum(tradeOrder.getGoodsNumber()); mqEntity.setCouponId(tradeOrder.getCouponId()); //2.返回订单确认失败消息 try { sendCancelOrder(topic, tag, tradeOrder.getOrderId().toString(), JSON.toJSONString(mqEntity)); } catch (Exception e1) { e1.printStackTrace(); } return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage()); } }
/** * 发送订单确认失败消息 * @param topic * @param tag * @param keys * @param body */ private void sendCancelOrder(String topic, String tag, String keys, String body) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { //判断Topic是否为空 if (StringUtils.isEmpty(topic)) { CastException.cast(ShopCode.SHOP_MQ_TOPIC_IS_EMPTY); } //判断消息内容是否为空 if (StringUtils.isEmpty(body)) { CastException.cast(ShopCode.SHOP_MQ_MESSAGE_BODY_IS_EMPTY); } //消息体 Message message = new Message(topic, tag, keys, body.getBytes()); //发送消息 rocketMQTemplate.getProducer().send(message); }
4.2.2 消息接收方
# RocketMQ rocketmq.name-server=192.168.184.208:9876;192.168.184.209:9876 rocketmq.producer.group=orderProducerGroup mq.order.consumer.group.name=order_orderTopic_cancel_group mq.order.topic=orderTopic
package com.itheima.shop.mq; import org.apache.rocketmq.client.consumer.listener.MessageListener; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.MessageModel; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import sun.plugin2.message.Message; @Component @RocketMQMessageListener(topic = "${mq.order.topic}", consumerGroup = "${mq.order.consumer.group.name}", messageModel = MessageModel.BROADCASTING) public class CancelMQListener implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt messageExt) { } }
(1)回退库存
package com.itheima.shop.mq; import com.alibaba.fastjson.JSON; import com.itheima.constant.ShopCode; import com.itheima.entity.MQEntity; import com.itheima.shop.mapper.TradeGoodsMapper; import com.itheima.shop.mapper.TradeGoodsNumberLogMapper; import com.itheima.shop.mapper.TradeMqConsumerLogMapper; import com.itheima.shop.pojo.*; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.MessageModel; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; @Slf4j @Component @RocketMQMessageListener(topic = "${mq.order.topic}",consumerGroup = "${mq.order.consumer.group.name}",messageModel = MessageModel.BROADCASTING ) public class CancelMQListener implements RocketMQListener<MessageExt>{ @Value("${mq.order.consumer.group.name}") private String groupName; @Autowired private TradeGoodsMapper goodsMapper; @Autowired private TradeMqConsumerLogMapper mqConsumerLogMapper; @Autowired private TradeGoodsNumberLogMapper goodsNumberLogMapper; @Override public void onMessage(MessageExt messageExt) { String msgId=null; String tags=null; String keys=null; String body=null; try { //1. 解析消息内容 msgId = messageExt.getMsgId(); tags= messageExt.getTags(); keys= messageExt.getKeys(); body= new String(messageExt.getBody(),"UTF-8"); log.info("接受消息成功"); //2. 查询消息消费记录 TradeMqConsumerLogKey primaryKey = new TradeMqConsumerLogKey(); primaryKey.setMsgTag(tags); primaryKey.setMsgKey(keys); primaryKey.setGroupName(groupName); TradeMqConsumerLog mqConsumerLog = mqConsumerLogMapper.selectByPrimaryKey(primaryKey); if(mqConsumerLog!=null){ //3. 判断如果消费过... //3.1 获得消息处理状态 Integer status = mqConsumerLog.getConsumerStatus(); //处理过...返回 if(ShopCode.SHOP_MQ_MESSAGE_STATUS_SUCCESS.getCode().intValue()==status.intValue()){ log.info("消息:"+msgId+",已经处理过"); return; } //正在处理...返回 if(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode().intValue()==status.intValue()){ log.info("消息:"+msgId+",正在处理"); return; } //处理失败 if(ShopCode.SHOP_MQ_MESSAGE_STATUS_FAIL.getCode().intValue()==status.intValue()){ //获得消息处理次数 Integer times = mqConsumerLog.getConsumerTimes(); if(times>3){ log.info("消息:"+msgId+",消息处理超过3次,不能再进行处理了"); return; } mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode()); //使用数据库乐观锁更新 TradeMqConsumerLogExample example = new TradeMqConsumerLogExample(); TradeMqConsumerLogExample.Criteria criteria = example.createCriteria(); criteria.andMsgTagEqualTo(mqConsumerLog.getMsgTag()); criteria.andMsgKeyEqualTo(mqConsumerLog.getMsgKey()); criteria.andGroupNameEqualTo(groupName); criteria.andConsumerTimesEqualTo(mqConsumerLog.getConsumerTimes()); int r = mqConsumerLogMapper.updateByExampleSelective(mqConsumerLog, example); if(r<=0){ //未修改成功,其他线程并发修改 log.info("并发修改,稍后处理"); } } }else{ //4. 判断如果没有消费过... mqConsumerLog = new TradeMqConsumerLog(); mqConsumerLog.setMsgTag(tags); mqConsumerLog.setMsgKey(keys); mqConsumerLog.setGroupName(groupName); mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode()); mqConsumerLog.setMsgBody(body); mqConsumerLog.setMsgId(msgId); mqConsumerLog.setConsumerTimes(0); //将消息处理信息添加到数据库 mqConsumerLogMapper.insert(mqConsumerLog); } //5. 回退库存 MQEntity mqEntity = JSON.parseObject(body, MQEntity.class); Long goodsId = mqEntity.getGoodsId(); TradeGoods goods = goodsMapper.selectByPrimaryKey(goodsId); goods.setGoodsNumber(goods.getGoodsNumber()+mqEntity.getGoodsNum()); goodsMapper.updateByPrimaryKey(goods); //6. 将消息的处理状态改为成功 mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_SUCCESS.getCode()); mqConsumerLog.setConsumerTimestamp(new Date()); mqConsumerLogMapper.updateByPrimaryKey(mqConsumerLog); log.info("回退库存成功"); } catch (Exception e) { e.printStackTrace(); TradeMqConsumerLogKey primaryKey = new TradeMqConsumerLogKey(); primaryKey.setMsgTag(tags); primaryKey.setMsgKey(keys); primaryKey.setGroupName(groupName); TradeMqConsumerLog mqConsumerLog = mqConsumerLogMapper.selectByPrimaryKey(primaryKey); if(mqConsumerLog==null){ //数据库未有记录 mqConsumerLog = new TradeMqConsumerLog(); mqConsumerLog.setMsgTag(tags); mqConsumerLog.setMsgKey(keys); mqConsumerLog.setGroupName(groupName); mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_FAIL.getCode()); mqConsumerLog.setMsgBody(body); mqConsumerLog.setMsgId(msgId); mqConsumerLog.setConsumerTimes(1); mqConsumerLogMapper.insert(mqConsumerLog); }else{ mqConsumerLog.setConsumerTimes(mqConsumerLog.getConsumerTimes()+1); mqConsumerLogMapper.updateByPrimaryKeySelective(mqConsumerLog); } } } }
(2)回退优惠券
package com.itheima.shop.mq; import com.alibaba.fastjson.JSON; import com.itheima.constant.ShopCode; import com.itheima.entity.MQEntity; import com.itheima.shop.mapper.TradeCouponMapper; import com.itheima.shop.pojo.TradeCoupon; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.client.consumer.listener.MessageListener; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.MessageModel; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import sun.plugin2.message.Message; import java.io.UnsupportedEncodingException; @Slf4j @Component @RocketMQMessageListener(topic = "${mq.order.topic}", consumerGroup = "${mq.order.consumer.group.name}", messageModel = MessageModel.BROADCASTING) public class CancelMQListener implements RocketMQListener<MessageExt> { @Autowired TradeCouponMapper tradeCouponMapper; @Override public void onMessage(MessageExt messageExt) { //1、解析消息内容 try { String body = new String(messageExt.getBody(), "UTF-8"); MQEntity mqEntity = JSON.parseObject(body, MQEntity.class); log.info("接收到消息"); if(mqEntity.getCouponId()!=null){ //2、查询优惠券信息 TradeCoupon tradeCoupon = tradeCouponMapper.selectByPrimaryKey(mqEntity.getCouponId()); //3、更改优惠券状态 tradeCoupon.setUsedTime(null); tradeCoupon.setIsUsed(ShopCode.SHOP_COUPON_UNUSED.getCode()); tradeCoupon.setOrderId(null); tradeCouponMapper.updateByPrimaryKey(tradeCoupon); log.info("回退优惠券成功"); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); log.error("回退优惠券失败"); } } }
(3)回退余额
package com.itheima.shop.service.impl; import com.alibaba.dubbo.config.annotation.Service; import com.itheima.api.IUserService; import com.itheima.constant.ShopCode; import com.itheima.entity.Result; import com.itheima.exception.CastException; import com.itheima.shop.mapper.TradeUserMapper; import com.itheima.shop.mapper.TradeUserMoneyLogMapper; import com.itheima.shop.pojo.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.xml.bind.SchemaOutputResolver; import java.math.BigDecimal; import java.util.Date; @Slf4j @Component @Service(interfaceClass = IUserService.class) public class UserServiceImpl implements IUserService { @Autowired TradeUserMapper tradeUserMapper; @Autowired TradeUserMoneyLogMapper tradeUserMoneyLogMapper; @Override public TradeUser findOne(Long userId) { if(userId==null){ CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID); } return tradeUserMapper.selectByPrimaryKey(userId); } /** * 扣减余额 * @param tradeUserMoneyLog * @return */ @Override public Result updateMoneyPaid(TradeUserMoneyLog tradeUserMoneyLog) { //1、校验参数是否合法 if (tradeUserMoneyLog == null || tradeUserMoneyLog.getUserId() == null || tradeUserMoneyLog.getUseMoney() == null || tradeUserMoneyLog.getOrderId() == null || tradeUserMoneyLog.getUseMoney().compareTo(BigDecimal.ZERO) <= 0) { CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID); } //2、查询订单余额使用日志 TradeUserMoneyLogExample tradeUserMoneyLogExample = new TradeUserMoneyLogExample(); tradeUserMoneyLogExample.createCriteria() .andUserIdEqualTo(tradeUserMoneyLog.getUserId()) .andOrderIdEqualTo(tradeUserMoneyLog.getOrderId()); int r = tradeUserMoneyLogMapper.countByExample(tradeUserMoneyLogExample); TradeUser tradeUser = tradeUserMapper.selectByPrimaryKey(tradeUserMoneyLog.getUserId()); //判断余额操作行为 //3、扣减余额 if(tradeUserMoneyLog.getMoneyLogType().intValue()== ShopCode.SHOP_USER_MONEY_PAID.getCode().intValue()){ if(r>0){ //已经付款 CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY); } //减余额 log.info("用户余额:"+tradeUser.getUserMoney()); log.info("订单金额:"+tradeUserMoneyLog.getUseMoney()); tradeUser.setUserMoney(new BigDecimal(tradeUser.getUserMoney()).subtract(tradeUserMoneyLog.getUseMoney()).longValue()); tradeUserMapper.updateByPrimaryKey(tradeUser); } //4、回退余额 if(tradeUserMoneyLog.getMoneyLogType().intValue()== ShopCode.SHOP_USER_MONEY_REFUND.getCode().intValue()){ //如果订单未付款,则不能退款,抛异常 if (r == 0) { CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY); } //防止多次退款 tradeUserMoneyLogExample = new TradeUserMoneyLogExample(); tradeUserMoneyLogExample.createCriteria() .andUserIdEqualTo(tradeUserMoneyLog.getUserId()) .andOrderIdEqualTo(tradeUserMoneyLog.getOrderId()) .andMoneyLogTypeEqualTo(ShopCode.SHOP_USER_MONEY_REFUND.getCode()); int r2 = tradeUserMoneyLogMapper.countByExample(tradeUserMoneyLogExample); if (r2 > 0) { CastException.cast(ShopCode.SHOP_USER_MONEY_REFUND_ALREADY); } //退款 tradeUser.setUserMoney(new BigDecimal(tradeUser.getUserMoney()).add(tradeUserMoneyLog.getUseMoney()).longValue()); tradeUserMapper.updateByPrimaryKey(tradeUser); } //5、记录订单余额使用日志 //记录用户使用余额日志 tradeUserMoneyLog.setCreateTime(new Date()); tradeUserMoneyLogMapper.insert(tradeUserMoneyLog); return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage()); } }
(4)取消订单
package com.itheima.shop.mq; import com.alibaba.fastjson.JSON; import com.itheima.constant.ShopCode; import com.itheima.entity.MQEntity; import com.itheima.shop.mapper.TradeOrderMapper; import com.itheima.shop.pojo.TradeOrder; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.MessageModel; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; @Slf4j @Component @RocketMQMessageListener(topic = "${mq.order.topic}", consumerGroup = "${mq.order.consumer.group.name}", messageModel = MessageModel.BROADCASTING) public class CancelMQListener implements RocketMQListener<MessageExt> { @Autowired TradeOrderMapper tradeOrderMapper; @Override public void onMessage(MessageExt messageExt) { try { //1、解析消息内容 String body = new String(messageExt.getBody(), "UTF-8"); MQEntity mqEntity = JSON.parseObject(body, MQEntity.class); log.info("接收消息成功"); //2、查询订单 TradeOrder tradeOrder = tradeOrderMapper.selectByPrimaryKey(mqEntity.getOrderId()); //3、更新订单状态为取消 tradeOrder.setOrderStatus(ShopCode.SHOP_ORDER_CANCEL.getCode()); tradeOrderMapper.updateByPrimaryKey(tradeOrder); log.info("订单状态设置为取消"); log.info("CancelOrderProcessor receive message:"+messageExt); } catch (UnsupportedEncodingException e) { e.printStackTrace(); log.info("订单取消失败"); } } }
4.3 测试
(1)准备测试环境
@RunWith(SpringRunner.class) @SpringBootTest(classes = ShopOrderServiceApplication.class) public class OrderTest { @Autowired private IOrderService orderService; }
-
用户数据
-
商品数据
-
优惠券数据
###2)测试下单成功流程
@Test public void add(){ Long goodsId=XXXL; Long userId=XXXL; Long couponId=XXXL; TradeOrder order = new TradeOrder(); order.setGoodsId(goodsId); order.setUserId(userId); order.setGoodsNumber(1); order.setAddress("北京"); order.setGoodsPrice(new BigDecimal("5000")); order.setOrderAmount(new BigDecimal("5000")); order.setMoneyPaid(new BigDecimal("100")); order.setCouponId(couponId); order.setShippingFee(new BigDecimal(0)); orderService.confirmOrder(order); }
###3)测试下单失败流程
代码同上。
执行完毕后,查看用户的余额、优惠券数据是否发生更改,订单的状态是否为取消。
5、支付业务
5.1 创建支付订单
package com.itheima.shop.service.impl; import com.alibaba.dubbo.config.annotation.Service; import com.itheima.api.IPayService; import com.itheima.constant.ShopCode; import com.itheima.entity.Result; import com.itheima.exception.CastException; import com.itheima.shop.mapper.TradePayMapper; import com.itheima.shop.pojo.TradePay; import com.itheima.shop.pojo.TradePayExample; import com.itheima.utils.IDWorker; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Slf4j @Component @Service(interfaceClass = IPayService.class) public class PayServiceImpl implements IPayService { @Autowired TradePayMapper tradePayMapper; @Autowired IDWorker idWorker; /** * 支付订单 * @param tradePay */ @Override public Result createPayment(TradePay tradePay) { if(tradePay==null || tradePay.getOrderId()==null){ CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID); } //1、判断订单支付状态 TradePayExample payExample = new TradePayExample(); TradePayExample.Criteria criteria = payExample.createCriteria(); criteria.andOrderIdEqualTo(tradePay.getOrderId()); criteria.andIsPaidEqualTo(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode()); int count = tradePayMapper.countByExample(payExample); if (count > 0) { CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY); } //2、设置订单的状态未支付 tradePay.setIsPaid(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY.getCode()); //3、保存支付订单 long payId = idWorker.nextId(); tradePay.setPayId(payId); tradePayMapper.insert(tradePay); log.info("创建支付订单成功:" + payId); return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage()); } }
5.2 支付回调
/** * 支付回调 * @param tradePay * @return */ @Override public Result callbackPayment(TradePay tradePay) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { log.info("支付回调"); //1、判断用户支付状态 if (tradePay.getIsPaid().equals(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode())) { //2、更新支付订单状态为已支付 Long payId = tradePay.getPayId(); TradePay pay = tradePayMapper.selectByPrimaryKey(payId); //判断支付订单是否存在 if(pay==null){ CastException.cast(ShopCode.SHOP_PAYMENT_NOT_FOUND); } pay.setIsPaid(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode()); int r = tradePayMapper.updateByPrimaryKeySelective(pay); log.info("支付订单状态改为已支付"); if(r==1){ //3、创建支付成功的消息 TradeMqProducerTemp tradeMqProducerTemp = new TradeMqProducerTemp(); tradeMqProducerTemp.setId(String.valueOf(idWorker.nextId())); tradeMqProducerTemp.setGroupName(groupName); tradeMqProducerTemp.setMsgTopic(topic); tradeMqProducerTemp.setMsgTag(tag); tradeMqProducerTemp.setMsgKey(String.valueOf(tradePay.getPayId())); tradeMqProducerTemp.setMsgBody(JSON.toJSONString(tradePay)); tradeMqProducerTemp.setCreateTime(new Date()); //4、将消息持久化数据库 tradeMqProducerTempMapper.insert(tradeMqProducerTemp); log.info("将支付成功消息持久化到数据库"); //5、发送消息到MQ SendResult result = sendMessage(topic, tag, String.valueOf(tradePay.getPayId()), JSON.toJSONString(tradePay)); if(SendStatus.SEND_OK.equals(result.getSendStatus())){ log.info("消息发送成功"); //6、等待发送结果,如果MQ接受到消息,删除发送成功的消息 tradeMqProducerTempMapper.deleteByPrimaryKey(tradeMqProducerTemp.getId()); log.info("持久化到数据库的消息删除"); } } return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage()); }else{ CastException.cast(ShopCode.SHOP_PAYMENT_PAY_ERROR); return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage()); } } /** * 发送支付成功下消息 * @param topic * @param tag * @param key * @param body */ private SendResult sendMessage(String topic, String tag, String key, String body) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { if(StringUtils.isEmpty(topic)){ CastException.cast(ShopCode.SHOP_MQ_TOPIC_IS_EMPTY); } if(StringUtils.isEmpty(body)){ CastException.cast(ShopCode.SHOP_MQ_MESSAGE_BODY_IS_EMPTY); } Message message = new Message(topic, tag, key, body.getBytes()); SendResult sendResult = rocketMQTemplate.getProducer().send(message); return sendResult; }
线程池优化消息发送逻辑
@Bean public ThreadPoolTaskExecutor getThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(100); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("Pool-A"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }
@Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; //在线程池中进行处理 threadPoolTaskExecutor.submit(new Runnable() { @Override public void run() { //5、发送消息到MQ SendResult result = null; try { result = sendMessage(topic, tag, String.valueOf(tradePay.getPayId()), JSON.toJSONString(tradePay)); } catch (Exception e) { e.printStackTrace(); } if(SendStatus.SEND_OK.equals(result.getSendStatus())){ log.info("消息发送成功"); //6、等待发送结果,如果MQ接受到消息,删除发送成功的消息 tradeMqProducerTempMapper.deleteByPrimaryKey(tradeMqProducerTemp.getId()); log.info("持久化到数据库的消息删除"); } } });
5.2.3 处理消息
支付成功后,支付服务payService发送MQ消息,订单服务、用户服务、日志服务需要订阅消息进行处理
-
订单服务修改订单状态为已支付
-
日志服务记录支付日志
-
用户服务负责给用户增加积分
以下用订单服务为例说明消息的处理情况
(1)配置RocketMQ属性值
mq.pay.topic=payTopic
mq.pay.consumer.group.name=pay_payTopic_group
(2)消费消息
package com.itheima.shop.mq; import com.alibaba.fastjson.JSON; import com.itheima.constant.ShopCode; import com.itheima.shop.mapper.TradeOrderMapper; import com.itheima.shop.pojo.TradeOrder; import com.itheima.shop.pojo.TradePay; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.MessageModel; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; @Slf4j @Component @RocketMQMessageListener(topic = "${mq.pay.topic}", consumerGroup = "${mq.pay.consumer.group.name}", messageModel = MessageModel.BROADCASTING) public class PaymentListener implements RocketMQListener<MessageExt> { @Autowired private TradeOrderMapper tradeOrderMapper; @Override public void onMessage(MessageExt messageExt) { log.info("接收到支付成功消息"); try { //1、解析消息内容 String body = new String(messageExt.getBody(), "utf-8"); TradePay tradePay = JSON.parseObject(body, TradePay.class); //2、根据订单ID查询订单对象 TradeOrder tradeOrder = tradeOrderMapper.selectByPrimaryKey(tradePay.getOrderId()); //3、更改订单支付状态为已支付 tradeOrder.setPayStatus(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode()); //4、更新订单数据到数据库 tradeOrderMapper.updateByPrimaryKey(tradeOrder); log.info("更改订单支付状态为已支付"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); log.error("订单支付失败"); } } }
1、高级功能
1.1 消息存储
分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。
-
-
MQ收到消息,将消息进行持久化,在存储中新增一条记录
-
返回ACK给生产者
-
MQ push 消息给对应的消费者,然后等待消费者返回ACK
-
如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功,在存储中删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会尝试重新push消息,重复执行4、5、6步骤
-
1.1.1 存储介质
1.1.2 性能对比
文件系统>关系型数据库DB
1.1.3 消息的存储和发送
(1)消息存储
磁盘如果使用得当,磁盘的速度完全可以匹配上网络 的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍!因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。RocketMQ的消息用顺序写,保证了消息存储的速度。
(2)消息发送
Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态的切换,免不了进行数据复制。
一台服务器 把本机磁盘文件的内容发送到客户端,一般分为两个步骤:
1)read;读取本地文件内容;
2)write;将读取的内容通过网络发送出去。
这两个看似简单的操作,实际进行了4 次数据复制,分别是:
从磁盘复制数据到内核态内存;
从内核态内存复 制到用户态内存;
然后从用户态 内存复制到网络驱动的内核态内存;
最后是从网络驱动的内核态内存复 制到网卡中进行传输。
通过使用mmap的方式,可以省去向用户态的内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的
RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了
1.1.4 消息存储结构
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。
- CommitLog:存储消息的元数据
- ConsumerQueue:存储消息在CommitLog的索引
- IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程
1.1.5 刷盘机制
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复, 又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时 候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
(1)同步刷盘
在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写 成功的状态。
(2)异步刷盘
在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
(3)配置
同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType 参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一个。
1.2 高可用性机制
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。
Master和Slave的区别:在Broker的配置文件中,参数 brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是 Slave,同时brokerRole参数也会说明这个Broker是Master还是Slave。
Master角色的Broker支持读和写,Slave角色的Broker仅支持读,也就是 Producer只能和Master角色的Broker连接写入消息;Consumer可以连接 Master角色的Broker,也可以连接Slave角色的Broker来读取消息。
1.2.1 消息消费高可用
在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave 读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。这就达到了消费端的高可用性。
1.2.2 消息发送高可用
在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同Broker名称,不同 brokerId的机器组成一个Broker组),这样当一个Broker组的Master不可 用后,其他组的Master仍然可用,Producer仍然可以发送消息。 RocketMQ目前还不支持把Slave自动转成Master,如果机器资源不足, 需要把Slave转成Master,则要手动停止Slave角色的Broker,更改配置文 件,用新的配置文件启动Broker。
1.2.3 消息主从复制
如果一个Broker组有Master和Slave,消息需要从Master复制到Slave 上,有同步和异步两种复制方式。
(1)同步复制
同步复制方式是等Master和Slave均写 成功后才反馈给客户端写成功状态;
在同步复制方式下,如果Master出故障, Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入 延迟,降低系统吞吐量。
(2)异步复制
异步复制方式是只要Master写成功 即可反馈给客户端写成功状态。
在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写 入Slave,有可能会丢失
(3)配置
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三个值中的一个
(4)总结
异步刷盘保证消息的吞吐量
同步主从复制保证消息不丢失
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式, 尤其是SYNC_FLUSH方式,由于频繁地触发磁盘写动作,会明显降低 性能。通常情况下,应该把Master和Save配置成ASYNC_FLUSH的刷盘 方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台 机器出故障,仍然能保证数据不丢,是个不错的选择。
1.3 负载均衡
1.3.1 Producer负载均衡
Producer端,每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就发送到不同的broker下,如下图:
图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推。
1.3.2 Consumer负载均衡
(1)集群模式
在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
默认的分配算法是AllocateMessageQueueAveragely,如下图:
还有另外一种平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分摊每一条queue,只是以环状轮流分queue的形式,如下图:
需要注意的是,集群模式下,queue都是只允许分配只一个实例,这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个queue只分给一个consumer实例,一个consumer实例可以允许同时分到不同的queue。
通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。
但是如果consumer实例的数量比message queue的总数量还多的话,多出来的consumer实例将无法分到queue,也就无法消费到消息,也就无法起到分摊负载的作用了。所以需要控制让queue的总数量大于等于consumer的数量。
(2)广播模式
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。
在实现上,其中一个不同就是在consumer分配queue的时候,所有consumer都分到所有的queue。
1.4 消息重试
1.4.1 顺序消息的重试
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
1.4.2 无序消息的重试
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。
如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。
注意:
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):
-
返回 Action.ReconsumeLater (推荐)
-
返回 Null
-
抛出异常
public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { //处理消息 doConsumeMessage(message); //方式1:返回 Action.ReconsumeLater,消息将重试 return Action.ReconsumeLater; //方式2:返回 null,消息将重试 return null; //方式3:直接抛出异常, 消息将重试 throw new RuntimeException("Consumer Message exceotion"); } }
消费失败后,不重试配置方式
public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { try { doConsumeMessage(message); } catch (Throwable e) { //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; return Action.CommitMessage; } //消息处理正常,直接返回 Action.CommitMessage; return Action.CommitMessage; } }
自定义消息最大重试次数
-
最大重试次数小于等于 16 次,则重试时间间隔同上表描述。
-
最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。
Properties properties = new Properties(); //配置对应 Group ID 的最大消息重试次数为 20 次 properties.put(PropertyKeyConst.MaxReconsumeTimes,"20"); Consumer consumer =ONSFactory.createConsumer(properties);
注意:
-
消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。
-
如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
-
配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置
消费者收到消息后,可按照如下方式获取消息的重试次数:
public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { //获取消息的重试次数 System.out.println(message.getReconsumeTimes()); return Action.CommitMessage; } }
1.5 死信队列
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
1.5.1 死信特性
死信消息具有以下特性
-
-
有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。
死信队列具有以下特性:
-
一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
-
如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
-
一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
1.5.2 查看死信信息
一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。
1.6 消息幂等
消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。
1.6.1 消息幂等的必要性
在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况:
-
发送时消息重复
-
投递时消息重复
消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
-
负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启)
当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。
1.6.2 处理方式
因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据。 最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置:
Message message = new Message(); message.setKey("ORDERID_100"); SendResult sendResult = producer.send(message);
订阅方收到消息时可以根据消息的 Key 进行幂等处理:
consumer.subscribe("ons_test", "*", new MessageListener() { public Action consume(Message message, ConsumeContext context) { String key = message.getKey() // 根据业务唯一标识的 key 做幂等处理 } });