什么是RocketMQ?
消息中间件
可拓展的高效的分布式消息队列系统
主要四个部分组(NameServer,Producer,Consumer,Broker)
1.1 解决的问题
微服务之间的解耦
削峰填谷 (流量缓冲)
数据分发
1.2 常见的消息中间件
ActiveMQ : 基于JMS规范的消息中间件
Kafka : 用于大数据量,吞吐量百万级,异步刷盘,容易丢数据,日志消息,监控数据
RocketMQ : 吞吐量十万级,同步刷盘,异步刷盘,非日志的可靠消息传输
RabbitMQ : 基于AMQP协议的消息中间件,吞吐量万级,同步刷盘,非日志的可靠消息传输
1.3 核心概念
Producer (消息生产者) : 生产消息
NameServer (命名服务) : 类似注册中心,将Broker注册到NameServer上,生产者和消费者后续通过BrokerID从NameServer中查找对应的Broker
Broker (代理服务器) : 消息接收和存储
Topic (主题) : 一个主题可以有多个队列,可以区分不同业务类型的消息
Message (消息) : 存储消息的对象
MessageQueue (消息队列) : 存储数据,通过队列的方式进行存储
Consumer (消息消费者) : 进行消息的消费
Tag (标签过滤) : 消息的过滤
尝试RocketMQ编码
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
创建消息生产者
创建生产者 producer (DefaultMQProducer)
定义 NameServer 地址
启动生产者与 broker 建立连接
消息发送的目的地,定义 Topic
释放资源
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:22
* 生产者
*/
public class Producer
{
public static void main(String[] args) throws Exception
{
// 创建发送者
DefaultMQProducer producer = new DefaultMQProducer("helloGroup");
// 定义 NameServer 地址
producer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 开启
producer.start();
// 定义 Topic
String topic = "HelloTopic";
for (int i = 0; i < 10; i++)
{
// 发送消息到 Topic
Message message = new Message(topic,"hello".getBytes());
SendResult result = producer.send(message);
System.out.println("消息ID: " + result.getMsgId() + "发送状态" + result.getSendStatus());
}
// 释放资源,真实开发不需要
producer.shutdown();
}
}
消费者
创建消费者 consumer
定义 NameServer 地址
获取什么消息,从 Topic 查找
订阅该主题
定义消息的监听器
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:38
* 消费者
*/
public class Consumer
{
public static void main(String[] args) throws MQClientException
{
// 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("helloConsumerGroup");
// NameServer 地址
consumer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 主题
String topic = "HelloTopic";
// 订阅主题 * 是后面标签过滤的
consumer.subscribe(topic,"*");
// 获取消息,监听
// MessageListenerConcurrently 多线程,并行执行消费
consumer.setMessageListener(new MessageListenerConcurrently()
{
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext)
{
// 获取消息
for (MessageExt messageExt : list)
{
System.out.println("线程: " + Thread.currentThread() + "消息内容: " + new String(messageExt.getBody()));
}
// 返回消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
}
}
消息发送的三种方式
同步消息
发送方会在收到服务器的响应后才会继续执行下面的代码,保证消息的可靠性
异步消息
发送方发送消息后不会等待服务器的响应,而是立即继续执行下面的代码,有可能会出现消息丢失的情况
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:22
* 生产者
*/
public class Producer
{
public static void main(String[] args) throws Exception
{
// 创建发送者
DefaultMQProducer producer = new DefaultMQProducer("helloGroup");
// 定义 NameServer 地址
producer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 开启
producer.start();
// 定义 Topic
String topic = "HelloTopic";
// 异步发送消息
Message msg = new Message(topic,("异步消息").getBytes());
System.out.println("开始: " + Thread.currentThread());
producer.send(msg, new SendCallback()
{
// 发送异步消息
@Override
public void onSuccess(SendResult sendResult)
{
// 另外一个线程
System.out.println("异步线程: " + Thread.currentThread());
System.out.println("消息发送结果: " + sendResult.getSendStatus());
}
// 发送异步消息失败
@Override
public void onException(Throwable throwable)
{
throwable.printStackTrace();
}
});
System.out.println("结束: " + Thread.currentThread());
}
}
一次性消息
发送方发送消息后不会等待服务器的响应,也不会收到服务器的响应,适用于如日志收集
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:22
* 生产者
*/
public class Producer
{
public static void main(String[] args) throws Exception
{
// 创建发送者
DefaultMQProducer producer = new DefaultMQProducer("helloGroup");
// 定义 NameServer 地址
producer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 开启
producer.start();
// 定义 Topic
String topic = "HelloTopic";
// 一次性发送消息
Message msg = new Message(topic,("一次性消息").getBytes());
producer.sendOneway(msg);
producer.shutdown();
System.out.println("发送完毕");
}
}
如何保证消息的可靠性?
![](https://i-blog.csdnimg.cn/blog_migrate/c865def95416595179d470ff8c175ddb.png)
我们可以使用同步刷盘,保证消息的可靠性,在Linux里的brock.conf配置文件,将默认异步刷盘改为同步刷盘
flushDiskType = ASYNC_FLUSH
异步刷盘(默认),接收到客户端的消息的存储请求之后,把消息写入操作系统 PageCache 缓存中
这个时候就可以响应给消息生产者了,如果这个时候崩了,就会出现丢失数据,但是异步的性能更快
(异步)操作系统将 PageCache 数据持久化到磁盘中
flushDiskType = SYNC_FLUSH
同步刷盘,接收到客户端的消息的存储请求之后,把消息存储到磁盘之后响应给消息生产者
延迟消息
![](https://i-blog.csdnimg.cn/blog_migrate/63176aa11c9da45a7713b58887c89c3b.png)
生产者发送一条延时消息,消费者会在延时时间后进行消费
// 延时消息
Message message = new Message(topic, ("一次性消息" + new Date()).getBytes());
message.setDelayTimeLevel(3);
// 一次性消息
producer.sendOneway(message);
消息消费的两种模式
集群模式 (默认)
![](https://i-blog.csdnimg.cn/blog_migrate/b62883996095e74833f12092c1bcc29c.png)
消费者采用负载均衡,多个消费者同时消费队列里的信息,每个消费者处理的信息都不一样
广播模式
![](https://i-blog.csdnimg.cn/blog_migrate/45e4f9935ceb2021565b2b05cc283b0c.png)
多个消费者都会消费到消息队列的消息,消费的消息都是同样的
// 消息消费模式广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
消息生产的执行
![](https://i-blog.csdnimg.cn/blog_migrate/0acfb84878e2e36598ed169e3bae2f95.png)
在消费者里面有一个index,每次发送完消息进入消息队列就会对index++
消息队列都有一个index编号
一般计算的公式为 index % 队列长度
消息过滤
6.1 Tag过滤
![](https://i-blog.csdnimg.cn/blog_migrate/d0950116926f4a9defb6124526237402.png)
生产者,给消息添加Tag
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:22
* 生产者消息过滤
*/
public class Producer
{
public static void main(String[] args) throws Exception
{
// 创建发送者
DefaultMQProducer producer = new DefaultMQProducer("tagFilterGroup");
// 定义 NameServer 地址
producer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 开启
producer.start();
// 定义 Topic
String topic = "tagFilter";
// 标签过滤
Message msg1 = new Message(topic, ("标签过滤1消息").getBytes());
Message msg2 = new Message(topic, ("标签过滤2消息").getBytes());
Message msg3 = new Message(topic, ("标签过滤3消息").getBytes());
msg1.setTags("Tag1");
msg2.setTags("Tag2");
msg3.setTags("Tag3");
// 一次性消息
producer.sendOneway(msg1);
producer.sendOneway(msg2);
producer.sendOneway(msg3);
producer.shutdown();
System.out.println("发送完毕时间: " + new Date());
}
}
消费者,使用consumer.subscribe(topic,"Tag1 || Tag2"); 过滤Tag
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:38
* 消费者标签过滤
*/
public class Consumer
{
public static void main(String[] args) throws MQClientException
{
// 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("helloConsumerGroup");
// NameServer 地址
consumer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 主题
String topic = "tagFilter";
// 订阅主题 筛选A和C
consumer.subscribe(topic,"Tag1 || Tag2");
// 消息消费模式广播模式
consumer.setMessageModel(MessageModel.CLUSTERING);
// 获取消息,监听
// MessageListenerConcurrently 多线程,并行执行消费
consumer.setMessageListener(new MessageListenerConcurrently()
{
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext)
{
// 获取消息
for (MessageExt messageExt : list)
{
System.out.println(
"线程: " + Thread.currentThread() +
"消息内容: " + new String(messageExt.getBody()) + "" +
"消费时间: " + new Date());
}
// 返回消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
}
}
6.2 SQL92过滤
![](https://i-blog.csdnimg.cn/blog_migrate/f3fcc08f2b385ff1e41c3aeb03bed318.png)
生产者给用户设置属性 message.putUseProperty();
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:22
* 生产者消息过滤
*/
public class Producer
{
public static void main(String[] args) throws Exception
{
// 创建发送者
DefaultMQProducer producer = new DefaultMQProducer("sql92FilterGroup");
// 定义 NameServer 地址
producer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 开启
producer.start();
// 定义 Topic
String topic = "sql92Filter";
// 标签过滤
Message msg1 = new Message(topic, ("SQL92过滤1消息").getBytes());
Message msg2 = new Message(topic, ("SQL92过滤2消息").getBytes());
Message msg3 = new Message(topic, ("SQL92过滤3消息").getBytes());
msg1.putUserProperty("name","小赤");
msg1.putUserProperty("age","18");
msg2.putUserProperty("name","小蓝");
msg2.putUserProperty("age","30");
msg3.putUserProperty("name","小绿");
msg3.putUserProperty("age","60");
// 一次性消息
producer.sendOneway(msg1);
producer.sendOneway(msg2);
producer.sendOneway(msg3);
producer.shutdown();
System.out.println("发送完毕时间: " + new Date());
}
}
消费者添加过滤 MessageSelector.bySql()
/**
* @author: 南瓜战士
* @create-date: 2023/3/12 20:38
* 消费者标签过滤SQL92
*/
public class Consumer
{
public static void main(String[] args) throws MQClientException
{
// 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("sql92ConsumerGroup");
// NameServer 地址
consumer.setNamesrvAddr("你部署的RocketMQ的IP地址:9876");
// 主题
String topic = "sql92Filter";
// 订阅主题 SQL92过滤
consumer.subscribe(topic, MessageSelector.bySql("name = '小蓝' AND age > 20"));
// 消息消费模式广播模式
// consumer.setMessageModel(MessageModel.CLUSTERING);
// 获取消息,监听
// MessageListenerConcurrently 多线程,并行执行消费
consumer.setMessageListener(new MessageListenerConcurrently()
{
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext)
{
// 获取消息
for (MessageExt messageExt : list)
{
System.out.println(
"线程: " + Thread.currentThread() +
"消息内容: " + new String(messageExt.getBody()) + "" +
"消费时间: " + new Date());
}
// 返回消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
}
}
集成SpringBoot
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
添加配置文件
rocketmq.name-server=你部署的RocketMQ的IP地址:9876
rocketmq.producer.group=my-group
7.1 普通发送消息
/**
* @author: 南瓜战士
* @create-date: 2023/3/13 13:12
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void test1() {
Message<String> message = MessageBuilder.withPayload("同步消息Boot").build();
// 传入主题名,message,这个msg是Spring的
rocketMQTemplate.syncSend("helloBoot",message);
}
}
7.2 发送延时消息
生产者
/**
* 发送延时消息
*/
@Test
public void test2() {
Message<String> message = MessageBuilder.withPayload("延时消息消息Boot").build();
// 传入主题名,message,这个msg是Spring的
rocketMQTemplate.syncSend("helloBoot",message,3000,3);
System.out.println("发送延时消息的时间: " + new Date());
TimeUnit.SECONDS.sleep(5);
}
消费者
/**
* @author: 南瓜战士
* @create-date: 2023/3/13 14:51
* 消费者
*/
@Component
// 消费者集群,广播消费模式
//@RocketMQMessageListener(consumerGroup = "consumerBootGroup",topic = "helloBoot",messageModel = MessageModel.BROADCASTING)
@RocketMQMessageListener(consumerGroup = "consumerBootGroup",topic = "helloBoot")
public class HelloBootListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
System.err.println("消息内容: " + new String(messageExt.getBody()) + ",消费的时间: " + new Date());
}
}
7.3 消息过滤Tag
生产者
/**
* 消息过滤
*/
@Test
public void test3() throws InterruptedException {
Message<String> msg1 = MessageBuilder.withPayload("消息过滤Boot").build();
Message<String> msg2 = MessageBuilder.withPayload("消息过滤Boot").build();
Message<String> msg3 = MessageBuilder.withPayload("消息过滤Boot").build();
// 打上标签 Tag
rocketMQTemplate.sendOneWay("helloFilterBoot:TagA",msg1);
rocketMQTemplate.sendOneWay("helloFilterBoot:TagB",msg2);
rocketMQTemplate.sendOneWay("helloFilterBoot:TagC",msg3);
}
消费者,添加注解selectorExpression() 过滤Tag
@RocketMQMessageListener(consumerGroup = "tagFilterGroup",topic = "helloFilterBoot",selectorExpression = "TagA || TagB")
7.4 消息过滤SQL92
生产者
/**
* SQL92
*/
@Test
public void test4() throws InterruptedException {
Message<String> msg1 = MessageBuilder.withPayload("消息过滤A").setHeader("age","11").build();
Message<String> msg2 = MessageBuilder.withPayload("消息过滤B").setHeader("age","12").build();
Message<String> msg3 = MessageBuilder.withPayload("消息过滤C").setHeader("age","13").build();
// 打上标签 Tag
rocketMQTemplate.sendOneWay("helloFilterSQL92Boot",msg1);
rocketMQTemplate.sendOneWay("helloFilterSQL92Boot",msg2);
rocketMQTemplate.sendOneWay("helloFilterSQL92Boot",msg3);
}
消费者,添加 selectorType = SelectorType.SQL92,selectorExpression = "") 注解
@RocketMQMessageListener(consumerGroup = "SQL92FilterBoot",topic = "helloFilterSQL92Boot",selectorType = SelectorType.SQL92,selectorExpression = "age > 11")
7.5 顺序消息
发送消息和消费消息的顺序是一样的
不过消费者是多线程消费,所以消费的时候会出现抢占CPU的情况,就会出现导致生产和消费的顺序不一致的情况
局部有序
生产消息和消费消息的顺序一致,但是如果消息分散在不同队列,消费消息的时候就会出现与生产消息不一致的情况
全局有序
生产消息和消费消息的顺序一致
生产者,定义消息选择器
/**
* 顺序消息
*/
@Test
public void test5() throws InterruptedException {
List<OrderStep> orderSteps = OrderUtil.buildOrders();
// 设置消息选择器,通过索引最终确定消息发送到哪里去
rocketMQTemplate.setMessageQueueSelector(new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, org.apache.rocketmq.common.message.Message message, Object o) {
String orderIdStr = (String) o;
long id = Long.parseLong(orderIdStr);
int index = (int) (id % list.size());
// 返回消息队列索引
return list.get(index);
}
});
// 发送消息
for (OrderStep step : orderSteps) {
// 设置消息
Message<String> msg = MessageBuilder.withPayload(step.toString()).build();
// 发送顺序消息
rocketMQTemplate.sendOneWayOrderly("orderlyTopicBoot",msg,String.valueOf(step.getOrderId()));
}
消费者,设置consumeMode = ConsumeMode.ORDERLY顺序消息
/**
* @author: 南瓜战士
* @create-date: 2023/3/14 16:22
* 顺序消息消费者
*/
@Component
@RocketMQMessageListener(topic = "orderlyTopicBoot",consumerGroup = "orderlyTopicGroup",consumeMode = ConsumeMode.ORDERLY)
public class OrderlyTopicBootListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt messageExt) {
System.out.println("线程: " + Thread.currentThread() +
",消费的队列ID: " + messageExt.getQueueId() +
",消息的内容: " + new String(messageExt.getBody()));
}
}
8. 不同消费组对同一主题消费的情况说明
![](https://i-blog.csdnimg.cn/blog_migrate/6ea3d2992a8068c76cc8963943fc4fdc.png)
积分集群,此时多了个订单服务
不会对1,2,3进行重复消费,因为逻辑不同,积分服务和订单服务的逻辑不一样
默认在MQ中,磁盘存储72小时,有文件记录
记录了每个消费组的消费的位置,如果后续有其他消费者加入进来,就会按照记录时间加载到哪个位置的消息
9. 消息重试 & 死信队列
![](https://i-blog.csdnimg.cn/blog_migrate/7e6b96870d5ba0eb13c02d98831fee25.png)
消费者接收到消息的时候,需要执行业务逻辑,如果执行失败返回失败结果
MQ会重发给消费者,默认会有16次的重试
如果重试16次都没有成功,会进入死信队列
有这样的情况,消息发送给消费者,消息正常消费,返回(ACK)的时候,MQ没有收到应答(可能出现网络问题)
然后会进行重试,业务重复执行
9.1 (面试题) 怎么去保证消费者消费消息成功信息?
使用消息重试,默认有16次,如果都没有成功,会进入死信队列,然后进行人工处理