Rocket MQ学习笔记
RocketMQ
点击标题可以选择目录
一.环境配置
1.修改系统配置vim /etc/profile
export JAVA_HOME=/usr/lib/jvm/jre-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64/ export JRE_HOME=$JAVA_HOME/jre export ROCKETMQ_HOME=/usr/local/rocketmq/rocketmq-all-4.9.4-bin-release export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH #配置nameserver的环境变量 export NAMESRV_ADDR=192.168.231.131:9876
完成后执⾏命令,让环境变量⽣效
source /etc/profile
2.修改RocketMQ的jvm内存(虚拟机内存小,大jvm内存可以不用)
1.修改bin/runserver.sh⽂件,由于RocketMQ默认设置的JVM内存为4G,但虚拟机⼀般没有这么4G内存,因此调整为512mb。
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
2.修改broker的JVM参数配置,将默认8G内存修改为512m。修
改 bin/runbroker.sh ⽂件
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m"
二.快速启动(单机)
1.启动NameServer
静默⽅式启动NameServer nohup ./mqnamesrv -n 192.168.231.131:9876 &
查看bin/nohup.out显示如下内容表示启动成功
root@ubuntu:/usr/local/rocketmq/rocketmq-all-4.7.1-bin-release/bin#cat nohup.out Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew youngcollector with the CMS collector is deprecated and will likely be removed in a future release Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release. The Name Server boot success. serializeType=JSON
2.启动Broker
以静默⽅式启动broker nohup ./mqbroker -n 192.168.231.131:9876 &
在 conf/broker.conf ⽂件中加⼊如下配置,开启⾃动创建Topic功能
autoCreateTopicEnable=true
查看 bin/nohup.out ⽇志,显示如下内容表示启动成功
The broker[localhost, 192.168.231.131:10911] boot success. serializeType=JSON
3.使⽤发送和接收消息验证MQ
1.配置nameserver的环境变量
在发送/接收消息之前,需要告诉客户端nameserver的位置。配置环境变量NAMESRV_ADDR :
export NAMESRV_ADDR=192.168.231.131:9876
2.使⽤bin/tools.sh⼯具验证消息的发送,默认会发1000条消息
./tools.sh org.apache.rocketmq.example.quickstart.Producer 1
3.使⽤bin/tools.sh⼯具验证消息的接收
./tools.sh org.apache.rocketmq.example.quickstart.Consumer
4.关闭服务器
关闭broker ./mqshutdown broker 关闭nameserver ./mqshutdown namesrv
三.集群搭建(主从异步)
1.准备三台Linux服务器(三个服务器都需要按环境配置进行)
服务器 | 服务器IP | NameServer | broker节点部署 |
---|---|---|---|
服务器1 | 192.168.231.133 | 192.168.231.133:9876 | |
服务器2 | 192.168.231.134 | 192.168.231.134:9876 | broker-a(master),broker-b-s(slave) |
服务器3 | 192.168.231.135 | 192.168.231.135:9876 | broker-b(master),broker-a-s(slave) |
三台服务器都需要安装jdk和rocketmq,安装步骤参考上⼀章节
2.启动三台nameserver
服务器1: nohup ./mqnamesrv -n 192.168.231.133:9876 & 服务器2: nohup ./mqnamesrv -n 192.168.231.134:9876 & 服务器3: nohup ./mqnamesrv -n 192.168.231.135:9876 &
3.配置broker
第二台服务器
broker-a.properties
brokerClusterName=DefaultCluster # broker名字 brokerName=broker-a # broker所在服务器的ip brokerIP1=192.168.231.134 # broker的id,0表示master,>0表示slave brokerId=0 # 删除⽂件时间点,默认在凌晨4点 deleteWhen=04 # ⽂件保留时间为48⼩时 fileReservedTime=48 # broker的⻆⾊为master brokerRole=ASYNC_MASTER # 使⽤异步刷盘的⽅式 flushDiskType=ASYNC_FLUSH # 名称服务器的地址列表 namesrvAddr=192.168.231.133:9876;192.168.231.134:9876;192.168.231.135:9876 # 在发送消息⾃动创建不存在的topic时,默认创建的队列数为4个 defaultTopicQueueNums=4 # 是否允许 Broker ⾃动创建Topic,建议线下开启,线上关闭 autoCreateTopicEnable=true # 是否允许 Broker ⾃动创建订阅组,建议线下开启,线上关闭 autoCreateSubscriptionGroup=true # broker对外服务的监听端⼝ 一个服务器上不要一样 listenPort=10911 # abort⽂件存储路径 abortFile=/usr/local/rocketmq/store/abort # 消息存储路径 storePathRootDir=/usr/local/rocketmq/store # commitLog存储路径 storePathCommitLog=/usr/local/rocketmq/store/commitlog # 消费队列存储路径 storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue # 消息索引存储路径 storePathIndex=/usr/local/rocketmq/store/index # checkpoint⽂件存储路径 storeCheckpoint=/usr/local/rocketmq/store/checkpoint # 限制的消息⼤⼩ maxMessageSize=65536 # commitLog每个⽂件的⼤⼩默认1G mapedFileSizeCommitLog=1073741824 # ConsumeQueue每个⽂件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000
broker-b-s.properties
brokerClusterName=DefaultCluster brokerName=broker-b-s brokerIP1=192.168.231.134 brokerId=1 deleteWhen=04 fileReservedTime=48 #配置角色为slave brokerRole=SLAVE flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.231.133:9876;192.168.231.134:9876;192.168.231.135:9876 defaultTopicQueueNums=4 autoCreateTopicEnable=true autoCreateSubscriptionGroup=true listenPort=11011 abortFile=/usr/local/rocketmq/store-slave/abort storePathRootDir=/usr/local/rocketmq/store-slave storePathCommitLog=/usr/local/rocketmq/store-slave/commitlog storePathConsumeQueue=/usr/local/rocketmq/store-slave/consumequeue storePathIndex=/usr/local/rocketmq/store-slave/index storeCheckpoint=/usr/local/rocketmq/store-slave/checkpoint maxMessageSize=65536 mapedFileSizeCommitLog=1073741824 mapedFileSizeConsumeQueue=300000
第三台服务器
broker-a-s.properties
brokerClusterName=DefaultCluster brokerName=broker-a-s brokerIP1=192.168.231.135 brokerId=1 deleteWhen=04 fileReservedTime=48 brokerRole=SLAVE flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.231.133:9876;192.168.231.134:9876;192.168.231.135:9876 defaultTopicQueueNums=4 autoCreateTopicEnable=true autoCreateSubscriptionGroup=true listenPort=11011 abortFile=/usr/local/rocketmq/store-slave/abort storePathRootDir=/usr/local/rocketmq/store-slave storePathCommitLog=/usr/local/rocketmq/store-slave/commitlog storePathConsumeQueue=/usr/local/rocketmq/store-slave/consumequeue storePathIndex=/usr/local/rocketmq/store-slave/index storeCheckpoint=/usr/local/rocketmq/store-slave/checkpoint maxMessageSize=65536 mapedFileSizeCommitLog=1073741824 mapedFileSizeConsumeQueue=300000
broker-b.properties
brokerClusterName=DefaultCluster brokerName=broker-b brokerIP1=192.168.231.135 brokerId=0 deleteWhen=04 fileReservedTime=48 brokerRole=ASYNC_MASTER flushDiskType=ASYNC_FLUSH namesrvAddr=192.168.231.133:9876;192.168.231.134:9876;192.168.231.135:9876 defaultTopicQueueNums=4 autoCreateTopicEnable=true autoCreateSubscriptionGroup=true listenPort=10911 abortFile=/usr/local/rocketmq/store/abort storePathRootDir=/usr/local/rocketmq/store storePathCommitLog=/usr/local/rocketmq/store/commitlog storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue storePathIndex=/usr/local/rocketmq/store/index storeCheckpoint=/usr/local/rocketmq/store/checkpoint maxMessageSize=65536 mapedFileSizeCommitLog=1073741824 mapedFileSizeConsumeQueue=300000
4.启动broker
-
在服务器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 & -
在服务器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 &
5.验证集群
修改环境变量
export NAMESRV_ADDR='192.168.231.133:9876;192.168.231.134:9876;192.168.231.135:9876'
启动⽣产者
./tools.sh org.apache.rocketmq.example.quickstart.Producer
启动消费者
./tools.sh org.apache.rocketmq.example.quickstart.Consumer
6.mqadmin管理⼯具
RocketMQ提供了命令⼯具⽤于管理topic、broker、集群、消息等。⽐如可以使⽤
mqadmin创建topic:
./mqadmin updateTopic -n 172.16.253.101:9876 -c DefaultCluster -t myTopic1
创建topic:updateTopic
删除Topic:deleteTopic
创建(修订)订阅组:updateSubGroup
删除订阅组配置:deleteSubGroup
更新Broker 配置⽂件:updateBrokerConfig
查看Topic 列表信息:topicList
查看Topic 路由信息:topicRoute
查看Topic 统计信息:topicStats
查看Broker 统计信息:brokerStats
根据消息ID 查询消息:queryMsgById
根据消息Key 查询消息:queryMsgByKey
根据Offset 查询消息:queryMsgByOffset
查询Producer 的⽹络连接:producerConnection
查询Consumer 的⽹络连接:consumerConnection
查看订阅组消费状态:consumerProgress
查看集群消息:clusterList
添加(更新)KV 配置信息:updateKvConfig
删除KV 配置信息:deleteKvConfig
添加(更新)Project group 配置信息:updateProjectGroup
删除Project group 配置信息:deleteProjectGroup
取得Project group 配置信息:getProjectGroup
设置消费进度:resetOffsetByTime
清除特定Broker权限:wipeWritePerm
获取Consumer消费进度:getConsumerStatus
7.安装可视化管理控制平台
RocketMQ没有提供可视化管理控制平台,可以使⽤第三⽅管理控制平台:https://github.com/apache/rocketmq-externals/tree/rocketmq-console-1.0.0/rocketmq-console
1.下载管理控制平台
/usr/local/rocketmq
2.解压缩在linux服务器上
unzip ...
3.需要安装maven
Linux安装maven(详细教程) - 付宗乐 - 博客园 (cnblogs.com)
4.rocketmqconsole/src/main/resources/application.properties 配置⽂件中的nameserver地址
rocketmq.config.namesrvAddr=192.168.231.133:9876;192.168.231.134:9876;192.168.231.135:9876
pom.xml添加
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>compile</scope> </dependency>
5.mvn打包jar(jdk配置一定要正确)
mvn clean package -Dmaven.test.skip=true
6.允许jar包
运⾏jar包。进⼊到 rocketmq-externals/rocketmq-externals-master/rocketmq-console/target ⽬录内执⾏如下命令
nohup java -jar rocketmq-console-ng-1.0.1.jar &
7.访问服务器8080接口(可自定义接口)
四.消息示例
1.构建Java基础环境
在maven项⽬中构建出RocketMQ消息示例的基础环境,即创建⽣产者程序和消费者程序。通过⽣产者和消费者了解RocketMQ操作消息的原⽣API。
2.引入依赖
#和MQ版本一致 <dependencies> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.9.4</version> </dependency> </dependencies>
3.简单的生产和消费
1.编写生产者
public class BaseProducer { public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException { //1.创建生产者 DefaultMQProducer producer = new DefaultMQProducer("my-producer-group1"); //2.指定nameserver地址 producer.setNamesrvAddr("192.168.231.133:9876"); //3.启动生产者 producer.start(); //4.创建消息 for (int i = 0; i < 10; i++) { Message message = new Message("MyTopic1","TagA",("hello rocketmq"+i).getBytes(StandardCharsets.UTF_8)); //5.发送消息 SendResult sendResult =producer.send(message); System.out.println(sendResult); } //6.关闭生产者 producer.shutdown(); } }
2.编写消费者
public class BaseConsumer { public static void main(String[] args) throws MQClientException { //1.创建消费者对象 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer-group1"); //2.指明nameserver的地址 consumer.setNamesrvAddr("192.168.231.133:9876"); //3.订阅主题:topic 何过滤消息用的tag表达式 consumer.subscribe("Jodie_topic_1023","TagA"); //4.创建一个监听器,当broker把消息推过来时调用 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for (Message msg : msgs){ //将bat数组转化为string //System.out.println("收到的消息"+new String(msg.getBody())); System.out.println("收到的消息"+msg); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者 consumer.start(); System.out.println("消费者已启动"); } }
3.启动消费者和⽣产者,验证消息的收发
4.简单消息示例(同步,异步,单向)
只有生产者 消费者可以用四-3.1的消费者
简单消息分成三种:同步消息、异步消息、单向消息。
1.同步消息
⽣产者发送消息后,必须等待broker返回信息后才继续之后的业务逻辑,在broker返回信息之前,⽣产者阻塞等待。
public class SyncProducer { public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException { //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("producerGroup1"); // Specify name server addresses. producer.setNamesrvAddr("172.16.253.101:9876"); //Launch the instance. producer.start(); for (int i = 0; i < 100; i++) { //Create a message instance, specifying topic, tag and message body. Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); //Call send message to deliver message to one of brokers. SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } //Shut down once the producer instance is not longer in use. producer.shutdown(); } }
2.异步消息
⽣产者发完消息后,不需要等待broker的回信,可以直接执⾏之后的业务逻辑。⽣产者提供⼀个回调函数供broker调⽤,体现了异步的⽅式。
public class AsyncProducer { public static void main(String[] args) throws Exception { //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // Specify name server addresses. producer.setNamesrvAddr("192.168.231.133:9876"); //Launch the instance. producer.start(); //设置失败重发次数 producer.setRetryTimesWhenSendAsyncFailed(0); int messageCount = 100; //定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前, //阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程 final CountDownLatch countDownLatch = new CountDownLatch(messageCount); for (int i = 0; i < messageCount; i++) { try { final int index = i; Message msg = new Message("Jodie_topic_1023", "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); //重点在这里 异步发送回调 producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { //countDown()对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。 countDownLatch.countDown(); System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } @Override public void onException(Throwable e) { countDownLatch.countDown(); System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } }); } catch (Exception e) { e.printStackTrace(); } } System.out.println("============="); //在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行 //countDown()提前结束为0可以提前唤醒 countDownLatch.await(5, TimeUnit.SECONDS); producer.shutdown(); } }
3.单向消息
⽣产者发送完消息后不需要等待任何回复,直接进⾏之后的业务逻辑,单向传输⽤于需要中等可靠性的情况,例如⽇志收集。
public class OnewayProducer { public static void main(String[] args) throws Exception { //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // Specify name server addresses. producer.setNamesrvAddr("172.16.253.101:9876"); //Launch the instance. producer.start(); for (int i = 0; i < 100; i++) { //Create a message instance, specifying topic, tag and message body. Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); //Call send message to deliver message to one of brokers. producer.sendOneway(msg); } //Wait for sending to complete Thread.sleep(5000); producer.shutdown(); } }
5.顺序消息
顺序消息指的是消费者消费消息的顺序按照发送者发送消息的顺序执⾏。顺序消息分成两种:局部顺序和全局顺序
1.局部顺序(主)
局部消息指的是消费者消费某个topic的某个队列中的消息是顺序的。消费者使⽤MessageListenerOrderly类做消息监听,实现局部顺序。
实际上,每一个消费者的的消费端都是采用线程池实现多线程消费的模式,即消费端是多线程消费。虽然MessageListenerOrderly被称为有序消费模式,但是仍然是使用的线程池去消费消息。 MessageListenerConcurrently是拉取到新消息之后就提交到线程池去消费,而MessageListenerOrderly则是通过加分布式锁和本地锁保证同时只有一条线程去消费一个队列上的数据。
生产者
public class OrderProducer { public static void main(String[] args) throws Exception { //Instantiate with a producer group name. MQProducer producer = new DefaultMQProducer("example_group_name"); //名字服务器的地址已经在环境变量中配置好了:NAMESRV_ADDR=192.168.231.133:9876 //Launch the instance. producer.start(); for (int i = 0; i < 10; i++) { int orderId = i; for(int j = 0 ; j <= 5 ; j ++){ Message msg = new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId, ("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg, new MessageQueueSelector() { @Override //arg代表的就时orderId public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Integer id = (Integer) arg; int index = id % mqs.size(); return mqs.get(index); } }, orderId); System.out.printf("%s%n", sendResult); } } //server shutdown producer.shutdown(); } }
消费者
public class OrderConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.subscribe("OrderTopicTest", "*"); consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(true); for(MessageExt msg:msgs){ System.out.println("消息内容:"+new String(msg.getBody())); } return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
2.全局顺序
消费者消费全部消息都是顺序的,只能通过⼀个某个topic只有⼀个队列才能实现,这种应⽤场景较少,且性能较差。3.
3.乱序消费
消费者消费消息不需要关注消息的顺序。消费者使⽤MessageListenerConcurrently类做消息监听.
public class OrderConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.subscribe("OrderTopicTest", "*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for(MessageExt msg:msgs){ System.out.println("消息内容:"+new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
6.⼴播消息
⼴播是向主题(topic)的所有订阅者发送消息。订阅同⼀个topic的多个消费者,每一个消费者都能全量收到⽣产者发送的所有消息
消费者
/** * 广播消息 */ public class BroadcastConsumer { public static void main(String[] args) throws Exception { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //set to broadcast mode 设置为 广播模式 consumer.setMessageModel(MessageModel.BROADCASTING); consumer.subscribe("TopicTest", "*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for(MessageExt msg:msgs){ System.out.println("消息内容:"+new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Broadcast Consumer Started.%n"); } }
生产者(没区别)
public class BroadcastProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); producer.start(); for (int i = 0; i < 100; i++){ Message msg = new Message("TopicTest", "TagA", "OrderID188", ("Hello world"+i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } producer.shutdown(); } }
7.延迟消息
延迟消息与普通消息的不同之处在于,它们要等到指定的时间之后才会被传递。
RocketMQ设计了18个延迟等级,分别是:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 在商业版RocketMQ中,不仅可以设置延迟等级,还可以设置具体的延迟时间,但是 在社区版RocketMQ中,只能设置延迟等级。
消息⽣产者
public class ScheduledProducer { public static void main(String[] args) throws Exception { // Instantiate a producer to send scheduled messages DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); // Launch producer producer.start(); int totalMessagesToSend = 100; for (int i = 0; i < totalMessagesToSend; i++) { Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes()); // This message will be delivered to consumer 10 seconds later. //延迟发送 等级为3 就是10s 不同等级对应不同延迟时间 message.setDelayTimeLevel(3); // Send the message producer.send(message); } // Shutdown producer after use. producer.shutdown(); } }
消息消费者
public class ScheduledConsumer { public static void main(String[] args) throws MQClientException { // Instantiate message consumer DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); // Subscribe topics consumer.subscribe("TestTopic", "*"); // Register message listener consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) { for (MessageExt message : messages) { // Print approximate delay time period System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later"); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // Launch consumer consumer.start(); } }
8.批量消息
批量发送消息提⾼了传递⼩消息的性能。
使⽤批量消息
public class BatchProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); producer.start(); String topic = "BatchTest"; List<Message> messages = new ArrayList<>(); messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); producer.send(messages); producer.shutdown(); } }
超出限制的批量消息
官⽅建议批量消息的总⼤⼩不应超过1m,实际不应超过4m。如果超过4m的批量消息需要进⾏分批处理,同时设置broker的配置参数为4m(在broker的配置⽂件中修改: maxMessageSize=4194304 )
public class MaxBatchProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName"); producer.start(); //large batch String topic = "BatchTest"; List<Message> messages = new ArrayList<>(100*1000); for (int i = 0; i < 100*1000; i++) { messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes())); } // producer.send(messages); //split the large batch into small ones:将一个大的数组分割为多个小于4M的小数组 //broker里面也需要对应设置 默认的小于4m ListSplitter splitter = new ListSplitter(messages); while (splitter.hasNext()) { List<Message> listItem = splitter.next(); producer.send(listItem); } producer.shutdown(); } }
9.过滤消息
在⼤多数情况下,标签是⼀种简单⽽有⽤的设计,可以⽤来选择您想要的消息。
大家想一下,这个消息过滤是在Broker端进行的还是在Consumer端进行的?
答:Broker端,避免网络浪费
1.Tag过滤
tag过滤的⽣产者
public class TagProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC"}; for (int i = 0; i < 15; i++) { Message msg = new Message("TagFilterTest", tags[i % tags.length], //每次使用一个tag "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } producer.shutdown(); } }
tag过滤的消费者
public class TagConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); consumer.subscribe("TagFilterTest", "TagA || TagC"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
消费者将收到包含 TAGA 或 TAGB 或 TAGC 的消息。但是限制是⼀条消息只能有⼀个标签,这可能不适⽤于复杂的场景。在这种情况下,您可以使⽤ SQL 表达式来过滤掉消息。
2.使⽤SQL过滤
!!!!使用sql需要在每一个对应broker.conf中添加设置
enablePropertyFilter=true
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 | ------------
1.语法
RocketMQ 只定义了⼀些基本的语法来⽀持这个特性,也可以轻松扩展它。
1. 数值⽐较,如`>`, `>=`, `<`, `<=`, `BETWEEN`, `=`; 2. 字符⽐较,如`=`, `<>`, `IN`; 3. `IS NULL`或`IS NOT NULL`; 4. 逻辑`AND`, `OR`, `NOT`;
常量类型有:
1. 数字,如 123、3.1415; 2. 字符,如'abc',必须⽤单引号; 3. `NULL`,特殊常数; 4. 布尔值,`TRUE`或`FALSE`;
使⽤注意:只有推模式的消费者可以使⽤SQL过滤。拉模式是⽤不了的。
2.SQL过滤的⽣产者示例
public class SQLProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC"}; for (int i = 0; i < 15; i++) { Message msg = new Message("SqlFilterTest", tags[i % tags.length], ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) ); // Set some properties. 自定义设置属性 msg.putUserProperty("a", String.valueOf(i)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } producer.shutdown(); } }
3.SQL过滤的消费者示例
public class SQLConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); // Don't forget to set enablePropertyFilter=true in broker 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)")); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
10.事务消息
1.事务消息的定义
它可以被认为是⼀个两阶段的提交消息实现,以确保分布式系统的最终⼀致性。事务性消息确保本地事务的执⾏和消息的发送可以原⼦地执⾏。事务消息有三种状态:
a.TransactionStatus.CommitTransaction:提交事务,表示允许消费者消费该消息。
b.TransactionStatus.RollbackTransaction:回滚事务,表示该消息将被删除,不允许消费。
c.TransactionStatus.Unknown:中间状态,表示需要MQ回查才能确定状态。
2.事务消息的实现流程
生产者
public class TransactionProducer { public static void main(String[] args) throws Exception { //事务监听器 TransactionListener transactionListener = new TransactionListenerImpl(); //事务生产者 TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("172.16.253.101:9876"); //线程池 适合大量短小的任务 // corePoolSize : 核心线程数,一旦创建将不会再释放。(当corePoolSize创建满了才会创建非核心线程 且总数不能大于最大线程数) //maximumPoolSize : 最大线程数,允许创建的最大线程数量 //keepAliveTime : 也就是当线程空闲时,所允许保存的最大时间,超过这个时间,线程将被释放销毁,但只针对于非核心线程。 //unit : 时间单位,TimeUnit.SECONDS等。 //workQueue : 任务队列,用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。 // ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,必须设置容量。此队列按 FIFO(先进先出)原则对元素进行排序。 // LinkedBlockingQueue:一个基于链表结构的阻塞队列,可以设置容量,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。 // SynchronousQueue:一个不存储元素的阻塞队列。每个插入offer操作必须等到另一个线程调用移除poll操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。 // PriorityBlockingQueue:一个具有优先级的无限阻塞队列。 //threadFactory : 线程工厂,用于创建线程。 ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("client-transaction-msg-check-thread"); return thread; } }); //设置线程池 producer.setExecutorService(executorService); //设置事务监听器 producer.setTransactionListener(transactionListener); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; for (int i = 0; i < 10; i++) { try { Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); //发送事务消息 SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); Thread.sleep(10); } catch (MQClientException | UnsupportedEncodingException e) { e.printStackTrace(); } } for (int i = 0; i < 100000; i++) { Thread.sleep(1000); } producer.shutdown(); } }
本地事务处理-TransactionListener
public class TransactionListenerImpl implements TransactionListener { /** *当第一阶段(half)成功,将会执行本地事务 * @param msg Half(prepare) message * @param arg Custom business parameter * @return Transaction state */ @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { String tags = msg.getTags(); if(StringUtils.contains(tags,"TagA")){ return LocalTransactionState.COMMIT_MESSAGE; }else if(StringUtils.contains(tags,"TagB")){ return LocalTransactionState.ROLLBACK_MESSAGE; }else{ return LocalTransactionState.UNKNOW; } } /** * When no response to prepare(half) message. broker will send check message to check the transaction status, and this * method will be invoked to get local transaction status. *当没有响应准备阶段的half消息,broker将会发送检查消息,检查本地事务的状态 * @param msg Check message * @return Transaction state */ //检查本地事务 执行事务中状态unknow的事务会再一次执行 //如果连续15次为nuknow次抛弃该事务 @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; } } }
消费者
public class TransactionConsumer { public static void main(String[] args) throws MQClientException { //1.创建消费者对象 对于消费者则普通 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer-group1"); //2.指明nameserver的地址 consumer.setNamesrvAddr("172.16.253.101:9876"); //3.订阅主题:topic 和过滤消息用的tag表达式 consumer.subscribe("TopicTest","*"); //4.创建一个监听器,当broker把消息推过来时调用 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { // System.out.println("收到的消息:"+new String(msg.getBody())); System.out.println("收到的消息:"+msg); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者 consumer.start(); System.out.println("消费者已启动"); } }
3.使⽤限制
- 事务性消息没有调度和批处理⽀持。
- 为避免单条消息被检查次数过多,导致半队列消息堆积,我们默认将单条消息的检查次数限制为15次,但⽤户可以通过更改“transactionCheckMax”来更改此限制”参数在broker的配置中,如果⼀条消息的检查次数超过“transactionCheckMax”次,broker默认会丢弃这条消息,同时打印错误⽇志。⽤户可以通过重写“AbstractTransactionCheckListener”类来改变这种⾏为。
- 事务消息将在⼀定时间后检查,该时间由代理配置中的参数“transactionTimeout”确定。并且⽤户也可以在发送事务消息时通过设置⽤户属性“CHECK_IMMUNITY_TIME_IN_SECONDS”来改变这个限制,这个参数优先于“transactionMsgTimeout”参数。
- ⼀个事务性消息可能会被检查或消费不⽌⼀次。
- 提交给⽤户⽬标主题的消息reput可能会失败。⽬前,它取决于⽇志记录。
- ⾼可⽤是由 RocketMQ 本身的⾼可⽤机制来保证的。如果要保证事务消息不丢失,保证事务完整性,推荐使⽤同步双写机制。
- 事务性消息的⽣产者 ID 不能与其他类型消息的⽣产者 ID 共享。与其他类型的消息不同,事务性消息允许向后查询。MQ 服务器通过其⽣产者 ID 查询客户端。
11.生产者-消费者细节
生产者:
有拉模式(pull)和推模式(push)
推模式更加简单
拉模式(Litepull用的多,Defaultpull被遗弃了)虽然复杂一点,但是可以自定义更加精细化的操作。例如:回溯消费
生产者给broker的queue发送消息的时候,获取有该topic的所有队列数,通过线程获取index,通过自增的方式对队列数取模,也可以理解为轮询的方式
五.SpringBoot整合RocketMQ
1.引入依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency>
2.编写配置⽂件
生产者yml
server: port: 8080 spring: application: name: my-boot-producer-demo rocketmq: name-server: 192.168.231.133:9876 producer: group: my-boot-producer-group
消费者yml
#消费组 topic可以在在代码中配置 @RocketMQMessageListener(consumerGroup = "my-boot-consumer-group1",topic = "my-boot-topic") #也可以yml配置(还需要验证) server: port: 8081 spring: application: name: my-boot-consumer-demo rocketmq: name-server: 192.168.231.133:9876
3.编写⽣产者发送普通消息
@Component public class MyProducer { @Resource private RocketMQTemplate rocketMQTemplate; /** * 发送消息 * @param msg 消息内容 * @param topic ⽬标主题 */ public void sendMessage(String topic,String message){ //将msg转换成Message对象并发送 rocketMQTemplate.convertAndSend(topic,message); } }
4.编写JUnit单元测试发送消息
@Resource private MyProducer myProducer; @Test void contextLoads() { String topic = "my-boot-topic"; String message = "hello spring boot rocketmq"; myProducer.sendMessage(topic,message); System.out.println("消息发送成功"); }
5.创建消费者程序
@Component //消费者组 topic等 @RocketMQMessageListener(consumerGroup = "my-boot-consumer-group1", topic = "my-boot-topic") public class MyConsumer implements RocketMQListener<String> { @Override public void onMessage(String s) { System.out.println("收到的消息"+s); } }
6.发送事务消息
编写⽣产者⽅法
public void sendMessageInTransaction(String msg, String topic) throws Exception { String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"}; for (int i = 0; i < 10; i++) { //注意该message为org.springframework.messaging.Message Message<String> message = MessageBuilder.withPayload(msg).build(); //topic和tag整合在⼀起,以":"隔开 String destination = topic+":"+tags[i % tags.length]; //第⼀个destination为消息要发到的⽬的地,第⼆个destination为 消协携带的业务数据 TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message, destination); System.out.println(sendResult); Thread.sleep(10); } }
编写事务监听器类
//rocketMQTemplateBeanName:指定使用哪个 RocketMQTemplate 实例来发送 RocketMQ 事务消息。 // 如果不指定,默认使用名为 "rocketMQTemplate" 的 RocketMQTemplate 实例; @RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate") public class MyTransactionListenerImpl implements RocketMQLocalTransactionListener { @Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { //获得业务参数中的数据 MyProducer中的第三个值destination String destination = (String) arg; //StringMessageConverter()是 Spring 框架中的一个消息转换器 将普通消息转为spring消息 //再使⽤RocketMQUtil将spring的message转换成rocketmq的message org.apache.rocketmq.common.message.Message message = RocketMQUtil.convertToRocketMessage( new StringMessageConverter(),"utf- 8",destination,msg); //获得消息中的业务数据tags String tags = message.getTags(); if(StringUtils.contains(tags,"TagA")){ //提交本地事务 return RocketMQLocalTransactionState.COMMIT; }else if(StringUtils.contains(tags,"TagB")){ //回滚 return RocketMQLocalTransactionState.ROLLBACK; }else{ //中间状态 return RocketMQLocalTransactionState.UNKNOWN; } } @Override public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { return null; } }
编写单元测试发送事务消息
@Test void testSendTransactionMessage() throws Exception { String topic = "MyBootTopic"; String message = "hello transaction spring boot rocketmq"; producer.sendMessageInTransaction(message,topic); }
消费者同上面一套
六.Spring Cloud Stream整合RocketMQ
1.Spring Cloud Stream介绍
Spring Cloud Stream 是⼀个框架,⽤于构建与共享消息系统连接的⾼度可扩展的事件驱动微服务。
该框架提供了⼀个灵活的编程模型,该模型基于已经建⽴和熟悉的 Spring 习惯⽤法和最佳实践,包括对持久 pub/sub 语义、消费者组和有状态分区的⽀持。
Spring Cloud Stream 的核⼼构建块是:
- Destination Binders:负责提供与外部消息传递系统集成的组件。
- Destination Bindings:外部消息系统和最终⽤户提供的应⽤程序代码(⽣产者/消费者)之间的桥梁。
- Message:⽣产者和消费者⽤来与⽬标绑定器(以及通过外部消息系统的其他应⽤程序)进⾏通信的规范数据结构。
2.编写⽣产者
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 一定要主意版本的对应rocket4.9.4--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-stream-rocketmq</artifactId> </dependency>
编写配置⽂件
server: port: 8080 spring: cloud: stream: rocketmq: binder: name-server: 192.168.231.133:9876 group: group-producer #生产者组名称必须要 不然报错 定义在binder中 bindings: output: destination: TopicStream contentType: application/json application: name: my-cloud-rocketmq-producer
启动类上打上注解
其中 @EnableBinding(Source.class) 指向配置⽂件的output参数。
@EnableBinding(Source.class) @SpringBootApplication public class MyCloudRocketmqProducerApplication { public static void main(String[] args) { SpringApplication.run(MyCloudRocketmqProducerApplication.class, args); } }
编写⽣产者程序
@Component public class MyProducer { @Resource private Source source; public void sendMessage(String msg){ //封装消息头 Map<String, Object> headers = new HashMap<>(); headers.put(MessageConst.PROPERTY_TAGS,"TagA"); MessageHeaders messageHeaders = new MessageHeaders(headers); //创建消息对象 Message<String> message = MessageBuilder.createMessage(msg,messageHeaders); //发送消息 source.output().send(message); } }
编写单元测试发送消息
@SpringBootTest class MySRocketmqDemoApplicationTests { @Autowired private MyProducer producer; @Test void testSendMessage(){ producer.sendMessage("hello spring cloud stream"); } }
3.编写消费者
引⼊依赖和生产者一致
编写配置⽂件
server: port: 8081 spring: application: name: my-cloud-rocketmq-consumer cloud: stream: rocketmq: binder: name-server: 192.168.231.133:9876 bindings: input: destination: TopicStream content-type: application/json group: group-consumer #消费者组 必须不然消费者名字随机生产
启动类上打上注解
其中 @EnableBinding(Sink.class) 指向配置⽂件的input参数。
@EnableBinding(Sink.class) @SpringBootApplication public class MyCloudRocketmqConsumerApplication { public static void main(String[] args) { SpringApplication.run(MyCloudRocketmqConsumerApplication.class, args); } }
编写消费者程序
@Component public class MyConsumer { @StreamListener(Sink.INPUT) public void onMessage(String message){ System.out.println("收到的消息:"+message); } }
七.RocketMQ核⼼概念
1.消息模型(Message Model)
RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责⽣产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应⼀台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分⽚存储于不同的 Broker。Message Queue ⽤于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。
2.消息⽣产者(Producer)
负责⽣产消息,⼀般由业务系统负责⽣产消息。⼀个消息⽣产者会把业务应⽤系统⾥产⽣的消息发送到broker服务器。RocketMQ提供多种发送⽅式,同步发送、异步发送、顺序发送、单向发送。同步和异步⽅式均需要Broker返回确认信息,单向发送不需要。
⽣产者组将多个⽣产者归为⼀组。⽤于保证⽣产者的⾼可⽤,⽐如在事务消息中回查本地事务状态,需要⽣产者具备⾼可⽤的特性,才能完成整个任务。
3.消息消费者(Consumer)
负责消费消息,⼀般是后台系统负责异步消费。⼀个消息消费者会从Broker服务器拉取消息、并将其提供给应⽤程序。从⽤户应⽤的⻆度⽽⾔提供了两种消费形式:拉取式消费、推动式消费。
消费者组将多个消息消费者归为⼀组,⽤于保证消费者的⾼可⽤和⾼性能。
4.主题(Topic)
表示⼀类消息的集合,每个主题包含若⼲条消息,每条消息只能属于⼀个主题,是RocketMQ进⾏消息订阅的基本单位。
5.代理服务器(Broker Server)
消息中转⻆⾊,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从⽣产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
6.名字服务(Name Server)
名称服务充当路由消息的提供者。⽣产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独⽴,没有信息交换。
7.拉取式消费(Pull Consumer)
Consumer消费的⼀种类型,应⽤通常主动调⽤Consumer的拉消息⽅法从Broker服务器拉消息、主动权由应⽤控制。⼀旦获取了批量消息,应⽤就会启动消费过程。
8.推动式消费(Push Consumer)
Consumer消费的⼀种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式⼀般实时性较⾼。
9.⽣产者组(Producer Group)
同⼀类Producer的集合,这类Producer发送同⼀类消息且发送逻辑⼀致。如果发送的是事务消息且原始⽣产者在发送之后崩溃,则Broker服务器会联系同⼀⽣产者组的其他⽣产者实例以提交或回溯消费。
10.消费者组(Consumer Group)
同⼀类Consumer的集合,这类Consumer通常消费同⼀类消息且消费逻辑⼀致。消费者组使得在消息消费⽅⾯,实现负载均衡和容错的⽬标变得⾮常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ ⽀持两种消息模式:集群消费(Clustering)和⼴播消费(Broadcasting)。
11.集群消费(Clustering)
集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
12.⼴播消费(Broadcasting)
⼴播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。
13.普通顺序消息(Normal Ordered Message)
普通顺序消费模式下,消费者通过同⼀个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是⽆顺序的。
14.严格顺序消息(Strictly Ordered Message)
严格顺序消息模式下,消费者收到的所有消息均是有顺序的。
15.消息(Message)
消息系统所传输信息的物理载体,⽣产和消费数据的最⼩单位,每条消息必须属于⼀个主题。RocketMQ中每个消息拥有唯⼀的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。
16.标签(Tag)
为消息设置的标志,⽤于同⼀主题下区分不同类型的消息。来⾃同⼀业务单元的消息,可以根据不同业务⽬的在同⼀主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可根据Tag实现对不同⼦主题的不同消费逻辑,实现更好的扩展性。
八.消息存储机制
消息存储是RocketMQ中最为复杂和最为重要的⼀部分,本节将分别从RocketMQ的消息存储整体架构、PageCache与Mmap内存映射以及RocketMQ中两种不同的刷盘⽅式三⽅⾯来分别展开叙述。
1.消息存储整体架构
消息存储架构图中主要有下⾯三个跟消息存储相关的⽂件构成。
- CommitLog
消息主体以及元数据的存储主体,存储Producer端写⼊的消息主体内容,消息内容不是定⻓的。单个⽂件⼤⼩默认1G ,⽂件名⻓度为20位,左边补零,剩余为起始偏移量,⽐如00000000000000000000代表了第⼀个⽂件,起始偏移量为0,⽂件⼤⼩为1G=1073741824;当第⼀个⽂件写满了,第⼆个⽂件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写⼊⽇志⽂件,当⽂件满了,写⼊下⼀个⽂件;
- ConsumeQueue
消息消费队列,引⼊的⽬的主要是提⾼消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进⾏的,如果要遍历commitlog⽂件中根据topic检索消息是⾮常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息⼤⼩size和消息Tag的HashCode值。consumequeue⽂件可以看成是基于topic的commitlog索引⽂件,故consumequeue⽂件夹的组织⽅式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue⽂件采取定⻓设计,每⼀个条⽬共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息⻓度、8字节tag hashcode,单个⽂件由30W个条⽬组成,可以像数组⼀样随机访问每⼀个条⽬,每个ConsumeQueue⽂件⼤⼩约5.72M;
- IndexFile
IndexFile(索引⽂件)提供了⼀种可以通过key或时间区间来查询消息的⽅法。Index⽂件的存储位置是:$HOME \store\index${fileName},⽂件名fileName是以创建时的时间戳命名的,固定的单个IndexFile⽂件⼤⼩约为400M,⼀个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在⽂件系统中实现HashMap结构,故rocketmq的索引⽂件其底层实现为hash索引。
在上⾯的RocketMQ的消息存储整体架构图中可以看出,RocketMQ采⽤的是混合型的存储结构,即为Broker单个实例下所有的队列共⽤⼀个⽇志数据⽂件(即为CommitLog)来存储。RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于⼀个CommitLog中)针对Producer和Consumer分别采⽤了数据和索引部分相分离的存储结构,Producer发送消息⾄Broker端,然后Broker端使⽤同步或者异步的⽅式对消息刷盘持久化,保存⾄CommitLog中。只要消息被刷盘持久化⾄磁盘⽂件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当⽆法拉取到消息后,可以等下⼀次消息拉取,同时服务端也⽀持⻓轮询模式,如果⼀个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。这⾥,RocketMQ的具体做法是,使⽤Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引⽂件)数据。
2.⻚缓存与内存映射
⻚缓存(PageCache)是OS对⽂件的缓存,⽤于加速对⽂件的读写。⼀般来说,程序对⽂件进⾏顺序读写的速度⼏乎接近于内存的读写速度,主要原因就是由于OS使⽤PageCache机制对读写访问操作进⾏了性能优化,将⼀部分的内存⽤作PageCache。对于数据的写⼊,OS会先写⼊⾄Cache内,随后通过异步的⽅式由pdflush内核线程将Cache内的数据刷盘⾄物理磁盘上。对于数据的读取,如果⼀次读取⽂件时出现未命中PageCache的情况,OS从物理磁盘上访问读取⽂件的同时,会顺序对其他相邻块的数据⽂件进⾏预读取。
在RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取,在page cache机制的预读取作⽤下,Consume Queue⽂件的读性能⼏乎接近读内存,即使在有消息堆积情况下也不会影响性能。⽽对于CommitLog消息存储的⽇志数据⽂件来说,读取消息内容时候会产⽣较多的随机访问读取,严重影响性能。如果选择合适的系统IO调度算法,⽐如设置调度算法为“Deadline”(此时块存储采⽤SSD的话),随机读的性能也会有所提升。
另外,RocketMQ主要通过MappedByteBuffer对⽂件进⾏读写操作。其中,利⽤了NIO中的FileChannel模型将磁盘上的物理⽂件直接映射到⽤户态的内存地址中(这种Mmap的⽅式减少了传统IO将磁盘⽂件数据在操作系统内核地址空间的缓冲区和⽤户应⽤程序地址空间的缓冲区之间来回进⾏拷⻉的性能开销),将对⽂件的操作
转化为直接对内存地址进⾏操作,从⽽极⼤地提⾼了⽂件的读写效率(正因为需要使⽤内存映射机制,故RocketMQ的⽂件存储都使⽤定⻓结构来存储,⽅便⼀次将整个⽂件映射⾄内存)。
3.消息刷盘
- 同步刷盘
如上图所示,只有在消息真正持久化⾄磁盘后RocketMQ的Broker端才会真正返回给Producer端⼀个成功的ACK响应。同步刷盘对MQ消息可靠性来说是⼀种不错的保障,但是性能上会有较⼤影响,⼀般适⽤于⾦融业务应⽤该模式较多。
- 异步刷盘
能够充分利⽤OS的PageCache的优势,只要消息写⼊PageCache即可将成功的ACK返回给Producer端。消息刷盘采⽤后台异步线程提交的⽅式进⾏,降低了读写延迟,提⾼了MQ的性能和吞吐量.
九.集群核⼼概念
1.消息主从复制
RocketMQ官⽅提供了三种集群搭建⽅式。
- 2主2从异步通信⽅式
使⽤异步⽅式进⾏主从之间的数据复制,吞吐量⼤,但可能会丢消息。使⽤ conf/2m-2s-async ⽂件夹内的配置⽂件做集群配置。
- 2主2从同步通信⽅式
使⽤同步⽅式进⾏主从之间的数据复制,保证消息安全投递,不会丢失,但影响吞吐量使⽤ conf/2m-2s-sync ⽂件夹内的配置⽂件做集群配置。
- 2主⽆从⽅式
不存在复制消息,会存在单点故障,且读的性能没有前两种⽅式好。使⽤ conf/2m-noslave ⽂件夹内的配置⽂件做集群配置。
2.负载均衡
RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。
Producer的负载均衡
Producer端在发送消息的时候,会先根据Topic找到指定的TopicPublishInfo,在获取了TopicPublishInfo路由信息后,RocketMQ的客户端在默认⽅式下selectOneMessageQueue()⽅法会从TopicPublishInfo中的messageQueueList中选择⼀个队列(MessageQueue)进⾏发送消息。具体的容错策略均在MQFaultStrategy这个类中定义。这⾥有⼀个sendLatencyFaultEnable开关变量,如果开启,在随机递增取模的基础上,再过滤掉not available的Broker代理。所谓的"latencyFaultTolerance",是指对之前失败的,按⼀定的时间做退避。例如,如果上次请求的latency超过550Lms,就退避3000Lms;超过1000L,就退避60000L;如果关闭,采⽤随机递增取模的⽅式选择⼀个队列(MessageQueue)来发送消息,latencyFaultTolerance机制是实现消息发送⾼可⽤的核⼼关键所在。
Consumer的负载均衡
在RocketMQ中,Consumer端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,⽽在Push模式只是对pull模式的⼀种封装,其本质实现为消息拉取线程在从服务器拉取到⼀批消息后,然后提交到消息消费线程池后,⼜“⻢不停蹄”的继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟⼀下⼜继续拉取。在两种基于拉模式的消费⽅式(Push/Pull)中,均需要Consumer端在知道从Broker端的哪⼀个消息队列—队列中去获取消息。因此,有必要在Consumer端来做负载均衡,即Broker端中多个MessageQueue分配给同⼀个ConsumerGroup中的哪些Consumer消费。
Consumer的负责均衡可以通过consumer的api进⾏设置:
consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragelyByCircle());
- AllocateMachineRoomNearby :基于机房近侧优先级的代理分配策略。可以指定实际的分配策略。如果任何使⽤者在机房中活动,则部署在同⼀台机器中的代理的消息队列应仅分配给这些使⽤者。否则,这些消息队列可以与所有消费者共享,因为没有活着的消费者可以垄断它们
- AllocateMessageQueueAveragely:平均哈希队列算法
- AllocateMessageQueueAveragelyByCircle:循环平均哈希队列算法
- AllocateMessageQueueByConfig:不分配,通过指定MessageQueue列表来消费
- AllocateMessageQueueByMachineRoom:机房哈希队列算法,如⽀付宝逻辑机房
- AllocateMessageQueueConsistentHash:⼀致哈希队列算法,带有虚拟节点的⼀致性哈希环。
注意,在MessageQueue和Consumer之间⼀旦发⽣对应关系的改变,就会触发rebalance,进⾏重新分配。
3.消息重试
⾮⼴播模式下,Consumer消费消息失败后,要提供⼀种重试机制,令消息再消费⼀次。Consumer消费消息失败通常可以认为有以下⼏种情况:
- 由于消息本身的原因,例如反序列化失败,消息数据本身⽆法处理(例如话费充值,当前消息的⼿机号被注销,⽆法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,⽽这条失败的消息即使⽴刻重试消费,99%也不成功,所以最好提供⼀种定时重试机制,即过10秒后再重试。
- 由于依赖的下游应⽤服务不可⽤,例如db连接不可⽤,外系统⽹络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应⽤sleep 30s,再消费下⼀条消息,这样可以减轻Broker重试消息的压⼒。
在代码层⾯,如果消费者返回的是以下三种情况,则消息会重试消费:
consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { System.out.println("收到的消息:"+msg); } //第一种 return null return null; //第二种 直接返回重试状态 return ConsumeConcurrentlyStatus.RECONSUME_LATER; //第三种 抛出异常 } });
关于重试次数
RocketMQ会为每个消费组都设置⼀个Topic名称为“%RETRY%+consumerGroup”的重试队列(这⾥需要注意的是,这个Topic的重试队列是针对消费组,⽽不是针对每个Topic设置的),⽤于暂时保存因为各种异常⽽导致Consumer端⽆法消费的消息。考虑到异常恢复起来需要⼀些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越⼤。RocketMQ对于重试消息的处理是先保存⾄Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进⾏Delay后重新保存⾄“%RETRY%+consumerGroup”的重试队列中。
与延迟队列的设置相同,消息默认会重试16次,每次重试的时间间隔如下:
10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
重试超过指定次数的消息,将会进⼊到死信队列中
%DLQ%my-consumer-group1 。
4.死信队列
死信队列⽤于处理⽆法被正常消费的消息。当⼀条消息初次消费失败,消息队列会⾃动进⾏消息重试;达到最⼤重试次数后,若消费依然失败,则表明消费者在正常情况下⽆法正确地消费该消息,此时,消息队列 不会⽴刻将消息丢弃,⽽是将其发送到该消费者对应的特殊队列中。
RocketMQ将这种正常情况下⽆法被消费的消息称为死信消息(Dead-LetterMessage),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在RocketMQ中,可以通过使⽤console控制台对死信队列中的消息进⾏重发来使得消费者实例再次进⾏消费。
死信队列具备以下特点:
- RocketMQ会⾃动为需要死信队列的ConsumerGroup创建死信队列。
- 死信队列与ConsumerGroup对应,死信队列中包含该ConsumerGroup所有相关topic的死信消息。
- 死信队列中消息的有效期与正常消息相同,默认48⼩时。
- 若要消费死信队列中的消息,需在控制台将死信队列的权限设置为6,即可读可写。
5.幂等消息
幂等性:多次操作造成的结果是⼀致的。对于⾮幂等的操作,幂等性如何保证?
1.在请求⽅式中的幂等性的体现
get:多次get 结果是⼀致的
post:添加,⾮幂等
put:修改:幂等,根据id修改
delete:根据id删除,幂等
对于⾮幂等的请求,我们在业务⾥要做幂等性保证。
2.在消息队列中的幂等性体现
消息队列中,很可能⼀条消息被冗余部署的多个消费者收到,对于⾮幂等的操作,⽐如⽤户的注册,就需要做幂等性保证,否则消息将会被重复消费。可以将情况概括为以下⼏种:
- ⽣产者重复发送:由于⽹络抖动,导致⽣产者没有收到broker的ack⽽再次重发消息,实际上broker收到了多条重复的消息,造成消息重复
- 消费者重复消费:由于⽹络抖动,消费者没有返回ack给broker,导致消费者重试消费。
- rebalance时的重复消费:由于⽹络抖动,在rebalance重分配时也可能出现消费者重复消费某条消息。
3.如何保证幂等性消费
- mysql 插⼊业务id作为主键,主键是唯⼀的,所以⼀次只能插⼊⼀条
- 使⽤redis或zk的分布式锁(主流的⽅案)
十.RocketMQ最佳实践
1.保证消息顺序消费
1.为什么要保证消息有序
⽐如有这么⼀个物联⽹的应⽤场景,IOT中的设备在初始化时需要按顺序接收这样的消息:
- 设置设备名称
- 设置设备的⽹络
- 重启设备使配置⽣效
如果这个顺序颠倒了,可能就没有办法让设备的配置⽣效,因为只有重启设备才能让配置⽣效,但重启的消息却在设置设备消息之前被消费。
2.如何保证消息顺序消费
- 全局有序:消费的所有消息都严格按照发送消息的顺序进⾏消费
- 局部有序:消费的部分消息按照发送消息的顺序进⾏消费
2.快速处理积压消息
在rocketmq中,如果消费者消费速度过慢,⽽⽣产者⽣产消息的速度⼜远超于消费者消费消息的速度,那么就会造成⼤量消息积压在mq中。
1.如何查看消息积压的情况
在console控制台中可以查看
2.如何解决消息积压
- 在这个消费者中,使⽤多线程,充分利⽤机器的性能进⾏消费消息。
- 通过业务的架构设计,提升业务层⾯消费的性能。
- 创建⼀个消费者,该消费者在RocketMQ上另建⼀个主题,该消费者将poll下来的消息,不进⾏消费,直接转发到新建的主题上。新建的主题配上多个MessageQueue,多个MessageQueue再配上多个消费者。此时,新的主题的多个分区的多个消费者就开始⼀起消费了。
3.保证消息可靠性投递(重要!!!)
保证消息可靠性投递,⽬的是消息不丢失,可以顺利抵达消费者并被消费。要想实现可靠性投递,需要完成以下⼏个部分。
1.⽣产者发送事务消息
参考四.事务消息 保障了发送事务的可靠性
2.broker集群使⽤Dledger⾼可⽤集群
dledger集群的数据同步由两阶段完成
- 第⼀阶段:同步消息到follower,消息状态是uncommitted。follower在收到消息以后,返回⼀个ack给leader,leader⾃⼰也会返回ack给⾃⼰。leader在收到集群中的半数以上的ack后开始进⼊到第⼆阶段。
- 第⼆阶段:leader发送committed命令,集群中的所有的broker把消息写⼊到⽇志⽂件中,此时该消息才表示接收完毕。
3.保证消费者的同步消费
消费者使⽤同步的⽅式,在消费完后返回ack。
4.使⽤基于缓存中间件的MQ降级⽅案
当MQ整个服务不可⽤时,为了防⽌服务雪崩,消息可以暂存于缓存中间件中,⽐如redis。待MQ恢复后,将redis中的数据重新刷进MQ中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?