RocketMQ学习

  1. 什么是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 (标签过滤) : 消息的过滤

  1. 尝试RocketMQ编码

  1. 添加依赖

<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.4.0</version>
        </dependency>
  1. 创建消息生产者

  1. 创建生产者 producer (DefaultMQProducer)

  1. 定义 NameServer 地址

  1. 启动生产者与 broker 建立连接

  1. 消息发送的目的地,定义 Topic

  1. 释放资源

/**
 * @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();
    }
}
  1. 消费者

  1. 创建消费者 consumer

  1. 定义 NameServer 地址

  1. 获取什么消息,从 Topic 查找

  1. 订阅该主题

  1. 定义消息的监听器

/**
 * @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();
    }
}

  1. 消息发送的三种方式

  1. 同步消息

发送方会在收到服务器的响应后才会继续执行下面的代码,保证消息的可靠性

  1. 异步消息

发送方发送消息后不会等待服务器的响应,而是立即继续执行下面的代码,有可能会出现消息丢失的情况

/**
 * @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());
    }
}
  1. 一次性消息

发送方发送消息后不会等待服务器的响应,也不会收到服务器的响应,适用于如日志收集

/**
 * @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("发送完毕");
    }
}

  1. 如何保证消息的可靠性?

我们可以使用同步刷盘,保证消息的可靠性,在Linux里的brock.conf配置文件,将默认异步刷盘改为同步刷盘

  • flushDiskType = ASYNC_FLUSH

  • 异步刷盘(默认),接收到客户端的消息的存储请求之后,把消息写入操作系统 PageCache 缓存中

  • 这个时候就可以响应给消息生产者了,如果这个时候崩了,就会出现丢失数据,但是异步的性能更快

  • (异步)操作系统将 PageCache 数据持久化到磁盘中

  • flushDiskType = SYNC_FLUSH

  • 同步刷盘,接收到客户端的消息的存储请求之后,把消息存储到磁盘之后响应给消息生产者

  1. 延迟消息

生产者发送一条延时消息,消费者会在延时时间后进行消费

        // 延时消息
        Message message = new Message(topic, ("一次性消息" + new Date()).getBytes());
        message.setDelayTimeLevel(3);
        // 一次性消息
        producer.sendOneway(message);

  1. 消息消费的两种模式

  1. 集群模式 (默认)

消费者采用负载均衡,多个消费者同时消费队列里的信息,每个消费者处理的信息都不一样

  1. 广播模式

多个消费者都会消费到消息队列的消息,消费的消息都是同样的

        // 消息消费模式广播模式
        consumer.setMessageModel(MessageModel.BROADCASTING);

  1. 消息生产的执行

在消费者里面有一个index,每次发送完消息进入消息队列就会对index++

消息队列都有一个index编号

一般计算的公式为 index % 队列长度

  1. 消息过滤

6.1 Tag过滤

生产者,给消息添加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过滤

生产者给用户设置属性 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();
    }
}

  1. 集成SpringBoot

  1. 添加依赖

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>
  1. 添加配置文件

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. 不同消费组对同一主题消费的情况说明

积分集群,此时多了个订单服务

不会对1,2,3进行重复消费,因为逻辑不同,积分服务和订单服务的逻辑不一样

默认在MQ中,磁盘存储72小时,有文件记录

记录了每个消费组的消费的位置,如果后续有其他消费者加入进来,就会按照记录时间加载到哪个位置的消息

9. 消息重试 & 死信队列

  • 消费者接收到消息的时候,需要执行业务逻辑,如果执行失败返回失败结果

  • MQ会重发给消费者,默认会有16次的重试

  • 如果重试16次都没有成功,会进入死信队列

有这样的情况,消息发送给消费者,消息正常消费,返回(ACK)的时候,MQ没有收到应答(可能出现网络问题)

然后会进行重试,业务重复执行

9.1 (面试题) 怎么去保证消费者消费消息成功信息?

使用消息重试,默认有16次,如果都没有成功,会进入死信队列,然后进行人工处理

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值