RabbitMQ

1,初始MQ

MQ全称为Message Queue:消息队列。消息队列是在消息的传输过程中保存消息的容器。

它是典型的生产者,消费者模型。生产不断向消息队列中生产消息,消费者不断从队列中获取消息

因为消息的生产和消费是异步的,而且只关心消息的发送与接收,没有业务的侵入,解耦。

同步与异步

同步:指一个进程在执行某个请求时,若该请求需要一段时间才能返回,呢么该进程会一直等待,直到收到返回信息才会执行别的。事情必须一件一件做,前一件做完了才能做下一件。

例子:吃饭时候不能玩手机,吃完之后再去玩。

异步:指进程不需要一直等下去,继续执行下面操作,不管其他进程状态,当有消息返回时系统会通知进程进行处理,提高了执行的效率。

例子:边吃饭,边玩手机,边说话。

区别:请求发出后,是否需要等待响应,才能继续执行其他操作。

阻塞与非阻塞

阻塞:需要做一件事情能不能立即得到返回应答,如果不能立刻获得返回,需要等待,呢就阻塞了。进程或者线程就阻塞在哪里,不能做其他事情。

非阻塞:需要做一件事情能不能立即得到返回应答,如果不能立刻获得返回,需要等待,等待的时候还可以做其他事情,这是非阻塞。

概念

微服务:一个大项目拆分好的一个个小项目。

分布式:         

分布式架构:有人管理拆分好的小项目

以购买商品为例: 用户支付成功后,需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存准备发货。

在事件模式中,支付服务是事件发布者( publisher ),在支付完成后只需要发布一个支付成功的事件(event ),事件中带上订单 id 订单服务和物流服务是事件订阅者( Consumer ),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人( Broker )。发布者发布事件到Broker ,不关心谁来订阅事件。订阅者从 Broker 订阅事件,不关心谁发来的消息。
优势:
1,服务解耦合 :每个服务都可以灵活插拔,可替换
2,性能提升,吞吐量提高:无需等待订阅者处理完成,响应快速
3,服务没有强依赖,不担心级联失败问题
4,流量消峰: 不管发布事件的流量波动多大,都由Broker 接收,订阅者可以按照自己的速度去处理事件 
缺点:
1,架构复杂,业务没有明显的流程线,不好管理
2,需要依赖Broker的可靠,安全,性能

 2,安装RabbitMQ

//1,下载镜像  在线拉取
docker pull rabbitmq:3-management

//本地下载之后 进行加载
docker load -i mq.tar

//2,安装
docker run \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=123456 \
-v mq-plugins:/plugins \
--name mq \
--hostname zhaibx\
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3.9-management

 MQ基本结构

channel :操作 MQ 的工具 ( 通道 )
exchange:路由消息到队列中(交换机)
queue :缓存消息 (队列)
virtual host :虚拟主机,是对 queue exchange 等资源的逻辑分组(环境隔离)

3,RabbitMQ消息模型

环境搭建

Linux环境中安装rabbitMq 运行 rabbitMq 访问地址RabbitMQ Management进行登录,创建一个队列,生产消息与消费消息时需要该队列。

创建父子工程项目,用父工程依赖来管理子工程项目依赖,两个子工程 一个为Publisher生产消息,一个为Consumer消费消息。

1,基本消息队列

Publisher生产消息                                     消息队列                                   Consumer消费消息           

写法1:原始项目写法

生产信息

package com.zbx.mq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author zbx
 * @date 2023/5/6&10:57
 */
public class FirstMQ {
    public static void main(String[] args) throws IOException, TimeoutException {
        /**
         * 创建连接
         * 隔离环境
         * 获取连接
         * 声明队列
         * 发送消息
         * 关闭连接和channel
         */
        //1,创建连接
        ConnectionFactory factory = new ConnectionFactory();
        //隔离环境
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        factory.setPort(5672);
        factory.setHost("192.168.29.100");
        //获取连接
        Connection connection = factory.newConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //发送消息
        for (int i = 0; i <100; i++) {
            channel.basicPublish("","basic.queue",null,("张佳乐"+i).getBytes());
        }
        //释放资源
        channel.close();
        connection.close();
    }
}

生产信息工程配置文件 application.yaml

spring:
  rabbitmq:
    host: 192.168.29.100
    port: 5672
    password: 123456
    username: root
    virtual-host: /

消费信息 

package com.zbx.mq;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author zbx
 * @date 2023/5/6&11:13
 */
public class FirstMQ {
    public static void main(String[] args) throws IOException, TimeoutException {
        /**
         * 创建连接
         * 隔离环境
         * 获取连接
         * 声明队列
         * 发送消息
         * 关闭连接和channel
         */
        //1,创建连接
        ConnectionFactory factory = new ConnectionFactory();
        //隔离环境
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        factory.setPort(5672);
        factory.setHost("192.168.29.100");
        //获取连接
        Connection connection = factory.newConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //消费消息
        channel.basicConsume("basic.queue",true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("body = " + new String(body));
            }
        });
        //释放资源
        /*channel.close();
        connection.close();*/
    }
}

消费信息配置文件application.yaml

spring:
  rabbitmq:
    host: 192.168.29.100
    port: 5672
    password: 123456
    username: root
    virtual-host: /
    listener:
      simple:
        prefetch: 1
server:
  port: 8081

首先执行生产消息类的main方法,在网站中查看队列中是否有消息产生。

然后再执行消费信息类的main方法,打印查看消费信息主体信息,在网站中查看队列消息是否被消费。

写法2:使用Spring提供的RabbitTemplate来完成消息生产与消费

二者application.yaml配置文件写法不变

生产消息:发送一百条消息

@SpringBootTest
public class TestPublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void send(){
        for (int i = 0; i <100; i++) {
            //发送消息                     队列名
            rabbitTemplate.convertAndSend("basic.work.queue","xx"+i);
        }
    }
}

消费消息:

@Component
public class BasicQueueListener {
    //使用Spring提供的rabbit监听器 监听队列 
    @RabbitListener(queues = "basic.queue")
    public void receive(String message){
        System.out.println("message = " + message);
    }
}

使用Spring提供的rabbit模板 首先启动消费消息的项目,当队列有消息需要消费时,就会监听到,回调并打印输出。然后通过测试类来进行消息的生产。

2,工作消息队列

        publisher生产消息                队列                                                多个消费消息

 使用Spring提供的rabbit模板进行消息的生产与消费

生产消息

@SpringBootTest
public class TestPublisher {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void send(){
        for (int i = 0; i <100; i++) {
            rabbitTemplate.convertAndSend("basic.work.queue","xx"+i);
        }
    }
}

 生产消息配置文件application.yaml

spring:
  rabbitmq:
    host: 192.168.29.100
    port: 5672
    password: 123456
    username: root
    virtual-host: /

消费消息

@Component
public class WorkQueueListener {

    @RabbitListener(queues = "basic.work.queue")
    public void receive1(String message) throws InterruptedException {
        Thread.sleep(50);
        System.out.println("receive1 = " + message);
    }
    @RabbitListener(queues = "basic.work.queue")
    public void receive2(String message) throws InterruptedException {
        Thread.sleep(500);
        System.out.println("receive2 = " + message);
    }
    @RabbitListener(queues = "basic.work.queue")
    public void receive3(String message) throws InterruptedException {
        Thread.sleep(1000);
        System.out.println("receive3 = " + message);
    }
}

消费信息配置文件application.yaml

spring:
  rabbitmq:
    host: 192.168.29.100
    port: 5672
    password: 123456
    username: root
    virtual-host: /

server:
  port: 8081

使用Spring提供的rabbit模板进行消息生产与消费。首先运行消费信息的项目,同时有三个消费进行对该队列的监听,一旦队列中有消息来进行消费。然后使用生产消息的测试类进行生产消息,发送到队列中,然后三个消费同时进行消费,同时控制台打印消费信息。

面试题:怎么从队列中获取消息

当队列中消息量大时,默认走一个预分配策略,默认为250条消息,且有多个消费者时,每人都分配250条消息进行消费。

当队列中消息量少时,会进行消息均分,每个消费者消费的消息量都一样。

这两种情况都存在一些的问题,消息生产出来后分发至不同的消费者,消息虽然在队列中,但是消息已经被消费者占用。如果这个时候消费出现异常,会导致这一片消息出问题。

由于每个消费者性能不一致,有的处理快,有的处理慢,所以用一种分配策略,能者多劳,消费者在队列中拿一条消息,消费一条,不进行预分配。

配置能者多劳

listener:
      simple:
        prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息

 3,发布/订阅(交换机)

 订阅模型中,多了一个exchange角色,而且过程略有变化:

Exchange :交换机,图中的 X 。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例 如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange 的类型。Exchange 有以下 3 种类型:
1,Fanout :广播,将消息交给所有绑定到交换机的队列
2,Direct :定向,把消息交给符合指定 routing key 的队列
3,Topic :通配符,把消息交给符合 routing pattern (路由模式) 的队列

交换机作用:接收生产者产生的消息,将消息传递到队列中。如果没有队列与交换机绑定,则消息会丢失。FanoutExchange会将消息路由到每个绑定的队列。

案例1:广播:将消息交给所有绑定到交换机的队列

1,声明交换机 队列 以及绑定交换机与队列的关系

package com.zbx.configuration;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zbx
 * @date 2023/5/6&19:56
 * 定义交换机以及绑定交换机和队列
 */
@Configuration
public class FanoutBind {

    //声明交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanout");
    }

    //声明队列1
    @Bean
    public Queue queue1(){
        return new Queue("queue1");
    }

    //声明队列2
    @Bean
    public Queue queue2(){
        return new Queue("queue2");
    }

    //交换机与队列绑定关系  通过监听器 监听所有的队列 加载队列
    @Bean
    //此时 参数中的 queue1 与 fanoutExchange 是从ioc容器中获取的 这两个名字一定要与上面声明队列与声明交换机的名字一致
    //因为默认IOC是将方法名 转为对象名
    public Binding binding1(Queue queue1,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(queue1).to(fanoutExchange);
    }
    //交换机与队列绑定关系  通过监听器 监听所有的队列 加载队列
    @Bean
    //此时 参数中的 queue1 与 fanoutExchange 是从ioc容器中获取的 这两个名字一定要与上面声明队列与声明交换机的名字一致
    //因为默认IOC是将方法名 转为对象名
    public Binding binding2(Queue queue2,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(queue2).to(fanoutExchange);
    }
}

查看RabbitMQ Management网站中交换机是否声明出来,与队列的关系是否有绑定。

2,生产者生产消息

@SpringBootTest
public class FanoutTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testFanout(){
        for (int i = 0; i <10; i++) {
            //发送信息                    交换机名称  声明队列 不写代表广播  信息内容
            rabbitTemplate.convertAndSend("fanout",    "",             "哈哈哈"+i);
        }
    }
}

3,消费者消费信息

@Component
public class FanoutListener {

    @RabbitListener(queues = "queue1")
    public void receive1(String message){
        System.out.println("queue1 = " + message);
    }
    
    @RabbitListener(queues = "queue2")
    public void receive2(String message){
        System.out.println("queue2 = " + message);
    }
    
}
//打印结果
queue2 = 哈哈哈
queue1 = 哈哈哈

监听这两个队列是否有消息,进行打印。

当两个队列,同时增加消费者时

当消费者多了时,队列只有两个时,只会选择其中一个接收。

因为广播是从交换机广播到队列而不是广播到消费者。。

@Component
public class FanoutListener {
    
    @RabbitListener(queues = "queue1")
    public void receive1(String message){
        System.out.println("queue1 = " + message);
    }
    @RabbitListener(queues = "queue1")
    public void receive11(String message){
        System.out.println("queue11 = " + message);
    }
    @RabbitListener(queues = "queue2")
    public void receive2(String message){
        System.out.println("queue2 = " + message);
    }
    @RabbitListener(queues = "queue2")
    public void receive22(String message){
        System.out.println("queue22 = " + message);
    }
}
//打印结果
queue2 = 哈哈哈0
queue1 = 哈哈哈0
queue2 = 哈哈哈1
queue2 = 哈哈哈2
queue1 = 哈哈哈1
queue1 = 哈哈哈2
queue2 = 哈哈哈3
queue1 = 哈哈哈3
queue2 = 哈哈哈4
queue1 = 哈哈哈4
queue2 = 哈哈哈5
queue1 = 哈哈哈5
queue2 = 哈哈哈6
queue2 = 哈哈哈7
queue1 = 哈哈哈6
queue2 = 哈哈哈8
queue1 = 哈哈哈7
queue2 = 哈哈哈9
queue1 = 哈哈哈8
queue1 = 哈哈哈9

 声明队列Bean:Queue 交换机Bean:FanoutExchange 绑定关系Bean:Binding

 案例2:Direct:通过指定路由key的方式,由路由向队列发送消息时并指定路由key,将消息发送到带有指定key的队列中。

  生产者                        交换机        路由key            队列                     消费者

 生产者:

@Test
    public void testDirect(){
        //发送信息                              交换机名称  指定路由key名称  信息内容
        rabbitTemplate.convertAndSend("fanout","xx","哈哈哈");
    }

 通过测试类方式,向交换机发送消息,由交换机发送消息到指定路由key队列。

消费者:

@Component
public class DirectListener {
    //使用注解模式 对交换机与队列进行绑定 Direct Exchange:路由
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue1"),
            exchange = @Exchange(name ="fanout",type = ExchangeTypes.DIRECT),
            key ="xx"  //指定队列的路由key
    ))
    public void receive1(String message) {
        System.out.println("fanout.queue1.xx = " + message);
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue2"),
            exchange = @Exchange(name ="fanout",type = ExchangeTypes.DIRECT),
            key = "ww" //指定队列的路由key
    ))
    public void receive2(String message) {
        System.out.println("fanout.queue2.ww = " + message);
    }
}

//结果打印
fanout.queue1.xx = 哈哈哈

 总结:交换机通过路由key方式,将消息发送到指定的路由key队列中,如果配置多个队列相同的路由key,则功能与广播相同。

案例3:Topic 使用通配符规则方式,由交换机将消息发送到不同队列。是Fanout与Direct的结合

通配符规则:#:匹配一个或多个词,*:匹配刚好一个词

举例:item.#:能匹配item.123与item.123.123

item.*:只能匹配item.123

生产者:

@Test
    public void testTopic2(){
        //发送信息               交换机名称  指定路由key名称  信息内容   输出结果
        rabbitTemplate.convertAndSend("fanout","xx.123","哈哈哈");fanout.queue2.xx.# = 哈哈哈
        rabbitTemplate.convertAndSend("fanout","123.xx","哈哈哈");fanout.queue1.#.xx = 哈哈哈
        rabbitTemplate.convertAndSend("fanout","","哈哈哈");
        rabbitTemplate.convertAndSend("fanout","123.xx","哈哈哈");fanout.queue1.#.xx = 哈哈哈
    }

消费者:

@Component
public class TopicListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue1"),
            exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC),
            key = "#.xx"
    ))
    public void receive1(String message){
        System.out.println("fanout.queue1.#.xx = " + message);
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue2"),
            exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC),
            key = "xx.*"
    ))
    public void receive2(String message){
        System.out.println("fanout.queue2.xx.# = " + message);
    }
    
}

 总结:Topic为路由key与广播的结合。不指定key为广播。

消息转化器

当消息发送时,发送对象类型,消费者监听到之后,将消息进行消费打印。消息在队列中为json类型,消费时打印接收为String类型,转换会出错。配置json转化器。

//依赖引入
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

在项目的启动类下添加一个Bean即可。

@Bean
public MessageConverter jsonMessageConverter(){
    return new Jackson2JsonMessageConverter();
}

4,消息可靠性

在消息的发送过程中,会有情况导致消息丢失。

 生产者在发送过程中,消息未到达交换机。发送到交换机后,消息未发送到队列。

MQ宕机,队列将消息丢失。 消费者接收到消息后未消费就宕机。

生产者消息确认 

RabbitMQ 提供了 publisher confirm 机制来避免消息发送到 MQ 过程中丢失。消息发送到 MQ 以后,会返回 一个结果给发送者,表示消息是否处理成功。结果有两种请求:
1,publisher-confirm ,发送者确认 消息成功投递到交换机,返回ack (acknowledge - 答谢、告知已经收到 )
2,消息未投递到交换机,返回 nack publisher-return,发送者回执
消息投递到交换机了,但是没有路由到队列。返回 ACK ,及路由失败原因。
1,在生产者配置文件中添加
spring:
    rabbitmq:
        publisher-confirm-type: correlated
        publisher-returns: true
        template:
            mandatory: true
publish-confirm-type :开启 publisher-confirm(生产者确认机制) ,这里支持两种类型:
        simple:同步等待 confirm 结果,直到超时
        correlated:异步回调,定义 ConfirmCallback MQ 返回结果时会回调这个 ConfirmCallback
publish-returns:开启 publish-return(生产者消息回执) 功能,同样是基于 callback 机制,不过是定义 ReturnCallback
template.mandatory :定义消息路由失败时的策略。
        true,则调用ReturnCallback
        false:则直接丢弃消息

 2,生产者编写

@Test
    public void testTopic(){
        Student student = new Student(1,"123","123");
        //定义生产者消息确认异步回调对象
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //添加回调方法
        correlationData.getFuture().addCallback(result -> {
            //判断是否成功
            if(result.isAck()){
                //ack
                log.info("消息投递成功,消息id{}",correlationData.getId());
            }else{
                //nack
                log.warn("消息投递到交换机失败! 消息id{}",correlationData.getId());
            }
        },
        ex -> {
            log.error("消息发送失败!",ex);
        });
        //发送信息                 交换机名称  声明路由key  信息内容 回调对象
        rabbitTemplate.convertAndSend("fanout","",student,correlationData);
    }

3,配置全局的消息失败Callback方法

@Log4j2
@Configuration
public class PublisherReturnConf implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //获取rabbitTemplate对象
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        //配置ReturnCallback             发送消息                   是否路由    路由名称      路由key
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            System.out.println("message = " + message);
            System.out.println("getReplyCode = " + replyCode);
            System.out.println("ReplyText = " + replyText);
            System.out.println("Exchange = " + exchange);
            System.out.println("RoutingKey = " + routingKey);
            // 判断是否是延迟消息
            log.error("消息发送到队列失败,响应码:{}, 失败原因:{}, 交换机: {}, 路由key:{},消息: {}",
            replyCode, replyText, exchange, routingKey, message.toString());
            // 判断是否是延迟消息
            Integer receivedDelay = message.getMessageProperties().getReceivedDelay();
            if (receivedDelay != null && receivedDelay > 0) {
                // 是一个延迟消息,忽略这个错误提示
                return;
            }
        });
    }
}

     4,打印结果

//正确发送消息到路由
2023-05-08 20:25:32.771  INFO 22924 --- [168.29.100:5672] com.zbx.FanoutTest                       : 消息投递成功,消息id51f2c748-44e8-468e-9abc-51bc8b500c54
//正确发送到交换机与队列 并消费
fanout.queue2.xx.# = Student(id=1, name=123, hobbies=123)
//路由发送消息到队列失败
2023-05-08 20:25:32.771 ERROR 22924 --- [nectionFactory1] com.zbx.config.PublisherReturnConf       : 消息发送到队列失败,响应码:312, 失败原因:NO_ROUTE, 交换机: fanout, 路由key:,消息: (Body:'{"id":1,"name":"123","hobbies":"123"}' MessageProperties [headers={spring_returned_message_correlation=51f2c748-44e8-468e-9abc-51bc8b500c54, __TypeId__=com.zbx.pojo.Student}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])

交换机持久化

在查看交换机状态时,Features选中的D 为DURable持久的。

当我们自己创建交换机,并给他设置不持久化。

方式1:通过代码创建,通过SpringBoot启动加载到容器中

@Configuration
public class ExchangeEX {
    @Bean
    public TopicExchange topicExchange(){
        //代码创建交换机                                 交换机名称      持久化 默认 true  自动删除 true
        TopicExchange topicExchange = new TopicExchange("临时交换机",true,true);
        return topicExchange;
    }
}

方式2:在消费者中通过监听器创建交换机以及队列时进行配置

@Component
public class TopicListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue1"),
            exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC,durable = "true",autoDelete = "true"),
            key = "#.xx"
    ))
    public void receive1(Student message){
        System.out.println("fanout.queue1.#.xx = " + message.toString());
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue2"),
            exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC,durable = "true",autoDelete = "true"),
            key = "xx.*"
    ))
    public void receive2(Student message){
        System.out.println("fanout.queue2.xx.# = " + message.toString());
    }
}

我们创建的交换机,druable属性默认为true,为持久化的,注解也一样。

autoDelete表示当前交换机如果没有队列绑定是否自动删除,默认false为不删除。

不需要重启mq,设置了antoDelete ,只要解除了当前交换机所有队列的绑定,则交换机自动删除

队列持久化

代码创建队列

@Bean
    public Queue queue(){
        Queue queue = new Queue("队列",true,false,false);
        return queue;
    }

在消费者中通过监听器创建队列

@RabbitListener(bindings = @QueueBinding(
            //                                     是否持久化队列        是否为排它队列     自动删除
            value = @Queue(name = "fanout.queue1",durable = "false",exclusive = "false",autoDelete = "true"),
            exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC,durable = "true",autoDelete = "true"),
            key = "#.xx"
    ))

排它队列:当消费者与该队列进行绑定时,该队列只允许该消费者连接,第二次连接或者其他消费者连接时,不允许连接。

但是会导致原队列消息丢失。

消息持久化

在发送消息前对消息进行持久化配置

 @Test
    public void testTopic(){
        Student student = new Student(1,"123","123");
        //定义生产者消息确认异步回调对象
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //添加回调方法
        correlationData.getFuture().addCallback(result -> {
            //判断是否成功
            if(result.isAck()){
                //ack
                log.info("消息投递成功,消息id{}",correlationData.getId());
            }else{
                //nack
                log.warn("消息投递到交换机失败! 消息id{}",correlationData.getId());
            }
        },
        ex -> {
            log.error("消息发送失败!",ex);
        });
        String ms = "12312312313";
        //持久化消息配置
        Message message = MessageBuilder.withBody(ms.getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
                .build();
        //发送信息                              交换机名称  声明队列 不写代表广播  信息内容
        rabbitTemplate.convertAndSend("fanout","xx.123",message,correlationData);
    }

发送消息代码封装,方便与业务处理

@Log4j2
public class RabbitTemplateConfig {
    //   外部传入          交换机名称            路由key          编号                                       信息
    public void confirm(String exchange, String routingKey, String id,RabbitTemplate rabbitTemplate, String message){
        //定义生产者消息确认异步回调对象
        CorrelationData correlationData = new CorrelationData(id);
        //添加回调方法
        correlationData.getFuture().addCallback(result -> {
                    //判断是否成功
                    if(result.isAck()){
                        //ack
                        log.info("消息投递成功,消息id{}",id);
                    }else{
                        //nack
                        log.warn("消息投递到交换机失败! 消息id{}",id);
                    }
                },
                ex -> {
                    log.error("消息发送失败!",ex);
                });
        //持久化消息配置
        Message ms = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
                //.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
                .build();
        //发送信息                              交换机名称  声明队列 不写代表广播  信息内容
        rabbitTemplate.convertAndSend(exchange,routingKey,ms,correlationData);
    }
}

业务层调用

@Service
public class PubService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void pay() {
        RabbitTemplateConfig rabbitTemplateConfig = new RabbitTemplateConfig();
        rabbitTemplateConfig.confirm("topic","", UUID.randomUUID().toString(),this.rabbitTemplate,"666666");
    }
}

消费者信息确认

当消息在生产者方面以及交换机队列方面正常的话,如果消费者操作错误也会导致消息丢失。

RabbitMQ 支持消费者确认机制,即:消费者处理消息后可以向 MQ 发送 ack 回执, MQ 收到 ack 回执后才会删 除该消息。如果你处理过程中宕机,那么mq 因为没有接收 ack 状态也会等待你重启之后再次将消息投递给 你,这样就保证了消息一定会被消费。
SpringAMQP 则允许配置三种确认模式:
1. manual :手动 ack ,需要在业务代码结束后,调用 api 发送 ack
2. auto :自动 ack ,由 spring 监测 listener 代码是否出现异常,没有异常则返回 ack ;抛出异常则返
nack (AOP 环绕 )
3. none :关闭 ack MQ 假定消费者获取消息后会成功处理,因此消息投递后立即被删除
在配置文件中配置
 listener:
      simple:
        prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
        acknowledge-mode: auto //自动ACK 自动监听代码是否有异常 返回ack与nack

消费失败重试机制

当消费者出现异常后,消息会不断 requeue (重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue入队列 ,无限循环,导致 mq 的消息处理飙升,带来不必要的压力。
我们可以利用 mq retry 机制,在消费者出现异常时利用本地重试,而不是无限制的 requeue mq 队列
首先在配置文件中配置
spring:
  rabbitmq:
    host: 192.168.29.100
    port: 5672
    password: 123456
    username: root
    virtual-host: fanout
    listener:
      simple:
        prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
        acknowledge-mode: auto
        retry:
          enabled: true #开启重试机制
          initial-interval: 1000
          # 初始化的失败等待时长为1秒
          multiplier: 2
          #下次重试的时长倍数,下次等待时长 multiplier * last-interval111/24 8
          max-attempts: 3
          #最大重试次数
          stateless: true # 无状态 false 有状态,如果业务中包含事务,必须是false这样在重试的时候保留事务不失效

消费者失败消息处理策略:将消息发送到专属的处理错误消息的交换机以及绑定的队列中。

由MessageRecoverer接口处理。

@Configuration
@Log4j2
public class DLXBindListener {
    //绑定错误消息的交换机以及队列
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "error.queue"),
                    exchange =@Exchange(value = "error.direct",type =
                            ExchangeTypes.DIRECT),
                    key = {"error"}
            ))
    //监听消费 错误队列中的消息
    public void error(String message){
        log.debug("叮,苦逼的运维你好,你有新的消息要人工处理了{}",message);
    }
    //消息处理
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }
}

当异常消息时,默认是丢弃消息,我们通过MessageRecoverer将消息保存至处理异常的交换机队列中,由人工处理。

保证RabbitMQ消息可靠性:

1. 开启生产者确认机制,确保生产者的消息能到达队列
2. 开启持久化功能,确保消息未消费前在队列中不会丢失
3. 开启消费者确认机制为 auto ,由 spring 确认消息处理成功后完成 ack
4. 开启消费者失败重试机制,并设置 MessageRecoverer ,多次重试失败后将消息投递到异常交换机,交由人工处理

5,死信交换机

当一个队列中消息出现以下情况时,成为死信

1,消息被消费者reject或者返回nack

2,消息超时未消费

3,队列满了

如果该队列中配置了dead-lletter-exchange属性,制定了一个交换机,呢么这个队列中的死信就会投递到这个交换机中,这个交换机称为死信交换机。

非注解形式创建

Queue queue = QueueBuilder.durable().deadLetterExchange("123123").deadLetterRoutingKey("ww").build();

注解形式创建

@RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1",arguments = {
                    //配置死信交换机以及队列 和routing-key
                    @Argument(name = "x-dead-letter-exchange",value = "dlx"),
                    @Argument(name = "x-dead-letter-routing-key",value = "dlx.key")
            }),
            exchange = @Exchange(name = "topic",type = ExchangeTypes.TOPIC)
    ))
    public void receive(String message){
        System.out.println("topic.queue1 = " + message);
    }

死信交换机实际就是一个普通定义的交换机和队列。只是在普通的队列创建时设置当消息出现错误时将消息投递到死信交换机。

TTL:消息存活时间,当一个队列中的消息TTL结束仍未消费,会变为死信。

给队列设置消息超时时间

@RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1",arguments = {
                    //配置死信交换机以及队列 和routing-key
                    @Argument(name = "x-dead-letter-exchange",value = "dlx"),
                    @Argument(name = "x-dead-letter-routing-key",value = "dlx.key"),
                    //配置队列消息超时时间  
                    @Argument(name = "x-message-ttl",value = "10000",type = "java.lang.Integer"),
            }),
            exchange = @Exchange(name = "topic",type = ExchangeTypes.TOPIC)
    ))
    public void receive(String message){
        System.out.println("topic.queue1 = " + message);
    }

发送消息时给消息设置超时时间

@Log4j2
public class RabbitTemplateConfig {
    //   外部传入          交换机名称            路由key          编号                                       信息
    public void confirm(String exchange, String routingKey, String id,RabbitTemplate rabbitTemplate, String message){
        //定义生产者消息确认异步回调对象
        CorrelationData correlationData = new CorrelationData(id);
        //添加回调方法
        correlationData.getFuture().addCallback(result -> {
                    //判断是否成功
                    if(result.isAck()){
                        //ack
                        log.info("消息投递成功,消息id{}",id);
                    }else{
                        //nack
                        log.warn("消息投递到交换机失败! 消息id{}",id);
                    }
                },
                ex -> {
                    log.error("消息发送失败!",ex);
                });
        //持久化消息配置
        Message ms = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
                //.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
                .setExpiration("5000")//设置消息过期时间
                .build();
        //发送信息                              交换机名称  声明队列 不写代表广播  信息内容
        rabbitTemplate.convertAndSend(exchange,routingKey,ms,correlationData);
    }
}

6,延迟队列

利用 TTL 结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue )模式。
1,插件安装
//1,查看卷
docker volume list
docker volume inspect mq-plugins

//2,打开挂载目录
"/var/lib/docker/volumes/mq-plugins/_data"

//3,将下载好的插件上传

//4,进入容器内部安装
docker exec -it mq bash

rabbitmq-plugins enable rabbitmq_delayed_message_exchange
2,DelayExchange
将一个交换机声明为delayed类型,发送消息到delayExchange
1,接收消息
2,判断消息是否具备 x-delay 属性
3,如果有 x-delay 属性,说明是延迟消息,持久化到硬盘,读取 x-delay 值,作为延迟时间
4,返回 routing not found 结果给消息发送者
5,x-delay时间到期后,重新投递消息到指定队列
3,使用DelayExchange
创建延迟消息交换机 
@Configuration
@Log4j2
public class DelayExchange {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue1"),
            exchange = @Exchange(name = "delay.exchange",type = ExchangeTypes.TOPIC,
            delayed = "true"),
            key = "delay"
    ))
    public void receive(String message){
        System.out.println("收到延迟消息 = " + message);
    }
}

发送消息设置延时

@Log4j2
public class RabbitTemplateConfig {
    //   外部传入          交换机名称            路由key          编号                                       信息
    public void confirm(String exchange, String routingKey, String id,RabbitTemplate rabbitTemplate, String message){
        //定义生产者消息确认异步回调对象
        CorrelationData correlationData = new CorrelationData(id);
        //添加回调方法
        correlationData.getFuture().addCallback(result -> {
                    //判断是否成功
                    if(result.isAck()){
                        //ack
                        log.info("消息投递成功,消息id{}",id);
                    }else{
                        //nack
                        log.warn("消息投递到交换机失败! 消息id{}",id);
                    }
                },
                ex -> {
                    log.error("消息发送失败!",ex);
                });
        //持久化消息配置
        Message ms = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
                //.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
                //.setExpiration("5000")//设置消息过期时间
                .setHeader("x-delay",10000)//设置延时发送   
                .build();
        //发送信息                              交换机名称  声明队列 不写代表广播  信息内容
        rabbitTemplate.convertAndSend(exchange,routingKey,ms,correlationData);
    }
}

步骤:1,声明交换机 设置delayed属性为true

2,发送消息时,添加x-delay头,值为超时时间

7,消息堆积与惰性队列

消息堆积

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达 到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积。
解决消息堆积的思路
1,增加更多消费者,提高消费速度。
2,扩大队列溶剂,提高堆积上限。
3,在消费者内开启线程池加快处理速度。

惰性队列

从RabbitMQ的3.6.0版本开始,增加了惰性队列概念

什么是惰性队列:

1,接收到消息之后直接存入本地磁盘而不是内存中。

2,消费者消费消息时,才会将消息从本地磁盘中读取加载到内存中。

3,支持百万条级别消息存储。

作用:解决消息堆积。

原始不配置惰性队列的话,队列中消息达到上限后,消息会丢弃,配置了惰性队列会往本地存数据。

基于命令行设置惰性队列

//先进入容器后
docker exec -it mq bash
//进入容器后 执行命令
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
rabbitmqctl RabbitMQ 的命令行工具
set_policy :添加一个策略
Lazy :策略名称,可以自定义
"^lazy-queue$" :用正则表达式匹配队列的名字
'{"queue-mode":"lazy"}' :设置队列模式为 lazy 模式
--apply-to queues :策略的作用对象,是所有的队列

使用注解设置惰性队列

@Configuration
@Log4j2
public class LazyListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "lazy.queue1",arguments = {
                    @Argument(name = "x-queue-mode",value = "lazy")
            }),
            exchange = @Exchange(name = "lazy.exchange",type = ExchangeTypes.TOPIC,
                    delayed = "true"),
            key = "lazy"
    ))
    public void receive(String message){
        System.out.println("收到惰性队列消息 = " + message);
    }
}

测试惰性队列

public String lazy() {
        for (int i = 0; i <10000 ; i++) {
            RabbitTemplateConfig rabbitTemplateConfig = new RabbitTemplateConfig();
            rabbitTemplateConfig.confirm("lazy.exchange","lazy", UUID.randomUUID().toString(),this.rabbitTemplate,"lazylazylazy");
        }
         return "成功";
    }
惰性队列的数据存在内存中的是非常少的,是 IO 出来准备消费的,其他的都在硬盘中存储,而普通队列所有信息都在内存中,及其消耗内存。
消息堆积问题的解决方案?
1,队列上绑定多个消费者,提高消费速度
2,使用惰性队列,可以再 mq 中保存更多消息
惰性队列的优点有哪些?
1,基于磁盘存储,消息上限高
2,没有间歇性的 page-out ,性能比较稳定​​​​​​​
惰性队列的缺点有哪些?
1,基于磁盘存储,消息时效性会降低
2,性能受限于磁盘的 IO

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值