文章目录
1 环境准备
- 新建一个moudel进行演示: 03-rocket-mq-producer
- 导入依赖
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- logback-classic包含logback-core依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
2 顺序消息
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序
或者全局有序
。
顺序消费的原理解析,
在默认的情况下消息发送会采取Round Robin轮询方式
把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序
。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
2.1 API介绍
- 发送顺序消息的接口
/****
* 第一个参数:message 就是要发送的消息
* 第二个参数MessageQueueSelector: 定义如何选择消息队列
* 第三个参数:Object类型,可以理解就是业务参数,会传给MessageQueueSelector接口中select方法的第三个参数
* 用于队列的选择
*/
SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg)
- MessageQueueSelector 接口
从名字就能看出该接口的作用消息队列选择器
public interface MessageQueueSelector {
/***
* List<MessageQueue> mqs: 就是消息队列的list集合,也就是topic下的消息队列的集合,默认大小是4
* Message message: 就是要发送的消息
* Object o: 见上面的解释
* 返回值:就是选择的队列
*/
MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}
- 消息消费的时候需要使用
MessageListenerOrderly
这个监听器
2.2 演示
下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成
。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
- 消息生产者
package study.wyy.mq.rocket.producer;
import lombok.extern.slf4j.Slf4j;
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 org.slf4j.Logger;
import study.wyy.mq.rocket.model.OrderStep;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* @author: wyaoyao
* @date: 2020-12-25 14:14
* @description: 顺序消息发送
*/
@Slf4j
public class OrderedMessageProducer {
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
// 构造订单数据
List<OrderStep> orderSteps = buildData();
// 1 构建生产者
DefaultMQProducer producer = new DefaultMQProducer("test_producer_group");
// 2 设置name server
producer.setNamesrvAddr("localhost:9876");
// 3 启动
producer.start();
// 遍历数据发送
for (OrderStep orderStep:orderSteps) {
Message message = new Message("myTopic", "myTags", orderStep.toString().getBytes(Charset.defaultCharset()));
/****
* 第一个参数:message 就是要发送的消息
* 第二个参数MessageQueueSelector: 定义如何选择消息队列
* 第三个参数:Object类型,可以理解就是业务参数,会传给MessageQueueSelector接口中select方法的第三个参数
* 用于队列的选择,比如这里就把orderId塞进去
*/
SendResult send = producer.send(message, new MessageQueueSelector() {
/***
* List<MessageQueue> mqs: 就是消息队列的list集合,也就是topic下的消息队列的集合,默认大小是4
* Message message: 就是要发送的消息
* Object o: 见上面的解释
* 返回值:就是选择的队列
*/
@Override
public MessageQueue select(List<MessageQueue> mqs, Message message, Object o) {
// 强转
Long orderId = (Long) o;
// 对orderId进行取余数,余数相同的放到一个队列中
long index = orderId % mqs.size();
MessageQueue select = mqs.get((int) index);
log.info("队列的大小: {}; orderID: {}; select: {}",mqs.size(), orderId,select.getQueueId());
return select;
}
}, orderStep.getOrderId());
}
producer.shutdown();
}
private static List<OrderStep> buildData(){
List<OrderStep> orderList = new ArrayList<OrderStep>();
OrderStep orderDemo = new OrderStep();
orderDemo.setOrderId(1L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(2L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(3L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(2L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(3L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(2L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1L);
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(3L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
return orderList;
}
}
package study.wyy.mq.rocket.model;
import lombok.Data;
/**
* @author: wyaoyao
* @date: 2020-12-25 14:01
* @description: 订单的流程
*/
@Data
public class OrderStep {
private long orderId;
private String desc;
@Override
public String toString() {
return this.getOrderId() + "-->" + this.getDesc();
}
}
- 消息消费
package study.wyy.mq.rocket.consumer;
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.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @author: wyaoyao
* @date: 2020-12-25 15:11
* @description:
*/
public class OrderedMessageConsumer {
public static void main(String[] args) throws MQClientException {
// 1 构建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_consumer_group");
// 2 设置 name server地址
consumer.setNamesrvAddr("localhost:9876");
/**
* 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
* 如果非第一次启动,那么按照上次消费的位置继续消费
*
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 3 设置订阅topic
consumer.subscribe("myTopic", "*");
// 4 注册回调函数,注意这里注册的是顺序监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for (MessageExt msg : msgs) {
// 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
System.out.println("consumeThread=" + Thread.currentThread().getName() + ", queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
}
}
- 测试结果: 每个订单都是按照订单流程进行的
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->创建
consumeThread=ConsumeMessageThread_3, queueId=3, content:3-->创建
consumeThread=ConsumeMessageThread_2, queueId=2, content:2-->创建
consumeThread=ConsumeMessageThread_2, queueId=2, content:2-->付款
consumeThread=ConsumeMessageThread_3, queueId=3, content:3-->付款
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->付款
consumeThread=ConsumeMessageThread_3, queueId=3, content:3-->完成
consumeThread=ConsumeMessageThread_2, queueId=2, content:2-->完成
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->推送
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->完成
3 延时消息
比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
3.1 API介绍
- 在构建消息的时候可以设置一个延时属性
// 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
message.setDelayTimeLevel(3);
现在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";
3.2 演示
- 消息生产者
package study.wyy.mq.rocket.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.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.nio.charset.Charset;
/**
* @author: wyaoyao
* @date: 2020-12-25 15:53
* @description: 延时消息发送
*
*/
public class ScheduledMessageProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 1 创建生产者
DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
// 2 设置 name server地址
producer.setNamesrvAddr("localhost:9876");
// 3 启动
producer.start();
// 4 构建消息
Message message = new Message();
message.setTopic("myTopic");
message.setTags("myTags");
// 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
message.setDelayTimeLevel(3);
message.setBody("我的第一个延时消息".getBytes(Charset.defaultCharset()));
producer.send(message);
producer.shutdown();
}
}
- 消费者
package study.wyy.mq.rocket.consumer;
import lombok.extern.slf4j.Slf4j;
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.nio.charset.Charset;
import java.util.List;
/**
* @author: wyaoyao
* @date: 2020-12-25 16:06
* @description: 延时消息消费者
*/
@Slf4j
public class ScheduledMessageConsumer {
public static void main(String[] args) throws MQClientException {
// 1 定义消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
// 2 设置 name server地址
consumer.setNamesrvAddr("localhost:9876");
// 3 设置订阅的主题
consumer.subscribe("myTopic","*");
// 4 设置回调函数
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(" ======遍历消息======= ");
if (msgs != null && msgs.size() > 0) {
for (MessageExt msg : msgs) {
System.out.println("消息id: " + msg.getMsgId());
System.out.println("topic: " + msg.getTopic());
System.out.println("tag: " + msg.getTags());
System.out.println("消息体:"+ new String(msg.getBody(), Charset.defaultCharset()));
System.out.println("=======end===========");
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5 启动
consumer.start();
}
}
- 测试
- 先启动消费者等待消息
- 启动生产者发送消息
- 十秒后消费者收到消息
4 批量消息
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic
,相同的waitStoreMsgOK
,而且不能是延时消息
。此外,这一批消息的总大小不应超过4MB
。
package study.wyy.mq.rocket.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.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* @author: wyaoyao
* @date: 2020-12-25 16:24
* @description:
*/
public class BatchMessageProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 1 创建生产者
DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
// 2 设置 name server地址
producer.setNamesrvAddr("localhost:9876");
// 3 启动
producer.start();
// 4 构建消息
List<Message> messages = new ArrayList<>();
Message message = new Message();
message.setTopic("myTopic");
message.setTags("myTags");
message.setBody("批量消息1".getBytes(Charset.defaultCharset()));
messages.add(message);
message = new Message();
message.setTopic("myTopic");
message.setTags("myTags");
message.setBody("批量消息2".getBytes(Charset.defaultCharset()));
messages.add(message);
producer.send(messages);
producer.shutdown();
}
}
5 事务消息
5.1 事务消息的状态
事务消息共有三种状态,提交状态、回滚状态、中间状态:
- TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
5.2 事务消息的流程
上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
- HalfI(prepare)Message
指的是暂不能投递的消息
,发送方已经将消息成功发送到MQ服务器(消息中心)
,但是服务器未收到对生产者对该消息的二次确认
,此时消息被标记为”暂不能投递“状态,处于状态的消息及半消息 - Message Status Check
由于网络闪断,生产者应用重启等原因,导致某条消息的二次确认丢失,MQ服务端会扫描发现某条消息长期处于”半消息状态“,需要主动向消息生产者询问该消息的最终状态
(commit或者rollback),该过程称之消息回查
执行大致流程:
- 发送方向MQ服务端发送消息
- MQ Server将消息持久化成功之后,向发送方ACK确认消息已经发送成功,此时消息为半消息
- 发送方开始执行本地事务逻辑
- 发送方根据本地事务执行的结果,向MQ Server提交二次确认(commit或者rollback),MQ Server收到Commit状态则将半消息标记为可投递,订阅方(消费方)最终将收到该消息,Mq Server收到Rollback状态,则删除半消息,订阅方不会收到该消息
- 在断网或者应用重启的特殊情况下,上诉步骤4的二次确认会最终未到达MQ server,经过固定的时间后,Mq server将对该消息发起消息回查
- 发送方收到消息回查之后,需要检查对相应消息的的本地事务执行的最终结果
- 发送方根据检查到的结果,再次提交二次确认呢,MQ Server扔按照步骤4进行半消息操作
5.3 API介绍
当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务。它返回前一节中提到的三个事务状态之一。checkLocalTransaction 方法用于检查本地事务状态,并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。
- 事务的监听接口
package org.apache.rocketmq.client.producer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
public interface TransactionListener {
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
LocalTransactionState checkLocalTransaction(final MessageExt msg);
}
- 生产者要使用TransactionMQProducer ,具体使用看下面的案例
5.4 演示
- 事务消息的生产者
package study.wyy.mq.rocket.producer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import study.wyy.mq.rocket.spi.TransactionListenerImpl;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
/**
* @author: wyaoyao
* @date: 2020-12-25 17:06
* @description: 事务消息
*/
public class TransactionMessageProducer {
// 记录事务的状态,key为事务的id,value为事务状态
private static Map<String,LocalTransactionState> STATE_MAP = new HashMap<String, LocalTransactionState>();
public static void main(String[] args) throws MQClientException {
// 1 创建消息发送者,注意这里是事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer();
// 2 设置 生产者组
producer.setProducerGroup("test-producer-group");
// 3 设置name sever
producer.setNamesrvAddr("localhost:9876");
// 4 设置事务监听器
producer.setTransactionListener(new TransactionListener(){
// 记录事务的状态,key为事务的id,value为事务状态
/*****
* 当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务。
* 也就是执行具体的业务逻辑
* 返回三个事务状态之一。
* - TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
* - TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
* - TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
LocalTransactionState localTransactionState= null;
try{
// 执行本地的事务逻辑
System.out.println("执行本地的事务逻辑");
System.out.println("arg: " + arg);
String s = new String(msg.getBody(), Charset.defaultCharset());
System.out.println("message: " + s);
// 模拟出现异常
int a = 1/0;
// 没有异常就返回可以提交
localTransactionState = LocalTransactionState.COMMIT_MESSAGE;
}catch (Exception e){
e.printStackTrace();
// 出现异常就回滚
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
}finally {
// 记录一下
STATE_MAP.put(msg.getTransactionId(),localTransactionState);
// 没有异常就返回可以提交
return localTransactionState;
}
}
/***
* 用于检查本地事务状态,并回应消息队列的检查请求。它也是返回三个事务状态之一。
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
return STATE_MAP.get(msg.getTransactionId());
}
});
// 5 启动
producer.start();
// 6 发送消息,注意发送消息的方法为sendMessageInTransaction
Message message = new Message();
message.setTopic("myTopic");
message.setBody("一条事务消息".getBytes(Charset.defaultCharset()));
// 第二个参数其他参数,会传到executeLocalTransaction的第二个参数
producer.sendMessageInTransaction(message,"事务消息测试");
// 还存在会查生产者的逻辑,生产者就不关闭了
// producer.shutdown();
}
}
- 消费者
package study.wyy.mq.rocket.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.nio.charset.Charset;
import java.util.List;
/**
* @author 20116651
* @description
* @date 2020/12/28 16:10
*/
public class TransactionMessageConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
consumer.setNamesrvAddr("localhost:9876");
//订阅消息,接收的是所有消息
consumer.subscribe("myTopic","*");
// 注册消息监听
consumer.registerMessageListener(new MessageListenerConcurrently(){
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(" ======遍历消息======= ");
if (msgs != null && msgs.size() > 0) {
for (MessageExt msg : msgs) {
System.out.println("消息id: " + msg.getMsgId());
System.out.println("topic: " + msg.getTopic());
System.out.println("tag: " + msg.getTags());
System.out.println("消息体:"+ new String(msg.getBody(), Charset.defaultCharset()));
System.out.println("=======end===========");
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
- 测试
- 当出现一场的时候消费者是不会拿到消息的
- 当没有异常的时候,消费者会成功打印出消息内容