一 业务:电商 - 订单系统
二 需求
- 订单系统中
- 用户只需要知道:下订单、下单成功、用户支付、支付成功
- 用户不需要知道:下订单成功后的那些步骤:
- 创建订单
- 扣减库存
- 加积分
- 派发优惠卷
- 同步的通信方式:性能不够高(累加12s)、成功率低(中间如果有某个步骤没成功就全回滚)、低吞吐量(同步)
- 异步的通信方式:性能好、成功率高、高吞吐量
三 解决方案
1 功能“集”:异步提交(消息队列)
2 “神”冰箱:RocketMQ
四 核心理论
1 思想、思路
- MQ是消息队列,从字面上理解呢,它就是一个队列。
- MQ是用来存放消息的。
2 流原
-
异步通信:通过消息队列,屏蔽底层的通信协议,使得解藕和并⾏消费得以实现。
-
非rocketmq(同步通信):
-
rocketmq(异步通信):
-
3 实现技术
- 队列
五 “完成”视频
1 rocketmq基本知识
(1)rocketmq vs activeme vs kafaka
(2)rocketmq技术架构
(3)rocketmq部署架构
2 快速开始
- 第一步:下载rocketmq
- 运行版(安装包)
- 源码(分析)。使用idea打开即可。
- 第二步:安装rocketmq
- 第1步:linux虚拟机。Ubuntu16.04版本。
- 第2步:安装jdk
- 第a步:上传jdk-8u191安装包
- 第b步:解压缩在 /usr/local/java ⽬录下
- 第c步:删除安装包
- 第3步:安装rocketmq
-
第a步:上传rocketmq安装包
-
第b步:使⽤unzip命令解压缩 在 /usr/local/rocketmq ⽬录下。
-
bin目录:linux版本与windows版本的命令
-
conf
-
rocketmq给我们配置集群的3种方式:
-
2m-2s-async:2个master,2个slave,异步刷盘
-
broker-a.properties:broker-a主节点
-
broker-a-s.properties:broker-a从节点
-
broker-b.properties:broker-b主节点
-
broker-b-s.properties:broker-b从节点
-
-
2m-2s-sync:2个master,2个slave,同步刷盘
-
2m-noslave:2个master,没有slave
-
-
broker.conf:如果我们使用单节点(rocketmq),那我们可以用配置文件broker.conf进行配置。
-
dledger:dledger集群配置方式,为了解决上面3种默认的配置方式不能高可用的问题(dledger自动完成,主节点master挂掉以后,不会通过计算让其中的一台slave变成master)。dledger是在rocketmq 4.5版本以后集成进来的。dledger其实也是一个第三方的插件,并不是rocketmq它自己推出的。
-
logXxx.xml:日志相关的一些东西。
-
-
-
第c步:删除安装包
-
-
第4步:配置jdk和rocketmq的环境变量:export JAVA_HOME = /usr/local/java/jdk1.8.0_191export JRE_HOME = /usr/local/java/jdk1.8.0_191/jreexport ROCKETMQ_HOME = /usr/local/rocketmq/rocketmq-all-4.7.1-bin-releaseexport CLASSPATH = $CLASSPATH : $JAVA_HOME /lib: $JAVA_HOME /jre/libexportPATH = $JAVA_HOME /bin: $JAVA_HOME /jre/bin: $ROCKETMQ_HOME /bin: $PATH : $HOME /bin注意,RocketMQ的环境变量⽤来加载 ROCKETMQ_HOME/conf 下的配置⽂件,如果不配置则⽆法启动NameServer和Broker。
-
第5步:让环境变量生效: source /etc/profile
-
第6步:验证
-
java、javac、java -version
-
-
第7步:rocketmq内存占用
-
nameServer调整:修改bin/runserver.sh⽂件,由于RocketMQ默认设置的JVM内存为4G,但虚拟机 ⼀般没有这么4G内存,因此调整为512mb:
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g - XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
在runserver.sh⽂件中找到上⾯这段内容,改为下⾯的参数:JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m - XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
- broker调整:修改bin/runbroker.sh⽂件,同样修改为512m:
-
-
第三步:启动
-
第1步:启动nameServer
-
后台启动:nohup ./mqnamesrv -n 10.203.5.185:9876 &
-
-n 10.203.5.185:9876:指定nameServer的ip和端口
-
-
验证:cat nohup.out
-
The Name Server boot success
-
-
-
第2步:启动broker
- 第a步:修改broker的JVM参数配置,将默认8G内存修改为512m。修
改 bin/runbroker.sh ⽂件:
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m" 1
-
第b步: 在 conf/broker.conf ⽂件中加⼊如下配置,开启⾃动创建 Topic 功能( ps:如果我们没有创建topic的话,在启动broker时会自动帮我们创建一个topic。便于我们往这个默认topic发送消息,和接取消息进行消费 ):
autoCreateTopicEnable=true
-
第c步:来到bin目录下, 以静默⽅式启动 broker(后台启动):
#-n localhost:9876:指定nameServer的ip和端口,ip建议使用真实的ip nohup ./mqbroker -n localhost:9876 &
-
第d步: 查看 bin/nohup.out ⽇志,显示如下内容表示启动成功:
#当前的broker是在本机(本台虚拟机VMware被安排的ip:172.17.0.1),本机的10911端口上面,启动成功。 #当前的broker会把信息注册到名称为“192.168.32.133:9876”的nameServer上面(之前取名) The broker[localhost.localdomain, 192.168.32.133:10911] boot success. serializeType=JSON and name server is 192.168.32.133:9876
- 第a步:修改broker的JVM参数配置,将默认8G内存修改为512m。修
-
第3步:创建topic
-
第4步:启动生产者
-
第5步:启动消费者
-
-
第四步:验证: 使⽤发送和接收消息验证MQ
-
第1步:生产者发送消息成功
- 第a步:vim /etc/profile,配置nameserver的环境变量,。ps:在发送和接收消息的程序所在的服务器上,需要有一个环境变量export NAMESRV_ADDR=localhost:9876。这个环境变量就是nameServer的地址,建议把localhost改成服务器的真实ip。
在发送 / 接收消息之前,需要告诉客户端 nameserver 的位置。配置环境变量NAMESRV_ADDR :
export NAMESRV_ADDR=localhost:9876 source /etc/profile
- 第b步:使⽤bin/tools.sh⼯具验证消息的发送,默认会发1000条消息:
#bin目录下有一个工具tools.sh #使用工具时带上参数:org.apache.rocketmq.example.quickstart.Consumer #总结:下面这句话相当于运行rocketmq提供的一个生产者的程序,发送1000条消息。 # 而生产者要发现broker,所以上面我们配置了环境变量:export NAMESRV_ADDR=localhost:9876, # 即连接指定注册中心nameServer的名称(ip+端口) ./tools.sh org.apache.rocketmq.example.quickstart.Producer
- 第c步:查看发送的消息⽇志:sendStatus=SEND_OK
... SendResult [sendStatus=SEND_OK, msgId=FD154BA55A2B1008020C29FFFED6A0855CFC12A3A380885CB70A0235, offsetMsgId=AC11000100002A9F000000000001F491, messageQueue=MessageQueue [topic=TopicTest, brokerName=ubuntu, queueId=0], queueOffset=141]
- 第a步:vim /etc/profile,配置nameserver的环境变量,。ps:在发送和接收消息的程序所在的服务器上,需要有一个环境变量export NAMESRV_ADDR=localhost:9876。这个环境变量就是nameServer的地址,建议把localhost改成服务器的真实ip。
-
第2步:消费者消费消息成功
- 第a步:使⽤bin/tools.sh⼯具验证消息的接收:
#同理tools.sh工具启动一个消费者程序 ./tools.sh org.apache.rocketmq.example.quickstart.Consumer
- 第b步:看到接收到的消息:toString()=Message{topic='TopicTest'
... ConsumeMessageThread_12 Receive New Messages: [MessageExt [brokerName=ubuntu, queueId=1, storeSize=227, queueOffset=245, sysFlag=0, bornTimestamp=1658892578234, bornHost=/172.16.253.100:48524, storeTimestamp=1658892578235, storeHost=/172.17.0.1:10911, msgId=AC11000100002A9F0000000000036654, commitLogOffset=222804, bodyCRC=683694034, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties= {MIN_OFFSET=0, MAX_OFFSET=250, CONSUME_START_TIME=1658892813497, UNIQ_KEY=FD154BA55A2B1008020C29FFFED6A0855CFC12A3A380885CB9BA03D6, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 32, 57, 56, 50], transactionId='null'}]]
- 第a步:使⽤bin/tools.sh⼯具验证消息的接收:
-
- 第五步:关闭
- 第1步:关闭broker:./mqshutdown broker
- 第2步:关闭nameServer:./mqshutdown namesrv
3 搭建rocketmq集群搭建:如3个无状态的nameServer、2个M、2个S
- 官方提供的集群方式:
-
2 主 2 从异步通信⽅式
-
ps优点:不同的消费组的2个消费者,去消费同一个topic。那么可以让消费者1去消费M中的此主题的消费,并行地可以让费者2去消费S中的此主题的消费,提升效率。
-
ps在M和S中进行的同步复制:等待S把数据复制(同步)好了以后,生产者才能发送另一条消息到M。
-
ps在M和S中进行的异步复制:生产者可以一直发消息到M,而M与S之间的消息复制异步并行着。
-
缺点:可能会丢消息。 ps:因为异步在M和S之间进行消息复制,如果某条消息复制(同步数据)失败,那么S中就会丢消息,并且生产都不知道(所以不知道去生发)。
-
-
2 主 2 从同步通信⽅式
-
优点:(阻塞)保证消息安全投递,不会丢失
- 缺点:影响吞吐量
-
- 2主⽆从⽅式(不建议)
-
Dledger ⾼可⽤集群
-
S自动选举为M
-
-
-
搭建2主2从异步通信⽅式
-
第一步:准备三台Linux服务器,三台服务器都需要安装jdk和rocketmq:
-
第二步:启动三台nameserver。
-
修改3台nameserver内存占用参数。
-
-
第三步:配置broker
-
第1步:服务器1,不用动,因为没有broker
-
第2步:服务器2,conf/2m-2s-async文件夹
-
第a步:第a步:broker-a.properties,部署M(broker-a)
-
brokerId=0:只有brokerId=1时,才会参与到负载均衡的读。
-
namesrvAddr=...:配置名称服务器的地址列表。
-
defaultTopicQueueNums=4:ps:自动创建topic以后,此topic会有4个队列(mq),即此topic分成了4块区域,相当于kafka中创建此topic的4个分区。当生产者发送消息到此topic时,会发送到其中的某个分区中。
-
listenPort=10911:ps:此端口需要跟同一服务器上的另一个broker区分开来。
-
Xxx路径:ps:注意,同一服务器上的两个broker的路径都不能一样。
-
-
第b步:broker-b-s.properties,部署S(broker-b-s)
-
-
第3步:服务器3,conf/2m-2s-async文件夹
- 第a步:broker-a-s.properties,部署S(broker-a-s)
- brokerClusterName=DefaultCluster:同主节点一样
- brokerName=broker-a:同主节点一样
- 第b步:broker-b.properties,部署M(broker-b)
- 第a步:broker-a-s.properties,部署S(broker-a-s)
-
第4步:修改2台broker内存占用参数。
-
- 第四步:启动broker。-c 参数指定使用哪个配置文件。
- 第1步:在服务器2中启动broker-a(master)和broker-b-s(slave):
- nohup ./mqbroker -c ../conf/2m-2s-async/broker-a.properties &
nohup ./mqbroker -c ../conf/2m-2s-async/broker-b-s.properties &
- nohup ./mqbroker -c ../conf/2m-2s-async/broker-a.properties &
- 第2步:在服务器3中启动broker-b(master),broker-a-s(slave):
-
nohup ./mqbroker -c ../conf/2m-2s-async/broker-b.properties &
nohup ./mqbroker -c ../conf/2m-2s-async/broker-a-s.properties &
-
- 第3步:验证
- 第a步:各个服务器分别,cat nohup.out
- 第b步:各个服务器分别,jps查看当前服务器有哪些java程序:
- 第1步:在服务器2中启动broker-a(master)和broker-b-s(slave):
- 第五步:验证集群:使⽤RocketMQ提供的tools⼯具验证集群是否正常⼯作
- 第1步:在服务器2上配置环境变量,⽤于被tools中的⽣产者和消费者程序读取该变量。
-
exportNAMESRV_ADDR = '172.16.253.103:9876;172.16.253.101:9876;172.16.253.102:9876'
-
作用:启动生产者和消费者时,生产者和消费者需要读取的nameServer(名称空间)的地址
-
-
- 第2步:启动生产者
-
./tools.sh org.apache.rocketmq.example.quickstart.Producer
-
日志:消息成功发送
-
- 第3步:启动消费者
-
./tools.sh org.apache.rocketmq.example.quickstart.Consumer
-
日志:消息成功消费
-
- 第1步:在服务器2上配置环境变量,⽤于被tools中的⽣产者和消费者程序读取该变量。
-
-
mqadmin管理工具
-
/bin/mqadmin
-
rocketmq提供的命令工具。
-
通过命令,可以管理topic、broker、集群、消息等。
-
CRUD操作
-
- 示例1:./mqadmin clusterList
- 示例2:./mqadmin updateTopic -n 172.16.253.101:9876 -c DefaultCluster -t myTopic1,创建topic
- -n 172.16.253.101:9876:nameserve 服务地址列表。
- -c DefaultCluster:此topic会往创建到这个broker集群上面。
-
-
安装可视化管理控制平台
-
可以安装在服务器1上
-
第一步:下载第三方工具, https://github.com/apache/rocketmq-externals
-
第二步:mkdir /usr/local/rocketMQExternals、rz、uzip、cd
-
控制台rocketmq-console需要重点关注,其它的我们不关心
-
从目录结构看rocketmq-console其实就是一个java的springboot工程,所以我们需要对它进行编译、打包、运行。
-
- 第三步:给服务器安装maven环境:apt install maven
- 第四步:修改 rocketmq-externals/rocketmq-externals-master/rocketmq-console/src/main/resources/application.properties 配置文件中nameserver地址:
-
rocketmq.config.namesrvAddr = 172.16.253.103 : 9876;172.16.253.101 : 9876;172.16.253.102 : 9876 :即nameServer服务列表
-
- 第五步:回到 rocketmq-externals/rocketmq-externals-master/rocketmq-console 路径下执行maven命令进行打包:
-
mvn clean package -Dmaven .test .skip = true
-
- 第六步:运行jar包。进入到 rocketmq-externals/rocketmq-externals-master/rocketmq-console/target 目录内执行如下命令:
-
nohup java - jar rocketmq - console - ng - 1.0 . 1. jar
-
- 第七步:访问所在服务器的8080端口,查看集群界面,可以看到之前部署的集群:
-
- 注:也可以在idea中用git下载,然后用idea直接运行。
-
六 管控台操作指南
3 集群
- 主从的身份
4 主题
七 官配应用
1 消息示例
(1)构建Java基础环境
- 业务:java程序完成rocketmq消息的收发。
- 思路:
- 生产者、消费者在同一个springboot程序中。
- java程序编写的消息生产者,通过rocketmq提供的生产者相关的API把消息发送到rocketmq服务器上。
- java程序编写的消息消费者,通过rocketmq提供的消费者相关的API消费(订阅)rocketmq服务器上的消息。
- 生产者发消息
- 第一步:创建springboot项目
- 第二步:引入依赖
- 第三步:编写生产者
package com.qf.rocketmq; 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.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.nio.charset.StandardCharsets; public class BaseProducer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { // 第一步:创建一个默认的生产者 /** * 参数1:producerGroup * 生产者也是有组的概念,producerGroup参数指定的就是生产者所属的组 */ DefaultMQProducer producer = new DefaultMQProducer("my-producer-group-1"); // 第二步:指定nameServer的地址 /** * 生产者往群集中的某个broker上的某个topic发消息,因此生产者要连接nameServer并从它上面订阅此broker的信息。 * 比如要把消息发送到topic1,那么生产者就要到nameServer上订阅topic1所在的broker的信息(地址)。 * 注:这里只需要指明nameServer的地址就可以了,不用关心broker在哪里,因为nameServer会告诉生产者broker的地址 */ producer.setNamesrvAddr("192.168.32.133:9876"); // 第三步:启动生产者 producer.start(); // 第四步:创建消息 /** * 描述:Message是rocketmq中描述消息的类 * 参数1:String topic,消息要往哪个topic上发送。消息会自动发送到此topic对应的broker上面去的。 * 参数2:String tags,消息的tags,作用是用来过滤消息的。 * 参数3:String keys,消息的keys。 * 参数4:byte[] body,消息的内容,是一个byte数组。 */ for(int i=0;i<10;i++){ Message message = new Message("MyTopic1","TagA",("hello rocketmq"+i).getBytes(StandardCharsets.UTF_8)); // 第五步:发送消息 SendResult sendResult = producer.send(message); System.out.println("发送消息的反馈:"+sendResult); } // 第六步:关闭生产者 producer.shutdown(); } }
- 第四步:关闭防火墙:stop firewalld.service
- 第五步:运行生产者的main()
- 第六步:日志打印:
- sendStatus=SEND_OK:表示消息发送成功。
- msgId=24098A5C06693970CC6F54F2449774A3424C18B4AAC2388D52520002:rocketmq给我们创建的此条消息的唯一id。
- offsetMsgId=C0A8208500002A9F00000000000327:消息所在的队列的偏移量。
- 消息所在哪个队列呢?这个队列上messageQueue=MessageQueue [topic=MyTopic1, brokerName=localhost.localdomain, queueId=2], queueOffset=2]
- brokerName=localhost.localdomain:哪个broker上?
- topic=MyTopic1:broker上的哪个主题上的队列?
- queueId=2:此队列的id是2
- 第七步:看rocketmq管控台
- 消费者
- 第一步:创建springboot项目
- 第二步:引入依赖
- 第三步:编写生产者:
package com.qf.rocketmq; 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 BaseConsumer { public static void main(String[] args) throws MQClientException { // 第一步:创建默认的消息拉取对象DefaultMQPushConsumer(主动摘取模式) 或 DefaultMQPullConsumer /** * 参数1:consumerGroup,此消费者所属的消费组 */ DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); // 第二步:指定nameServer的地址。因为消息者也需要去nameServer中获取它所订阅的topic所在的broker的信息。 consumer.setNamesrvAddr("10.203.5.185:9876"); // 第三步:消费者订阅主题 /** * 参数2:subExpression,这是一个子表达式,这个表达式是用来过滤消息的。 * 因为前面生产者也有过滤消息的标识,我们写的是tagA,意思就是说可以还有tagB、tagC等等。 * 如果此表达式我们写tagA,那么此消费者只要消费这个主题上的tagA的消息,tagB的消息消费者是消费不到的。 * *,表示不过滤,所有的消息都拉取。 */ consumer.subscribe("MyTopic1", "*"); // 第四步:创建一个监听器,当broker把消息推过来时调用 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override /** * 参数1:List<MessageExt> msgs,broker推送过来的消息都在这个集合里。 * 参数2:ConsumeConcurrentlyContext context,是一个上下文对象 */ public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg:msgs){ System.out.println("消费的消息:"+new String(msg.getBody())); } //告知broker消费是成功的 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // 第五步:启动消费者 consumer.start(); System.out.printf("Consumer Started.%n"); } }
- 第四步:关闭防火墙:stop firewalld.service
- 第五步:运行消费的main()
- 第六步:日志打印:
- 如上图所示:
- 消息会来自不同的broker(如brokerName=broker-a 或 broker-b 或broker-c),然后broker后面还有一个queueId=0/1/2/3。brokerName=broker-a后面为queueId=0/1/2/3,说明broker-a带着4个队列。同理,brokerName=broker-a后面也带着4个队列。
- 如上图所示:
- 第七步:看rocketmq管控台
(2)生产者发送:同步消息、异步消息、单向消息
- 生产者发送,同步消息:
- ps:生产者发送消息后,阻塞,一定要得到broker(集群)的反馈。得到反馈以后,生产者才会走之后的逻辑(如再发一条消息,又如执行业务处理逻辑)。
- ps:SendResult sendResult = producer.send(msg);
- 生产者发送,异步消息
- ps:生产者发送消息后,不阻塞,不需要等待直至得到broker(集群)的反馈。生产者可以继续走其它的逻辑(如再发一条消息,又如执行业务处理逻辑)。broker(集群)的反馈【回调函数】,broker会异步回调。
- ps:CountDownLatch在等着broker,CountDownLatch.await()方法必须等到CountDownLatch内部维护的总量的值变成0。变成0的时候,CountDownLatch才会跳出阻塞状态,继续往下一句代码(producer.shutdown();)执行。这样有助于生产者在发送异步消息时,等到所有的反馈,才执行producer.shutdown();结束程序。
- ps:缺点:可能会丢失。
- 生产者发送,单向消息
- ps:最简单的方式。单身消息,不需要得到broker(集群)的反馈。即生产者只要把消息发出去,就可以做后面的业务逻辑了,broker(集群)没有任何反馈。
- ps:缺点:容易丢失消息。
(3)消费者的顺序消息消费
- 顺序消息:ps:生产者发送消息的顺序与消费者消费消息的顺序是一致的。
- 局部顺序消息示例:横向上顺序,纵向上乱序
- 生产者向指定队列发送消息编码:
- 在idea中配置启动的环境变量://名字服务器的地址已经在环境变量中配置好了: NAMESRV_ ADDR=172. 16.253.101:9876
- 编码:
- ps:参数2:new MessageQueueSeLector(){...},这个参数的作用就是在做选择。为什么要做选择呢?因为某个broker上的某个topic默认是带着4个queue。选择什么呢?那肯定是选择某一个queue队列了。
- 局部顺序消息消费编码:
- 编码:new MessageListenerOrderly()局部顺序消费的消息的监听器,实现局部顺序消息的消费。
- 局部顺序消息消费原理图:
- 生产者向指定队列发送消息编码:
- 全局顺序消息示例:横向和纵向上都是顺序的
- 生产者发送消息:同上
- 消费者全局顺序消费消息
- 思路:只有一个broker(群集中的所有broker合成一个broker),topic只对应一个queue(topic的4个queue合成一个queue)。
- 原理图:
- 缺点:性能损耗(broker没有集群性能损耗。queue(相当于kafka中的分区partition)只有一个那么就没有并行存储和消费消息的效果了,也是性能损耗)。
- 局部顺序消息示例:横向上顺序,纵向上乱序
- 乱序消息
-
消费者消费消息不需要关注消息的顺序。
-
消费者使⽤MessageListenerConcurrently 类做消息监听。
-
(4)消费者广播消息消费:生产者发出的一条消息会被多个消费者拉取消费
- 生产者:
- 消费者广播消息消费:
- 编码:
-
consumer . setConsumeFromWhere ( ConsumeFromWhere . CONSUME_FROM_FIRST_OFFSET ); :让消费者从第1个offset就开始消费。
-
consumer . setMessageModel ( MessageModel . BROADCASTING ); :消费者把自己的消息消费模式设置也了广播的模式。 【其实就是这一句代码而已】
-
-
运行:
-
在idea中配置启动的环境变量://名字服务器的地址已经在环境变量中配置好了: NAMESRV_ ADDR=172. 16.253.101:9876
-
- 编码:
(5)消费者延迟消息消费
- 定义:
- 延迟投递:生产者在一定的时间间隔以后,才会把消息发送到broker。
- 对比:
- 在rabbitmq中,要实现延迟队列的效果必须要借助执行队列才能实现,而且实现的过程还较为麻烦。
- 在rocketmq中,它已经帮我们封装了延迟队列的功能,让我们可以快速实现延迟消息。
- 示例:
- 生产者发送消息时设置延迟等级:
- 在idea中配置启动的环境变量://名字服务器的地址已经在环境变量中配置好了: NAMESRV_ ADDR=172. 16.253.101:9876
-
message . setDelayTimeLevel (3);:设置了延迟时间的等级3,表示这条消息会被延迟10秒钟的时间再发送到broker。
-
18个等级
-
- 消费者延迟消息消费:
- 原理:系统为这18个等级配置了18个topic,⽤于实现延迟队列的效果。
- 比如说在生产者中设置的延迟等级是3(10秒),那么生产者在推送消息时就会把消息推送到延迟等级为3的topic上。那么这条消息就会被消费者延迟10秒地消费,在这个地方大家要注意,延迟效果是发生在消费者端的。而注意,对于生产者端来说,生产者并不会延迟发送消费。即延迟指的是延迟(x秒后去)消费,并非延迟发送。
- 管控台查看那系统topic和它带的18个系统queue:主题->(勾选)系统->(列表)SCHEDULE_TOPIC_ XXXX->状态->18个queue
- 原理:系统为这18个等级配置了18个topic,⽤于实现延迟队列的效果。
- 生产者发送消息时设置延迟等级:
(6)生产者批量消息发送
- 需求:生产者减少与rocketmq服务器的连接(IO通信)次数 。如果消息数量大,那么可以把一批消息在一次与rocketmq服务器的连接全部发出去。
- 分类
- 普通批量消息(官网1m,实际4m):producer.send(List<Message>),
- 大容量批量消息(可超过4m):ListSplitter.java类。原理:分割大批量消息,让每个小批量都不超过4m,每次发送一个小批量。
(7)消费过滤消息消费
- tag过滤:tag标记
- ⽣产者发送消息时打卡tag标记
- String[] tags = new String[] {"TagA", "TagB", "TagC"};:3个tag标记
-
for ( int i = 0 ; i < 15 ; i ++ ) {Message msg = new Message ( "TagFilterTest" , tags [ i % tags . length ], "Helloworld" . getBytes ( RemotingHelper . DEFAULT_CHARSET ));} :15条消息通过取模,分别对应3个tag标记。
- 消费者消费消息时使用tag过滤消息
- consumer.subscribe("TagFilterTest", "TagA || TagC");:只有消息上打上的是TagA 或 TagC,此消费者才会去消费此消息。
- 运行:
- 在idea中配置启动的环境变量://名字服务器的地址已经在环境变量中配置好了: NAMESRV_ ADDR=172. 16.253.101:9876
- 原理:
- 是谁来做过滤这个动作?是在broker端执行的过滤,还是在消费者端执行的过滤?实际上是broker在做,这样性能会更好。broker只会把TagA 和 TagC的消息发送给消费者,TagB的消息不会发送给消费者。
- 缺点:
- 消费者将收到包含 TAGA 或 TAGB 或 TAGC 的消息。但是限制是⼀条消息只能有⼀个 标签,这可能不适⽤于复杂的场景。在这种情况下,您可以使⽤ SQL 表达式来过滤掉消息。
- ⽣产者发送消息时打卡tag标记
- sql过滤:使用sql表达式来过滤
- 生产者
- msg.putUserProperty("a", String.valueOf(i));:存放用户属性,key和value都可以任意的。
- 消费者
- consumer.subscribe("SqlFilterTest", MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB')) and (a is not null and a between 0 and 3)"));
- 注意:
- 搭建broker集群的时候,别忘了加上配置项目:// Don't forget to set enablePropertyFilter=true in broker。
- 生产者
(8)生产者事务消息发送
-
应用场景:订单系统。
-
下订单。本地事务。
-
支付了才会去做,创建订单、加积分、减库存、发货。
- 如下图:
-
-
概念:
-
事务消息以被认为是⼀个两阶段的提交消息实现,以确保分布式系统的最终⼀致性。
- 两阶段提交逻辑
- 第1阶段:中间状态就会让人联想到2阶段提交的第1个阶段。
- 第2阶段:对于中间状态的消息呢,rocketmq会有什么动作去处理呢?回查。
- 两阶段提交逻辑
-
事务性消息确保本地事务的执⾏和消息的发送可以原⼦地执⾏。 ps:也就是说,把本地事务的执行和消息的发送捆绑在一起,要么本地事务的执行和消息的发送都成功,反之本地事务的执行和消息的发送都回滚。也就是说,如果本地事务执行成功了,那么消息的发送就会成功。如果本地事务发送失败了,那么生产者的消息也会发送失败。
-
本地事务的原子性执行
-
消息发送的原子性执行
-
-
-
示例:
-
本地事务处理
package com.qf.rocketmq.transaction; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; public class TransactionListenerImpl implements TransactionListener { /** * 描述:此方法用于执行本地事务 */ @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { String tags = msg.getTags(); if(StringUtils.contains(tags,"TagA")){ // 如果发送的消息是TagA标记的,那么就把本地事务(这条消息的事务)的状态设置成提交,消费者就可以看到了。 return LocalTransactionState.COMMIT_MESSAGE; }else if(StringUtils.contains(tags,"TagB")){ // 如果发送的消息是TagB标记的,那么就把本地事务(这条消息的事务)的状态设置成回滚,消费者就拉取不到这条消息的。 return LocalTransactionState.ROLLBACK_MESSAGE; }else{ // 如果发送的消息是TagC、TagD、TagE标记的,那么就把本地事务(这条消息的事务)的状态设置成中间状态(unknow)。中间状态就表明,不知道这条消息最后是提交还是回滚。 return LocalTransactionState.UNKNOW; } } /** * 描述:检查本地事务。 * 通过调用这个方法,broker会去回查事务状态为nuknow的那些消息(MessageExt msg)。 * 如果消息标识为TagC,就提交。 * 如果消息标识为TagD或TagE,就回滚。 */ @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { String tags = msg.getTags(); if(StringUtils.contains(tags,"TagC")){ return LocalTransactionState.COMMIT_MESSAGE; }else if(StringUtils.contains(tags,"TagD")){ return LocalTransactionState.ROLLBACK_MESSAGE; }else{ return LocalTransactionState.UNKNOW; } }
-
生产者 (ps:整个事务消息重点是在生产者这边)
-
TransactionListener transactionListener = new TransactionListenerImpl();: ps:是一个事务监听器对象。事务监听器到底是干什么的呢?看工作流程图。
-
producer . setTransactionListener ( transactionListener ); :ps:生产者把事务监听器给放进来了。
-
producer.sendMessageInTransaction(msg, null); :ps:说明生产者发出的消息是一个事务消息。此时,生产者对这条消息的发送就会和transactionListener事务监听器进行配合,怎么配合呢?看工作流程图。
-
-
消费者 (ps:关于消费者的事务呢,rocketmq的事务消息是没办法去保证的,或者说并没有牵扯到或涵盖到消费者事务)
-
效果:消费者先收到2条TagA的消息,过了一段时间再收到一条TagC的消息,又过了一段时间又收到另一条TagC的消息。为什么会出现这种情况呢?提交、回滚、回查。
-
-
流原(事务消息流程图):
- 概念:
- 生产者本地事务:指的就是我们定义的TransactionListener transactionListener = new TransactionListenerImpl();事务监听器对象。
- 步骤:
- 第一步:生产者发送事务消息:producer.sendMessageInTransaction(msg, null)。此时,实际上,生产者是发一条half消息到broker,half消息它是一条半消息。
- 半消息,可以理解为这条消息不是一条完整的消息。也可以这样理解:两阶段提交,两阶段提交把一条消息分成了两部分。
- 第二步:broker(集群)回复生产者half消息。表示broker没有问题。
- 第三步:生产者执行本地事务:TransactionListenerImpl.executeLocalTransaction()
- ps:本地事务:在生产者本地执行的一段事务逻辑(代码)。
- 第四步:生产者返回本地事务状态给生产者。
- 第五步:生产者把本地事务的状态提交给broker。
- 第1步:如果是提交状态,那么Broker就把消息发送给消费者。
- 第2步:如果是回滚状态,此条消息会被broker丢掉。
- 第3步:如果是中间状态(unknow),继承执行回查逻辑判断,才能最终确定状态。
- 第六步:对消息进行回查处理。
- 限制15次。丢掉消息。
- 第一步:生产者发送事务消息:producer.sendMessageInTransaction(msg, null)。此时,实际上,生产者是发一条half消息到broker,half消息它是一条半消息。
- 概念:
- half消息怎么去实现?
(9)消费者的实现细节(源码)(消费者是怎么去获取消息的?)
- 消费者的推模式:broker主动地把消息推送给消费者。参考rabbitmq。
- 消费者的拉模式:消费者主动去broker拉取消息。参考kafka。过时。
- 应用场景:回溯消费。
- 消费者的轻拉模式
- 应用场景:回溯消费。
(10)生产者负载均衡的细节(源码)
- 问题:
- 主题MyTopic在broker-a和broker-b都会有,所以对于主题MyTopic来说,会有带着8个队列(在broker-a的主题MyTopic带着4个队列,在broker-b的主题MyTopic带着4个队列)。那问题是,当生产者发送一条消息到主题MyTopic时,此条消息会保存到哪个队列中去呢?
- 生产者负载均衡实现细节:
- 达到轮询效果:
2 springboot整合rocketmq
- springboot整合rocketmq提供的便利操作
- RocketMQTemplate便利
- 消息的发送
- RocketMQTemplate便利
- 简单消息 —— 步骤
- 第一步:生产者编码
- 第一步:新建springboot工程
- 第二步:引入依赖
- 注意:依赖的版本要和rocketmq服务器的版本一致
- 第三步:编写配置文件
- 第四步:生产者编码
- 第1步:通过注解获取spring容器中的RocketMQTemplate
- 第五步:控制台打印 vs 管控台
- 第二步:消费者编码
- 第一步:新建springboot工程
- 第二步:引入依赖
- 注意:依赖的版本要和rocketmq服务器的版本一致
- 第三步:编写配置文件
- 第四步:消费者编码
- 第1步:@RocketMQMessageListener进行消息过滤、超时时间......配置。
- 第三步:启动生产者使用单元测试发送消息
- 第四步:启动消费者消费消息。控制台打印 vs 管控台。
- 第一步:生产者编码
- 生产者事务消息发送 —— 步骤
- 事务消息:会根据某些条件是否成立,完成本地事务的提交与回滚。
- 第一步:生产者编码
- 第1步:在生产者中添加如下方法:public void sendMessageInTransaction(String msg, String topic)
- 参数1:String msg:具体消息的内容。
- 参数2:String topic,消息要发到哪个topic。
- 第1步:在生产者中添加如下方法:public void sendMessageInTransaction(String msg, String topic)
- 第二步:事务监听器类:本地事务处理(什么时候会被提交、什么时候会被回滚的逻辑判断)
- 第1步:重写两个方法
- 第三步:验证
- 第1步:启动生产者使用单元测试发送事务性消息
- 第2步:控制台打印 vs 管控台
- 第一步:生产者编码
- 事务消息:会根据某些条件是否成立,完成本地事务的提交与回滚。
3 spring cloud stream整合rocketmq
- spring cloud stream提供的便利
- 用最少的改动(只改配置,不改java代码),把项目所用到的消息服务器由kafka或rabbitmq改成用rocketmq。
- 因为spring cloud stream对常用的消息中间件做了更高层次的抽象,底层屏蔽了不同的消息服务器的区别,开发时只需要使用spring cloud stream提供的API进行消息的发送与拉取消费。
- spring cloud stream的组织结构
- 如图:
- Destination Binders:ps:其实就是我们要整合的组件,比如说kafka、rabbitmq、rocketmq。即,首先,你要找到你要整合的组件。
- Destination Bindings:ps:为spring cloud stream和mq服务器之间打开一个传输的通道。生产者和消费者,都会有这么一个通道。
- Message:ps:是一个消息模型,就是用来发消息把生产者的通道里,消费者从通道里拉取消息进行消费s
- 如图:
- spring cloud stream整合rocketmq
- 生产者编码
- 第一步:创建一个springboot程序
- 第二步:引入依赖:web、spring-cloud-starter-stream-rocketmq
- 注意:版本问题:排除4.4.0,全部用4.7.1版本。
- 第三步:编写配置文件
- 第1步:应用名
- 第2步:端口
- 第3步:spring.cloud.stream.bindings:ps:找到我们的消息服务器,找到我们的rocketmq。
output:ps:配置生产者发送消息的目的地topic名称 - 第4步:配置nameServer地址
- 第四步:编写启动类:@EnableBinding(Source.class)
- 第五步:编写生产者类
- @Resource
private Source source ;
-
Map < String , Object > headers = new HashMap <> (); :封装消息头。键值对。
-
Message < String > message =MessageBuilder . createMessage ( msg , messageHeaders ); :构建消息对象
-
发消息:source . output (): ps:拿到一个通道。source . output (). send ( message ); : ps:这个通道就可以把消息发到我们想要的目的地。
- @Resource
- 第六步:写单元测试,发送消息
- 第七步:控制台打印 vs 管控台
- 消费者编码
- 第四步:编写启动类:@EnableBinding(Sink.class)
- 第五步:编写消费者类
-
验证
-
启动生产者、消费者
-
生产者发消息,消费者成功消费
-
- 生产者编码
八 深入理解rocketmq核心概念:概念、关系、规则、api表
概念 | 描述 | 备注 |
生产者 | ||
消费者 | ||
nameServer | 名称服务器 | |
broker集群 | 消息服务集群 |
- 概念
- nameServer:消息服务集群
- 消息模型
- 生产者:发送消息到broker(集群)
- tags:子表达式,过滤消息的。
- 消费者:从broker(集群)拉取消息进行消费
- subExpression:子表达式,过滤消息的。
- broker集群
- broker
- topic(主题):逻辑概念
- queue(队列):物理概念
- queueOffset(队列偏移量):broker->topic->queue->位置 = Offset
- queue(队列):物理概念
- topic(主题):逻辑概念
- broker
- 生产者:发送消息到broker(集群)
- 关系、规则
- nameServer:消息服务集群
- nameServer是轻量级的注册中心。
- 是谁的注册中心呢?broker(集群)会把自己的信息【topic与broker的映射关系】注册到nameServer上面,这样生产者和消费者才能能够通过nameServer找到topic对应的broker和Message Queue。
- 生产者与消费地获取topic地址、broker注册信息的地方。保存topic与broker之间的映射。
- nameServer之间是无状态的(互相不知道)
- nameServer是轻量级的注册中心。
- 消息
- 每条消息必须属于⼀个主题。
- RocketMQ中每条消息拥有唯⼀的Message ID,且可以携带具有业务标识
的Key。 - 系统(管控台)提供了通过Message ID 或者 Key查询消息的功能。
- broker(代理服务器)
- 代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
- broker的Dleger高可用集群:slave自动选举为master。
- cd /rocketmq-all-4.7.1-bin-release/conf/dledger
- broker-n0.conf
- brokerClusterName = RaftCluster
brokerName=RaftNode00:指定broker的名称。
listenPort=30911:端口
namesrvAddr=127.0.0.1:9876:名称服务器nameServer的地址。
storePathRootDir=/tmp/rmqstore/node00:保存的路径
storePathCommitLog=/tmp/rmqstore/node00/commitlog
enableDLegerCommitLog=true
dLegerGroup=RaftNode00
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913
## must be unique
dLegerSelfId=n0
sendMessageThreadPoolNums=16 - 那配置中指定的broker是master还是slave呢?在这里没有说明,所以是master还是slave是由Dleger这种集群它自己去选举出来的。
- brokerClusterName = RaftCluster
- broker-n1.conf
- broker-n2.conf
- broker-n0.conf
- Dleger高可用集群自动选举逻辑
- broker在dleger集群中的3种身份:leader、follower、candidate
- leader:主
- follower:从
- candidate:正在争当主
- dleger中candidate时间片(Term -> TermID)
- 某个节点随着时间片的推移,过程中它会被划分成很多个时间片。
- 时间片由:选举的时间和工作的时间,这2部分组成
- 自动选举逻辑
- 第一步:leader会不断地发送心跳给follower。
- 第二步:leader挂掉。
- 第三步:所有的follower马上都会变成candidate状态。
- 第四步:选举
- 第1步:每个Candidate节点都会投票。票由2部分组成,1部分是TermID,另1部分是它自己的节点ID。
- 第2步:那谁会成为leader呢?这回就要看谁的TermID大,即就近原则(最近活跃、数据最新)。
- broker在dleger集群中的3种身份:leader、follower、candidate
- 自动选举原理图:
- 特点:自动选举次数会比较频繁。
- cd /rocketmq-all-4.7.1-bin-release/conf/dledger
- 消息、broker、topic、Message Queue:
- 消息发送到broker上面。
- topic是一个逻辑的概念。
- 消息在broker上保存的基本单位就是topiic。
- 每个topic包含若干条消息。每条消息只能属于一个主题。
- topic表示一类消息的集合,即topic用来对消息进行逻辑上的分类。
- 每个 Broker 可以存储多个Topic的消息。
- 逻辑topic,对应到物理层面是broker中的Message Queue,Message Queue⽤于存储消息的物理地址。从逻辑角度上看,消息保存在Broker上的Topic中。但从物理角度上看,消息实际是保存在Broker上的逻辑Topic对应的物理Message Queue上。每个Topic中的消息地址存储于多个 Message Queue 中。
- 一条消息保存在topic上时,只会保存在此topic对应的一且仅仅一个Message Queue 中。
- 那这条消息具体会保存在哪一个Message Queue呢?生产者会去做一个轮询的负载均衡,生产者会记录着之前选择的第几号Message Queue,那这条消息进来就保存在之前的那号Message Queue,并且偏移量加1即可。
- 每个topic,会相应地配上多个Message Queue(相当于kafka中的topic分区)。
- 生产组、应用场景
- ⽣产者组将多个⽣产者归为⼀组。
- ⽣产者组是同⼀类Producer的集合。
- 同一个生产组内的生产者,发送同⼀类消息且发送逻辑⼀致。
- 应用场景
- 如果发送的是事务消息且原始⽣产者在发送之后崩溃,则Broker服务器会联系同⼀⽣产者组的其他⽣产者实例以提交或回溯消费。
- topic、消费者
- topic是RocketMQ进⾏消息订阅的基本单位。
- 消费者两种消费获取模式
- ⼀个消息消费者会从Broker服务器拉取消息、并将其提供给应⽤程序。
- 从⽤户应⽤的⻆度⽽⾔提供了两种消费形式:拉取式消费、推动式消费。
- 拉取式消费:消费者主动到broker拉取消息。
- 推动式消费:broker主动把消息推给消费者。
- 实时性较高。
- 消费组、应用场景、注意事项、消费模式
- 消费组是同⼀类Consumer的集合,ConsumerGroup 由多个Consumer 实例构成。
- 消费者组将多个消息消费者归为⼀组,⽤于保证消费者的⾼可⽤和⾼性能。
- 同一个ConsumerGroup 中的Consumer通常消费同⼀类消息且消费逻辑⼀致。
- 应用场景:
- 消费者组使得在消息消费方面,实现负载均衡和容错的⽬标变得⾮常容易。
- 注意事项:
- 要注意的是,消费者组的消费者实例必须订阅完全相同的Topic,这个是必然的。
- 消费组中消费者的两种消费模式:
- RocketMQ ⽀持两种消息模式:集群消费(Clustering)和⼴播消费(Broadcasting)。
- 集群消费:生产者发送多条消息,此时在集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊topic对应的Message Queue。如下图所示:
- 那Consumer Group中的一个Consumer到底去消费哪个Message Queue呢?由broker去做一个消费的负载均衡。
- 广播消费:生产者发送多条消息,此时在⼴播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。
- 集群消费:生产者发送多条消息,此时在集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊topic对应的Message Queue。如下图所示:
- RocketMQ ⽀持两种消息模式:集群消费(Clustering)和⼴播消费(Broadcasting)。
- tag标签
- tag标签用于做消息的分类、区分。
- 根据不同业务⽬的在同⼀主题下设置不同标签。
- key(业务键)的最佳实践
- 每个消息在业务层面的唯一标识码要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic、key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。
- 代码:
// 订单Id String orderId = "20034568923546"; message.setKeys(orderId);
- 代码:
- 每个消息在业务层面的唯一标识码要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic、key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。
- nameServer:消息服务集群
九 消息存储机制
- rocketmq在消息存储这块呢做了很多优化,主要是为了提升rocketmq的性能。
- rocketmq的消息不会保存在内存里面,因为如果消息保存在内存里面的话,消息可能会被丢掉。
- rocketmq和kafka一样,会把消息保存在磁盘文件当中,因此要考虑到对消息文件的读写的性能问题
1 消息存储整体架构
- rocketmq对消息存储的几个优化角度
- 首先,生产者发送的所有消息都会保存在1个或多个commitLog文件时面。
- 印象中生产者发出的消息不是保存在某个topic的某个queue中吗?实际上这只是rocketmq把消息文件commitLog映射成的一个逻辑的关系而已。
- # commitLog存储路径
storePathCommitLog=/usr/local/rocketmq/store/commitlog- 文件1:00000000000000000000000
- 所有的消息都会保存在这个文件里。
- 大小是固定的为1G,消息问题超过1G,就会创建出新的文件00000000001073741824(文件名称固定),再把消息保存到此文件中。
- 文件2:00000000001073741824,超出1g自动创建,用于保存消息数据。
- 文件1:00000000000000000000000
- 其次,消费者要去commitLog文件中去检索某个topic的某个queue(映射)中的消息,总的来说性能还是比较差的。
- rocketmq如何解决检索性能差的问题呢?它会去创建多个逻辑上的划分。为了提高消费者对commitLog文件的检索性能,rocketmq会有一些逻辑上的队列ConsumeQueueXxx。
- ConsumeQueueXxx中保存的是什么东西?ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息⼤⼩size和消息Tag的HashCode值。
- # 消费队列存储路径
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue,
在此文件夹中很多文件夹都是以我们创建过的topic名为名称的。- 如MyBootTopic文件夹
- 如MyTopic1文件夹,包含4个文件夹(0~3),对应此topic的4个(默认)队列:
- 0文件夹:对应队列MyTopic1MessageQueue0
- 00000000000000000文件。
- 此文就对应着图中的ConsumeQueue0。
- 保存着某个topic的某个queue的逻辑位置。
- 物理上讲,文件中存放着就是相应的偏移量。即此文件不会保存具体的消息数据,只会保存一些具体的偏移量数据。
- 00000000000000000文件。
- 1文件夹:对应队列MyTopic1MessageQueue1
- 2文件夹:对应队列MyTopic1MessageQueue2
- 3文件夹:对应队列MyTopic1MessageQueue3
- 0文件夹:对应队列MyTopic1MessageQueue0
- TopicTest如文件夹
- 首先,生产者发送的所有消息都会保存在1个或多个commitLog文件时面。
- 这张图我们可以理解为,它会去刷多个文件啊。
- 生产者
- 生产者把消息写在CommitLog里面
- broker(集群)
- broker保存着ConsumerQueue文件,存储的数据是某个topic的某个queue在CommitLog的位置(物理地址),是消费要去检索的逻辑队列。
- broker还把一些数据,保存到IndexFile索引文件中。
- IndexFile索引文件中也是保存着消息的一些信息,如消息的偏移量、消息的键、消息的头。然后,还可以通过键或者key的哈希也找到消息的位置。
- TimeStamp:时间戳。这是个关键点。和kafka一样,rocketmq可以根据某个时间点来找到某个消息的偏移量。
- IndexFile索引文件中也是保存着消息的一些信息,如消息的偏移量、消息的键、消息的头。然后,还可以通过键或者key的哈希也找到消息的位置。
- 总结:ConsumerQueue和IndexFile是broker中提供检索消息的两个文件
- 消费者
- 消费者代码有一句:consumer.setConsumeFromWhere (ConsumeFromWhere.CONSUME_FROM_FIRST_0FFSET);,设置的是你要从哪里开始消费。
- ConsumeFromWhere.CCONSUME_FROM_TIMESTAMP,即在这里我们也可以指定根据某个时间戳来检索消息并消息消息。
- 消费者代码有一句:consumer.setConsumeFromWhere (ConsumeFromWhere.CONSUME_FROM_FIRST_0FFSET);,设置的是你要从哪里开始消费。
- 生产者
2 页缓存与内存映射
- 我们知道所有的消息都保存在CommitLog文件中,CommitLog保存在磁盘文件中。我们读取它时,有一个用户态到内核心态的转换。
- 用户态:用户开始去读写文件。
- 内核态:真正读写文件时用户是在内核态的状态下才能读写的。
- 问题:读写速度还是慢
- 解决问题:页缓存内存映射
- 缓存:rocketmq给我们提供了页缓存机制。rocketmq会在内存中开辟一块空间叫做页缓存,保存的数据是映射磁盘数据的映射。这样一来,由于对内存的读写比对磁盘的读写会快很多,所以提高了对硬盘文件读写的效率。
- 顺序读写:对于数据的读取,如果⼀次读取⽂件时出现未命中PageCache的情况,OS从物理磁盘上访问读取⽂件的同时,会顺序对其他相邻块的数据⽂件进⾏预读取。这样,再一次提升了地磁盘数据的读写能力。
- 零拷贝技术:再再再一次提升了地磁盘数据的读写能力。
- 解决问题:页缓存内存映射
3 store/abort文件
- broker正常关闭状态下,会把abort文件删除。broker正常启动后,又重新创建abort文件。
- 如果broker启动时,发现有一个abort文件,那么就表示上一次是非正常关闭,此时broker会有一些机制对各个数据进行检查与修复。
- 如偏移量的检查与修复。
- 如果broker启动时,发现有一个abort文件,那么就表示上一次是非正常关闭,此时broker会有一些机制对各个数据进行检查与修复。
4 store/conf/Xxx.json
- 我们的管控台显示的数据,就是从这些json文件中获取的:
- topics.json:保存topic的相关json数据信息。
- consumerFilter.json:保存消息过滤的一些json数据信息。
5 消息刷盘机制
- 消息刷盘:把消息的内容刷到磁盘上
- 生产者把消息保存到磁盘上的CommitLog中。
- 同步刷盘:成功把消息保存到CommitLog中后,才返回ack。如下图所示:
- 优点:消息可靠。
- 缺点:性能、实时性不行。
- 场景:金融项目。
- 异步刷盘:如下图所示:
- 第一步:Producer把消息发送给Broker
- 第二步:消息经过Java Heap(java的堆)
- 第三步:消息经过Virtual Memory(虚拟的内存)
- 这个虚拟内存就是我们前面讲的页缓存。
- 第四步:Broker直接返回一个ack给到生产者。表明了,生产者不用等待数据真正存储到磁盘中,才去做后续的工作。
- 第五步:broker开启一条异步线程,去做消息刷盘的事情。即把消息发送到页缓存中就行了,Broker会开启一条异步线程帮你把消息写到磁盘文件上。这个就叫异步刷盘。
- 优点:提高响应速度、吞吐量
- 缺点:突发情况下,消息可能丢失
- 同步刷盘:成功把消息保存到CommitLog中后,才返回ack。如下图所示:
- 生产者把消息保存到磁盘上的CommitLog中。
十 集群核心概念
1 消息主从复制:消息在主从broker之间是如何复制的
- 2主2从异步通信⽅式
- 第一步:消息写入到broker-a(如果写到broker-a就不会写到broker-b上了)的master上。
- 第二步:master返回ack给生产者。
生产者可以往下继续执行代码,不阻塞。 - 第三步:消息同步到slave。
- 优点:性能好、效率高
- 缺点:可能丢消息
- 2主2从同步通信⽅式
- 第一步:消息写入到broker-a(如果写到broker-a就不会写到broker-b上了)的master上。
- 第二步:等到(阻塞生产者的代码执行)消息同步到slave。
- 第三步:master返回ack给生产者。
- 优点:主从都会有数据,主挂掉,从还有数据。
- 缺点:性能差
- 2主⽆从⽅式
2.负载均衡:RocketMQ 一个topic的队列可以在多个Broker
- 生产者的负载均衡:
- 环境:broker-a和broker-b组成的集群。MyTopicTest在broker-a和broker-b都有,broker-a上的MyTopicTest对应4个queue,broker-b上的MyTopicTest对应4个queue,所以一共8个queue。
- 问题:生产者怎么抉择把消息发送到哪个queue中?
- 实现负载均衡的关键代码:获取8个队列,取模,实现对queue的轮询
- 负载均衡效果:
- 消费者的负载均衡
- 环境:broker-a和broker-b组成的集群。MyTopicTest在broker-a和broker-b都有,broker-a上的MyTopicTest对应4个queue,broker-b上的MyTopicTest对应4个queue,所以一共8个queue。
然后,有属于同一个消费组的两个consumer。 - 问题:rocketmq的规则:1个queue只能被1个消费组中的1个消费者进行消费。
那这8个队列中的消息,到底会被哪个消费者消费呢?消费者的负载均衡是? - 实现负载均衡的关键代码:
-
Consumer的负责均衡可以通过consumer的api进⾏选择queue策略的设置:consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragelyByCircle());
-
- 环境:broker-a和broker-b组成的集群。MyTopicTest在broker-a和broker-b都有,broker-a上的MyTopicTest对应4个queue,broker-b上的MyTopicTest对应4个queue,所以一共8个queue。
3.消息重试
- 消息重试的定义:
- 第一步:某个消费者去消费某条消息
- 注意:消费消息后,消息不是说被删掉了,消息还是存在于broker上面。只是,在消费者消费完消息后提交offset即可。消息还在broker上面 。
- 第二步:然后提交自己的消费偏移量offset到ConsumerQueue
- 问题:消费者什么时候去提交offset?如果某个消费者消费消息是成功的,那么表明它成功提交offset,此时在consumerqueue中会把offset加1并保存,那么下一次消费者就会消费offset+1位置的那个消息。但是,如果提交offset的消费者有情况(比如,突然挂掉),没有成功提交offset,那么此条消费会再次被broker发送给这个消费者或同一个消费组中的另一个消费者进行重复消费消息(比如,挂掉那个消费者和新安排的那个消费者重复),这个就叫做消费的重试。
- 注意:提交offset完后,才意味着消息已经被消费了。如果offset没有被提交、提交offset时出现了异常、提交一些其它内容比如“我出现问题了,我过段时间再消费”,那么此条消息并不会表示被消费了。
- 问题:消费者什么时候去提交offset?如果某个消费者消费消息是成功的,那么表明它成功提交offset,此时在consumerqueue中会把offset加1并保存,那么下一次消费者就会消费offset+1位置的那个消息。但是,如果提交offset的消费者有情况(比如,突然挂掉),没有成功提交offset,那么此条消费会再次被broker发送给这个消费者或同一个消费组中的另一个消费者进行重复消费消息(比如,挂掉那个消费者和新安排的那个消费者重复),这个就叫做消费的重试。
- 第一步:某个消费者去消费某条消息
- 消息重试的条件:在代码层⾯,如果消费者返回的是以下三种情况,则消息会重试消费:
- 消费者返回null,
- 或者返回ConsumeConcurrentlyStatus.RECONSUME_LATER ,
- 或者抛出异常,都会触发重试。
- 消息重试的次数:16次,进入死信队列中。
- 管控台:
- 死信队列:
4.死信队列
- 当成普通队列即可:死信队列它也是一个主题topic(逻辑)对应的队列queue(默认4个):
- 最佳实践建议配置处理死信队列中的消息的消费者。
- 比如,保存到日志中。
- 比如,额外的处理。
5.幂等消息
- 幂等性:多次操作造成的结果是⼀致的。
- get:多次get请求的结果是一致的
- 非幂等性:
- post:添加,非幂等。ps:比如,表单的重复提交(网络延时),数据库中就会有两条记录。
-
⽣产者重复发送:由于⽹络抖动,导致⽣产者没有收到broker的ack⽽再次重发消息,实际上broker收到了多条重复的消息,造成消息重复。
- 消费者重复消费:由于⽹络抖动,消费者没有返回ack给broker,导致消费者重
试消费。
- 处理非幂等性(做幂等性的保证):
- 方案1:mysql 插⼊业务id作为主键,主键是唯⼀的,所以⼀次只能插⼊⼀条。
- 缺点:分布式数据库上,不能保证幂等性。
-
方案2:使⽤redis或zk的分布式锁(主流的⽅案)
- 方案1:mysql 插⼊业务id作为主键,主键是唯⼀的,所以⼀次只能插⼊⼀条。
十一 官配最佳实践
1 保证消息顺序消费
- 消息顺序消费
- 生产者发送消息的顺序是1、2、3,消费者消费消息的顺序也得是1、2、3。
- 如何保证消息顺序消费?
- 全局有序
- topic只会在一个broker上。
- topic只能有一个queue。
- 局部有序
- 生产者:SendResult sendResult = producer.send(msg,new MessageQueueSelector(){ //queue的取模 })
- 消费者:consumer.registerMessageListener(new MessageListener0rderly(){})
- 全局有序
2 快速处理积压消息
(1)什么是消息积压
(2)如何查看消息积压的情况
(3)如何解决消息积压
- (根本不行)增加消费者
- 因为一个queue只能被一个消费组中的一个消费者消费。
- (效果不佳)提高消费者的消费性能,即提高消费者的逻辑业务处理能力,达到迅速提交offset的效果
- 数据库sql的优化,如加索引
- 尽量使用缓存数据库
- 在这个消费者中,使⽤多线程,充分利⽤机器的性能进⾏消费消息。
- 通过业务的架构设计,提升业务层⾯消费的性能。
- 使用微服务
- 使用分布式
- 使用集群部署
- 创建⼀个消费者,该消费者在RocketMQ上另建⼀个主题,该消费者将poll下来的消息,不进⾏消费,直接转发到新建的主题上。新建的主题配上多个MessageQueue,多个MessageQueue再配上多个消费者。此时,新的主题的多个分区的多个消费者就开始⼀起消费了。如下图所示:
注意:这种解决消息积压方案的解决能力也是有限的,我们还是多从业务的角度(层面)去思考是否有这么做的必要性。比如,生产者是否真的有必要每10ms发1万条消息吗?比如,消费者真的有必要把生产者每10ms发送的1万条消息都拿来消费吗?比如,在IOT(物联网)中的消息大都是日志消息,并不是每条日志消息我们都要去处理,或者说实时处理的。
因此在业务层面上,你要去考虑的点就是:哪些消息你要去处理?哪些消息你不用去处理。这样考虑比起我们用这种处理方案,你在业务层面上去考虑去设计的话可能效果上会好很多。
3 保证消息可靠性投递
- 定义:生产者发送的消息,一定(99.99%)能够被消费者消费,也就是说生产者发的消息不会丢失,也就意味着消息从生产到消费的整个过程中消息不会丢失。
- 解决方案:消息处理链路的3个部分:
- 生产者把消息发到broker。
- 解决方案:生产者发事务消息【参考事务消息章节】
- broker集群内部呢,数据要做一些同步操作。因为数据同步操作的过程中,也有可能存在消息数据的丢失。
- 解决方案:broker集群使⽤Dledger⾼可⽤集群。
为什么使用dledger集群就能够保证消息的可靠性投递?也就是说,dledger集群在收到消息后用什么机制来保证消息不会丢失?这时,我们就要关注到dledger集群中数据同步的机制。
其实在dledger集群中会使用两阶段提交的机制:- dledger集群的数据同步由两阶段完成:
- 第⼀阶段:消息到达leader,leader同步消息到follower,消息状态是uncommitted。follower在收到消息以后,返回⼀个ack给leader。同时,leader也会把消息保存到自己的内存里面,并且leader⾃⼰也会返回ack给自己。leader在收到集群中的半数以上的ack后【保证了性能】,开始进⼊到第⼆阶段。
- 第⼆阶段:leader发送committed命令,集群中的所有的broker把消息写⼊到⽇志⽂件(磁盘上)中,此时该消息才表示接收完毕,也就是说消费者才能够消费此消息。
- dledger集群的数据同步由两阶段完成:
- 解决方案:broker集群使⽤Dledger⾼可⽤集群。
- 消费者在消费消息的时候,要防止消息数据的丢失。
- 解决方案:保证消费者的同步消费,
- 即消费者消费完消息完成业务后给broker返回ack(success),最后提交offset。
- 解决方案:保证消费者的同步消费,
- 使⽤基于缓存中间件的MQ降级⽅案:
当MQ整个服务不可⽤时,为了防⽌服务雪崩,生产者可以将消息(发送到)暂存于缓存中间件中,⽐如redis。待MQ恢复后,将redis中的数据重新刷进MQ中。
- 生产者把消息发到broker。