RocketMQ:一篇学会RocketMQ

MQ概述

MQ,Message Queue,是一种提供消息队列服务的中间件,也称为消息中间件,是一套提供了消息生产、存储、消费全过程API的软件系统。消息即数据。一般消息的体量不会很大。
知道是消息队列,他的规律是先进先出就行,光听概念没啥体验的。

MQ的作用

MQ的最重要的3个作用如下:

限流削峰

MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮。
image
说个简单的比喻:全班50个同学同时找老师问问题,老师肯定一时顶不住啊。这时候就需要有个中间人,比如说班长,大家的问题都可以发到班长那里,后面老师可根据自身的情况从班长那处理同学们处理的问题

异步解耦

上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。
image
异步解耦和限流削峰不一样的是请求的人群;除了用户发送的请求可以先储存在MQ上,系统间的调用也涉及请求发送,也理应可用mq进行中间储存。
举个简单的例子:假设A是用户,B是服务员,C是大厨。A发起请求要一份菜,B请求C去做菜,但由于C还做其他用户的菜,不能马上做A的菜,这时候A和B在干等着。这样肯定是不行,所以得有个中间人D,B告诉D做啥菜,D记起来,C到时候根据D的记录做就完了呗

数据收集

分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此类数据收集是最好的选择。
这时候的MQ的角色就相当于 数据库的功能了。

RocketMQ安装和测试

Rocket安装

下载地址:https://archive.apache.org/dist/rocketmq/4.2.0/rocketmq-all-4.2.0-bin-release.zip

环境搭建

window环境的搭建参考:https://www.ydlclass.com/doc21xnv/distribute/rocketmq/#_2-环境搭建
这里说下遇到的坑:

  1. 下载完的rocketmq-all-4.2.0-bin-release.zip所在的父级及上级文件夹不要有空格
  2. 如果遇到【service not available now, maybe disk full, CL: 0.95 CQ: 0.95 INDEX: 0.95, maybe your broker mach】,请删除掉C盘/用户/store文件夹/commitlog文件夹下的文件,然后重新启动name Server和broker即可。

实战

单生产者单消费者消息发送(OneToOne)

生产者

public class Producer {
    public static void main(String[] args) throws Exception {
        /**
         1. 谁来发?
         2. 发给谁?
         3. 怎么发?
         4. 发什么?
         5. 发的结果是什么?
         6. 打扫战场
         **/

        //1.创建一个发送消息的对象Producer
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.设定发送的命名服务器地址
        producer.setNamesrvAddr("localhost:9876");
        //3.1启动发送的服务
        producer.start();
        //4.创建要发送的消息对象,指定topic,指定内容body
        Message msg = new Message("topic1", "hello rocketmq".getBytes("UTF-8"));
        //3.2发送消息
        SendResult result = producer.send(msg);
        System.out.println("返回结果:" + result);
        //5.关闭连接
        producer.shutdown();
    }
}

消费者

public class Consumer {
    public static void main(String[] args) throws Exception {
        /**
         1. 谁来发?
         2. 发给谁?
         3. 怎么发?
         4. 发什么?
         5. 发的结果是什么?
         6. 打扫战场
         **/

        //1.创建一个接收消息的对象Consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.设定接收的命名服务器地址
        consumer.setNamesrvAddr("localhost:9876");
        //3.设置接收消息对应的topic,对应的sub标签为任意
        consumer.subscribe("topic1","*");
        //3.开启监听,用于接收消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //遍历消息
                for (MessageExt msg : list) {
                    System.out.println("收到消息:"+msg);
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //4.启动接收消息的服务
        consumer.start();
        System.out.println("接受消息服务已经开启!");
        //5 不要关闭消费者!
    }
}
  1. 生产者和消费者topic需要同一个,不然消费者监听不到这个消息。

单生产者多消费者消息发送(OneToMany)

生产者

		//1.创建一个发送消息的对象Producer
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.设定发送的命名服务器地址
        producer.setNamesrvAddr("localhost:9876");
        //3.1启动发送的服务
        producer.start();
        for (int i = 0; i < 10; i++) {
            //4.创建要发送的消息对象,指定topic,指定内容body
            Message msg = new Message("topic1", ("hello rocketmq"+i).getBytes("UTF-8"));
            //3.2发送消息
            SendResult result = producer.send(msg);
            System.out.println("返回结果:" + result);
        }
        //5.关闭连接
        producer.shutdown();

消费者之负载均衡模式

消费者1和消费者2(使用同一代码)

//1.创建一个接收消息的对象Consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.设定接收的命名服务器地址
        consumer.setNamesrvAddr("localhost:9876");
        //3.设置接收消息对应的topic,对应的sub标签为任意
        consumer.subscribe("topic1","*");
        //3.开启监听,用于接收消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //遍历消息
                for (MessageExt msg : list) {
                    System.out.println("收到消息:"+msg);
                    System.out.println("消息是:"+new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //4.启动接收消息的服务
        consumer.start();
        System.out.println("接受消息服务已经开启!");

        //5 不要关闭消费者!

PS:广播模式:多个消费者能共享同一个topic中的消息;负载均衡模式:多个消费者竞争同一个topic中的消息。
同一个组内的 消费者,并且监听同一个topic,默认情况下是负载均衡模式。

消费者之广播模式

不同组广播

消费者1

//1.创建一个接收消息的对象Consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.设定接收的命名服务器地址
        consumer.setNamesrvAddr("localhost:9876");
        //3.设置接收消息对应的topic,对应的sub标签为任意
        consumer.subscribe("topic1","*");
        //3.开启监听,用于接收消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //遍历消息
                for (MessageExt msg : list) {
                    System.out.println("收到消息:"+msg);
                    System.out.println("消息是:"+new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //4.启动接收消息的服务
        consumer.start();
        System.out.println("接受消息服务已经开启!");

        //5 不要关闭消费者!

消费者2

//1.创建一个接收消息的对象Consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group2");
        //2.设定接收的命名服务器地址
        consumer.setNamesrvAddr("localhost:9876");
        //3.设置接收消息对应的topic,对应的sub标签为任意
        consumer.subscribe("topic1","*");
        //3.开启监听,用于接收消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //遍历消息
                for (MessageExt msg : list) {
                    System.out.println("收到消息:"+msg);
                    System.out.println("消息是:"+new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //4.启动接收消息的服务
        consumer.start();
        System.out.println("接受消息服务已经开启!");

        //5 不要关闭消费者!

默认情况下,监听同一topic,不同消费组的消费者会共享topic,属于广播模式。

消费者1和消费者2(组内广播)

上面有提到过,组内默认消费模式是 负载均衡,如果想要组内广播,需要设置消费模式,如下:

 //1.创建一个接收消息的对象Consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.设定接收的命名服务器地址
        consumer.setNamesrvAddr("localhost:9876");
        //3.设置接收消息对应的topic,对应的sub标签为任意
        consumer.subscribe("topic1","*");
        //设置当前消费者的消费模式为广播模式(默认模式:负载均衡)
        consumer.setMessageModel(MessageModel.BROADCASTING);
        //3.开启监听,用于接收消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //遍历消息
                for (MessageExt msg : list) {
                    System.out.println("收到消息:"+msg);
                    System.out.println("消息是:"+new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //4.启动接收消息的服务
        consumer.start();
        System.out.println("接受消息服务已经开启!");

        //5 不要关闭消费者!

消息发送方式

同步消息

使用场景:即时性很强,发送消息后,需要马上拿到结果,进行后续的逻辑判断的。
上面演示的例子全是同步消息的发送,只要返回值是SendResult都是,如下:

SendResult result = producer.send(msg);

异步消息

使用场景:即时性较弱,发送消息后,不需要马上拿到结果,可进行后续的逻辑代码的。等结果拿到后再进行方法回调。
异步消息拿到结果的方式在于回调方法,如下:

        //1.创建一个发送消息的对象Producer
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.设定发送的命名服务器地址
        producer.setNamesrvAddr("localhost:9876");
        //3.1启动发送的服务
        producer.start();
        for (int i = 0; i < 10; i++) {
            //4.创建要发送的消息对象,指定topic,指定内容body
            Message msg = new Message("topic1", ("hello rocketmq"+i).getBytes("UTF-8"));
            //3.2 同步消息
            //SendResult result = producer.send(msg);
            //System.out.println("返回结果:" + result);

            //异步消息
            producer.send(msg, new SendCallback() {
                //表示成功返回结果
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.println(sendResult);
                }
                //表示发送消息失败
                @Override
                public void onException(Throwable throwable) {
                    System.out.println(throwable);
                }
            });
            
            System.out.println("消息"+i+"发完了,做业务逻辑去了!");
        }
        //休眠10秒
        TimeUnit.SECONDS.sleep(10);
        //5.关闭连接
        producer.shutdown();

注意:在异步发送消息后进行方法回调,保证生产者得活着,如果关闭了生产者则会默认调用失败回调的方法。

单向消息

使用场景:对返回结果不关心。

producer.sendOneway(msg);

延时消息

使用场景:希望消费者在某段时间后才能进行消费。

Message msg = new Message("topic3",("延时消息:hello rocketmq "+i).getBytes("UTF-8"));
//设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
msg.setDelayTimeLevel(3);
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
//1s为第1登记,5s为第2登记,以此类推

注意:这里的延迟发送是延迟发到broker上面。

批量消息

使用场景:发送一次,需要携带批量消息的情况

        List<Message> msgList = new ArrayList<Message>();
        Message msg1 = new Message("topic1", ("hello rocketmq1").getBytes("UTF-8"));
        Message msg2 = new Message("topic1", ("hello rocketmq2").getBytes("UTF-8"));
        Message msg3 = new Message("topic1", ("hello rocketmq3").getBytes("UTF-8"));

        msgList.add(msg1);
        msgList.add(msg2);
        msgList.add(msg3);


        SendResult result = producer.send(msgList);

消息过滤

tag过滤

tag:可以理解二级的topic;topic为一级目录,tag为二级目录。
而tag过滤其实很简单,比如说生产者发到 topic 下的 tag1,那么消费者在进行订阅的时候,可以有如下的写法:

consumer.subscribe("topic","*");//topic下的所有topic都进行消费
consumer.subscribe("topic6","tag1");//只消费topic下的tag1
consumer.subscribe("topic6","tag1 || tag2");//消费topic下的tag1或者tog2

属性过滤(SQL过滤)

属性过滤针对的是消息的属性。是的,你没有听错,消息也可以拥有自己的属性,而属性过滤是对消息属性进行筛选出消息的。
前提:需要在broker.conf配置enablePropertyFilter=true

//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
consumer.subscribe("topic7", MessageSelector.bySql("age >= 18"));
consumer.subscribe("topic6", MessageSelector.bySql("name = 'litiedan and age = 16'"));

SpringBoot-RocketMQ

生产者-消费者

配置文件

rocketmq.name-server=localhost:9876
rocketmq.producer.group=demo_producer

生产者

@RestController
public class DemoProducers {
    @Autowired
    private RocketMQTemplate template;

    @RequestMapping("/producer")
    public String producersMessage() {
        User user = new User("sharfine", "123456789");
        template.convertAndSend("demo-topic", user);
		//template.convertAndSend("demo-topic:tag1", user);
        return JSON.toJSONString(user);
    }
}

总结:生产者最重要就是发消息,实际开发我们的目的并不是去创建生产者,设置一堆的参数...而是直接拿到一个发送的工具类进行使用,这个工具就是RocketMQTemplate

消费者

@Service
@RocketMQMessageListener(topic = "demo-topic", consumerGroup = "demo_consumer")
public class DemoConsumers1 implements RocketMQListener<user> {
    @Override
    public void onMessage(user user) {
        System.out.println("Consumers1接收消息:" + demoEntity.toString());
    }
}

总结:如果你看过原生的消费者写法,会发现消费者中最重要,变化的那部分就是监听器...所以我们只需要创建监听器,编写监听后的业务逻辑就可以。

其余在springboot中配置

消息发送方式

同步发送

rocketMQTemplate.syncSend("topic1", basicMessage, 2000);

异步发送

       rocketMQTemplate.asyncSend("topic9", user, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println(sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println(throwable);
            }
        });

单向发送

rocketMQTemplate.sendOneWay("topic9",user);

批量发送

List<Message> msgList = new ArrayList<>();
msgList.add(new Message("topic6", "tag1", "msg1".getBytes()));
msgList.add(new Message("topic6", "tag1", "msg2".getBytes()));
msgList.add(new Message("topic6", "tag1", "msg3".getBytes()));       
rocketMQTemplate.syncSend("topic8",msgList,1000);

延时发送

rocketMQTemplate.syncSend("topic9", MessageBuilder.withPayload("test delay").build(),2000,2);

消息过滤

Tag过滤

@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",selectorExpression = "tag1 || tag2")

SQL过滤

@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1"
        ,selectorType= SelectorType.SQL92),selectorExpression = "age>18"

消息模式

@RocketMQMessageListener(topic = "topic9",consumerGroup = "group1",messageModel = MessageModel.BROADCASTING)

顺序消息发送

大家有没有注意到一个问题:就是我们在编写简单生产者和消费者的时候,会生产一个问题:

生产者按顺序发送消息,消费者消费时并不按顺序

产生这个问题的原因:topic里默认有4个队列,在发过去的时候是轮询方式进入到4个队列里的;然后消费者在消费的时候,也还是随机的从队列来消费的。随机进,随机出,这就导致了消息的顺序错乱。

在某些业务场景下,消息的消费顺序是不可乱的。因此我们要保证消息的消费顺序是正确的;这个得从两方面入手:

  1. 保证同一个体(比如同一订单)的业务步骤消息在同一队列中;比如1号订单,有创建,付款,完成3个步骤的消息,保证这3条消息都发送到topic的同一队列上
  2. 消费者这边要有顺序性的进行消费。消息谁先到先消费

生产者

保证第1点:

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        List<OrderStep> orderList = new Producer().buildOrders();

        //设置消息进入到指定的消息队列中
        for (final OrderStep order : orderList) {
            Message msg = new Message("topic1", order.toString().getBytes());
            //发送时要指定对应的消息队列选择器
            SendResult result = producer.send(msg, new MessageQueueSelector() {
                //设置当前消息发送时使用哪一个消息队列
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    //根据发送的信息不同,选择不同的消息队列
                    //根据id来选择一个消息队列的对象,并返回->id得到int值
                    long orderId = order.getOrderId();
                    long mqIndex = orderId % list.size();
                    return list.get((int) mqIndex);
                }
            }, null);
            System.out.println(result);
        }

消费者

保证消费顺序是有序的

         //使用单线程的模式从消息队列中取数据,一个线程绑定一个消息队列
		consumer.registerMessageListener(new MessageListenerOrderly() {
            //使用MessageListenerOrderly接口后,对消息队列的处理由一个消息队列多个线程服务,转化为一个消息队列一个线程服务
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                for (MessageExt msg : list) {
                    System.out.println(Thread.currentThread().getName()+"。消息:" + new String(msg.getBody())+"。queueId:"+msg.getQueueId());
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

事务

事务消息流程

事务消息只发生在生产者,与消费者无关!目的是为了保证消息不丢失,下面的流程图:
image

  1. 发送half消息(把消息发过去了,但暂时不允许被消费)
  2. 返回ok已接收到消息
  3. 执行本地事务,将消息保存一份
  4. 本地提交或回滚,同时告诉broker提交或回滚;如果提交,则消费者可消费,如果回滚,则broker丢弃此消息

事务补偿

如果第2步执行,但第4步迟迟没有响应。则会进行事务补偿,补偿过程:

  1. broker询问生产者事务结果
  2. 生产者检测本地事务状态
  3. 根据事务状态提交或回滚

生产者发送事务消息

//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
producer.setNamesrvAddr("localhost:9876");
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//这里编写将数据保存到数据库,然后返回提交或回滚,进而告诉broker结果
return LocalTransactionState.COMMIT_MESSAGE;
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
//检查数据的保存情况,然后返回提交或回滚,进而告诉broker结果
return null;
}
});
producer.start();
Message msg = new Message("topic8",("事务消息:hello rocketmq ").getBytes("UTF-8"));
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("返回结果:"+result);
producer.shutdown();

与之前编写的生产者代码有3点不同:

  1. 使用的生产者不同
  2. 有本地事务的监听器
  3. 发送消息方法不同

RocketMQ高级特性(了解即可,代码中可能体现不出来)

消息存储特性

image
上图为生产者-broker-消费者的一次消息的互动过程。在这个过程中,假设broker宕机了,会发生啥呢?

(1) 发送成功,此时broker宕机,(2)未发送;此时生产者会继续发送消息,导致消息重复发送
(3) 执行这个,此时broker宕机,(4)未执行到;后面broker恢复了,发现迟迟没有(4)的反馈,就继续发消息给消费者,也造成消息重发

rocketmq是这样解决上面的问题的:
image
(图中数据库DB可看成文件系统)

  1. 接收到生产者的消息后,先在文件系统保存一份
  2. 即使宕机然后恢复过来,生产者继续发送,查询文件系统中是否已存在,避免收到多条重复消息
  3. 由于broker宕机,(5)未执行成功;当broker恢复过来后,会重新执行(4)(5)(6)

死信队列

image
image

消息重复消费

消息重复消费有时候是不可避免的,但我们要做到业务幂等。即多次消息消费后的结果,与一次消费结果的相同。这样才能保证业务不错乱
image

posted @ 2022-07-14 16:25  爱编程DE文兄  阅读(352)  评论(0编辑  收藏  举报