RabbitMQ

文章详细介绍了RabbitMQ的安装、配置、使用,包括消息的发送、接收、不同类型的消息模式(广播、路由、主题),以及幂等性、TTL机制和死信队列。还展示了SpringBoot如何集成RabbitMQ,通过配置类和注解实现消息的发送和接收。
摘要由CSDN通过智能技术生成

简介

当前市面上mq的产品很多,比如RabbitMQ、Kafka、ActiveMQ、ZeroMQ和阿里巴巴捐献给Apache的RocketMQ等。甚至连redis这种NoSQL都支持MQ的功能。

RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在不同的应用之间共享数据(跨平台跨语言)。RabbitMQ是使用Erlang语言编写,并且基于AMQP协议实现。

简单来说AMQP 协议由三大模块组成,分别是交换机、消息队列、消息队列路由。通过这三大模块间的配合,可以实现对消息的发送、消息的监听等功能。

什么是MQ?

消息总线(Message Queue),是一种跨进程、异步的通信机制,用于上下游传递消息。由消息系统来确保消息的可靠传递。

MQ是干什么用的?

应用解耦、异步、流量削锋、数据分发、错峰流控、日志收集等等...

RbbitMQ的优势

可靠性(Reliablity):使用了一些机制来保证可靠性,比如持久化、传输确认、发布确认。

灵活的路由(Flexible Routing):在消息进入队列之前,通过Exchange来路由消息。对于典型的路由功能,Rabbit已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange。

消息集群(Clustering):多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。

高可用(Highly Avaliable Queues):队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

多种协议(Multi-protocol):支持多种消息队列协议,如STOMP、MQTT等。

多种语言客户端(Many Clients):几乎支持所有常用语言,比如Java、.NET、Ruby等。

管理界面(Management UI):提供了易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。

跟踪机制(Tracing):如果消息异常,RabbitMQ提供了消息的跟踪机制,使用者可以找出发生了什么。

插件机制(Plugin System):提供了许多插件,来从多方面进行扩展,也可以编辑自己的插件。

安装

准备一个新的虚拟机,各种配置设置好,比如防火墙之类的,然后把mq和erl拖到opt文件夹下面去

安装erlang

yum -y install esl-erlang_23.0.2-1_centos_7_amd64.rpm
erl #查看是否安装成功

安装mq

yum -y install rabbitmq-server-3.8.5-1.el7.noarch.rpm

安装完了过后,再安装可视化控制台,它只是mq的一个插件而已

rabbitmq-plugins list   #查看mq的插件

找到management,它就是可视化插件

rabbitmq-plugins enable rabbitmq_management

启动mq

 systemctl start rabbitmq-server.service

查看启动状态

 systemctl status rabbitmq-server.service

打开浏览器输入虚拟机的ip地址加端口15672会出现mq的页面,默认账号密码是guest

设置一下允许远程登录

这个路径是按照mq自动生成的

cd /etc/rabbitmq/
vim rabbitmq.config   #手动添加配置文件
[{rabbit,[{loopback_users,[]}]}].        #手动添加这句话,注意后面有个点

其他的一些配置,自己去网上搜
重启mq服务

systemctl restart rabbitmq-server.service

SpringBoot集成mq

添加依赖,此依赖是 SpringBoot 的 Starter 中封装好的 amqp 依赖,也是与RabbitMQ 进行整合的基础依赖

   <!--mq依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

添加配置文件,在spring配置下面加啊!

spring:
  rabbitmq:
    host: 192.168.64.5
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        #消费者最小数量
        concurrency: 5
        #消费者最大数量
        max-concurrency: 5
        #消费者每次最大处理量
        prefetch: 1
    template:
      retry:
        #发布者重试功能,默认是false,默认1秒,最大次数3,重试间隔时间默认10秒
        enabled: true

广播模式(Fanout)

广播模式顾名思义大家都能收到,发布者发布消息过后,订阅者都能接收到消息

换种说法就是把交换机(Exchange)里的消息发送给所有绑定该交换机的队列,忽略routingKey

什么是交换机?说白了就是为了解耦,发布者只需要把消息给交换机,然后就不管了,而消费者只需要对接交换机就行

在测试层,新加一个测试接口

@Autowired
    private Sender sender;

     //测试消息队列
    @RequestMapping("/mq/fanout")
    @ResponseBody
    public void mq(){
        sender.send("有口碑找老张,学习java不得慌");
    }

新建一个广播交换机配置类

@Configuration
public class RabbitMQFanoutConfig {

    private static final String QUEUE_1="我是对列一的消息";
    private static final String QUEUE_2="我是对列二的消息";
    private static final String EXCHANGE="我是广播模式的交换机";

    //新建队列一
    @Bean
    public Queue queue1(){
        return new Queue(QUEUE_1);
    }

    //新建队列二
    @Bean
    public Queue queue2(){
        return new Queue(QUEUE_2);
    }

    //新建广播模式交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange(EXCHANGE);
    }

    //把队列和交换机绑定
    @Bean
    public Binding binding1(){
        return BindingBuilder.bind(queue1()).to(fanoutExchange());
    }
    @Bean
    public Binding binding2(){
        return BindingBuilder.bind(queue2()).to(fanoutExchange());
    }

}

新建一个发布者

@Service
@Slf4j
public class Sender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String msg) {
        log.info("安静,听我说两句:"+msg);
        rabbitTemplate.convertAndSend("我是广播模式的交换机","",msg);
    }

}

注意上面convertAndSend里面的三个参数
第一个:交换机的名字,不是常量的名字,是常量后面的内容

第二个:路由key,没有就加个空串(路由模式会用到)

第三个:接收的消息

新建一个订阅者

@Service
@Slf4j
public class Receiver {

    @RabbitListener(queues = "我是队列一的消息")
    public void receive1(String msg) {
        log.info("cgb2109 :"+msg);
    }
    @RabbitListener(queues = "我是队列二的消息")
    public void receive2(String msg) {
        log.info("cgb2111:"+msg);
    }

}

注意上面注解里面绑定的是常量后面的内容而不是名字
开始测试:启动服务,访问地址,看后台控制台订阅者接收的消息

路由模式(Direct)

比广播模式多了个路由key,满足key的队列才能接受到消息

在测试层,新加两个测试接口

//测试路由模式
    @RequestMapping("/mq/direct1")
    @ResponseBody
    public void dmq1(){
        sender.dsend1("有口碑找老张,学习java不得慌");
    }
    @RequestMapping("/mq/direct2")
    @ResponseBody
    public void dmq2(){
        sender.dsend2("有口碑找老张,学习java不得慌");
    }

新建一个路由交换机配置类

@Configuration
public class RabbitMQDirectConfig {

    private static final String QUEUE_3="我是路由队列一的消息";
    private static final String QUEUE_4="我是路由队列二的消息";
    private static final String EXCHANGE="我是路由模式的交换机";
    private static final String ROUTING_KEY1="cgb2109";
    private static final String ROUTING_KEY2="cgb2111";

    //新建队列一
    @Bean
    public Queue queue3(){
        return new Queue(QUEUE_3);
    }

    //新建队列二
    @Bean
    public Queue queue4(){
        return new Queue(QUEUE_4);
    }

    //新建路由模式交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(EXCHANGE);
    }

    //把队列和交换机以及路由键绑定
    @Bean
    public Binding binding3(){
        return BindingBuilder.bind(queue3()).to(directExchange()).with(ROUTING_KEY1);
    }
    @Bean
    public Binding binding4(){
        return BindingBuilder.bind(queue4()).to(directExchange()).with(ROUTING_KEY2);
    }

}

在发布者层加两个方法

    public void dsend1(String msg) {
        log.info("安静,听我说两句:"+msg);
        rabbitTemplate.convertAndSend("我是路由模式的交换机","cgb2109",msg);
    }
    public void dsend2(String msg) {
        log.info("安静,听我说两句:"+msg);
        rabbitTemplate.convertAndSend("我是路由模式的交换机","cgb2111",msg);
    }

在订阅者层加两个方法

 @RabbitListener(queues = "我是路由队列一的消息")
    public void receive3(String msg) {
        log.info("cgb2109 :"+msg);
    }
    @RabbitListener(queues = "我是路由队列二的消息")
    public void receive4(String msg) {
        log.info("cgb2111:"+msg);
    }

开始测试:启动服务,访问不同的地址,看后台控制台订阅者接收的消息

主题模式(Topic)

路由模式的升级版,工作中常用的也是这个模式

它是通过通配符做匹配

*:表示一个

#:表示任意(啥没有也行)

如果一个队列有两种通配符都满足,那么消息也只会接收一条

新建一个主题交换机配置类

@Configuration
public class RabbitMQTopicConfig {



    private static final String QUEUE_5 = "我是主题队列一的消息";
    private static final String QUEUE_6 = "我是主题队列二的消息";
    private static final String EXCHANGE = "我是主题模式的交换机";
    private static final String ROUTING_KEY1 = "*.cgb2109";
    private static final String ROUTING_KEY2 = "#.cgb2111";

    //新建队列一
    @Bean
    public Queue queue5() {
        return new Queue(QUEUE_5);
    }

    //新建队列二
    @Bean
    public Queue queue6() {
        return new Queue(QUEUE_6);
    }

    //新建主题模式交换机
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(EXCHANGE);
    }

    //把队列和交换机以及路由键绑定
    @Bean
    public Binding binding5() {
        return BindingBuilder.bind(queue5()).to(topicExchange()).with(ROUTING_KEY1);
    }

    @Bean
    public Binding binding6() {
        return BindingBuilder.bind(queue6()).to(topicExchange()).with(ROUTING_KEY2);
    }

}

在发布者层加两个方法

  public void tsend1(String msg) {
        log.info("安静,听我说两句:"+msg);
        rabbitTemplate.convertAndSend("我是主题模式的交换机","graduate.cgb2109",msg);
    }
    public void tsend2(String msg) {
        log.info("安静,听我说两句:"+msg);
        rabbitTemplate.convertAndSend("我是主题模式的交换机","cgb2111",msg);
    }

在订阅者层加两个方法

    @RabbitListener(queues = "我是主题队列一的消息")
    public void receive5(String msg) {
        log.info("cgb2109 :"+msg);
    }
    @RabbitListener(queues = "我是主题队列二的消息")
    public void receive6(String msg) {
        log.info("cgb2111:"+msg);
    }

在测试层加两个方法

  //测试主题模式
    @RequestMapping("/mq/topic1")
    @ResponseBody
    public void tmq1(){
        sender.tsend1("有口碑找老张,学习java不得慌");
    }
    @RequestMapping("/mq/topic2")
    @ResponseBody
    public void tmq2(){
        sender.tsend2("有口碑找老张,学习java不得慌");
    }

测试吧

RabbitMQ 消息发送的步骤

第一步,生产者将消息生产出来,并将消息发送到 RabbitMQ Server 上,即我们发到 RabbitMQ 中的消息,会首先置于 RabbitMQ Server 中,RabbitMQ Server又叫Broker Server、RabbitMQ Broker有些地方简称Broker,简单来说,就是一个消息队列服务器实体

第二步,RabbitMQ Server 根据客户端所发来的连接请求,判断将消息传递到哪个 Virtual Host 中,如果我们在连接 RabbitMQ Server 时,没有设置要连接的 Virtual Host 地址,则 RabbitMQ Server 会将我们的消息传递到地址为 “/” 的 Virtual Host 中去;

第三步,在将消息传递到对应的 Virtual Host 中后,Virtual Host 会继续解析我们的连接请求,并在这一步解析出我们需要的 Exchange 的类型,以及 Channel 的名称,Queue 的名称,以及消息和 Exchange 之间是否有 routing_key ,Channel 和 Queue 之间是否有 bidding_key 这些信息;

第四步,Virtual Host 会根据解析出来的这些信息,将消息和 Exchange 进行匹配,相应的,Exchange 也会和对应的 Channel 进行匹配,并最终将 Queue 和 Channel 进行绑定,使消息进入到对应的消息队列中去;

第五步,待消息进入到对应的消息队列中之后,RabbitMQ Server 会返回给我们一个确认应答(确认应答后续会进行介绍),来通知我们,消息已经成功被 RabbitMQ Server 所发送,于是,消费者变回根据一定的策略来从消息队列中获取消费,并最终将该消息消费掉,消息消费之后,也会给我们返回一个确认应答(确认应答后续会进行介绍),告诉我们消息已经成功消费掉了。

RabbitMQ整体架构图

在经过初体验过后,是时候系统的了解一下RabbitMQ的整体流程了

由图可知,RabbitMQ  由 Virtual Host 、Exchange 、Connection 、Queue 四大核心组件所组成。

Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等

Connection:publisher/consumer 和 broker 之间的 TCP 连接

Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)

Queue:消息最终被送到这里等待 consumer 取走

Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

RabbitAdmin

由于RabbitMQ是RabbitMQ 是基于 AMQP 协议和 erlang 语言进行编码开发的,所以,在 Spring 中无法直接使用 RabbitMQ ,因此Spring提供了一个叫做 Spring-AMQP 的中间层依赖进行映射,就像Mapper一样,不过这种映射关系是直接把 RabbitMQ 中的各种元素与 Java 程序相对应,其提供了 RabbitMQ 中声明交换机、声明队列、绑定交换机和队列,以及绑定路由 Key 等其他 API ,如何配置看下面

配置类

添加一个配置类

@Configuration
public class RabbitAdminConfig {

    @Value("${spring.rabbitmq.host}")
    private String host;
    @Value("${spring.rabbitmq.port}")
    private Integer port;
    @Value("${spring.rabbitmq.username}")
    private String username;
    @Value("${spring.rabbitmq.password}")
    private String password;
    @Value("${spring.rabbitmq.virtualhost}")
    private String virtualhost;

    @Bean
    public ConnectionFactory connectionFactory () {
        CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
        cachingConnectionFactory.setAddresses(host);
        cachingConnectionFactory.setPort(port);
        cachingConnectionFactory.setUsername(username);
        cachingConnectionFactory.setPassword(password);
        cachingConnectionFactory.setVirtualHost(virtualhost);
       return cachingConnectionFactory;
    }
}

代码注释:
第16行:使用 Spring-AMQP 中的 ConnectionFactory 类,声明了connectionFactory 的连接工厂方法,用于对 RabbitMQ 进行初始化

第17行:实例化了一个 cachingConnectionFactory 实例,该实例是 Spring-AMQP 中对 RabbitMQ 连接信息进行初始化的基础实例,所有的 RabbitMQ 连接信息均来源于该实例

第18-22行:通过cachingConnectionFactory 实例,分别初始化 RabbitMQ Server 的服务地址、端口、用户名、密码、虚拟主机。

第23行:将配置好的 cachingConnectionFactory 实例进行返回,以初始化完成 RabbitMQ 客户端连接。

初始化RabbitAdmin

@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
    RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
    rabbitAdmin.setAutoStartup(true);
    return rabbitAdmin;
}

你们知道的,我个人不喜欢写这种不是必须的配置类,这里写来的意义纯粹就是为了让你们知道有个这个关键的玩意儿,所以我都是直接用的@Value注解直接拿yml里面的值来耍

这些在yml里面配好了就行了,不用再单独写个配置类,这个配置类不是必须的

RabbitMQ幂等性

在计算机层面,幂等性就是:

对于同一个系统,在相同条件下,一次对系统的请求和重复多次对系统的请求,对系统所造成的影响都是一样的

比如在网上买东西,挑选好要买的商品之后进行下单,并进行了付款,然而,当我们点击了确认支付后,收到了银行发来的扣款短信,但是没有收到网购平台支付成功的提示,于是我们又重新对该订单进行了支付,这最终造成了我们对于同一个订单确支付了两笔的现象,这就是没有做幂等性,如果网购平台对支付这块的功能做了幂等性的限制,那么我们在支付完第一笔后,再次进行支付时,网购平台会提示我们该笔订单已经支付过了,不能重复支付,而支付成功的提示会在等待一定的时间之后发给我们,这才是合理的处理结果。

那么RabbitMQ的幂等性是怎么回事呢

RabbitMQ 实际就是处理消息的一款中间件,主要处理消息的发送、消息的接收两大模块,消息作为 RabbitMQ 中的主角,即要保障消息准备被发送出去,也要保障消息准确被接收了

生产端的幂等性

应用程序中的数据,一旦被发送到 RabbitMQ Server 中后,无论 RabbitMQ Server 有没有接收到这一消息,都会返回给客户端一个接收消息的应答,来告诉客户端消息在 RabbitMQ Server 中的一个状态。

如果消息没有被 RabbitMQ Server 接收到,那么客户端也无需进行消息重复发送的操作,RabbitMQ Server 本身会自动将该消息进行重复发送,直到消息被 RabbitMQ Server 所接收,并返回消息已经发送的确认应答。

相反,如果消息被 RabbitMQ Server 接收到了,那么客户端无须进行消息的重复发送,如果此时我们向 RabbitMQ Server 中再次发送一条同样的消息,那么 RabbitMQ Server 会抛出消息已存在异常

消费端的幂等性

对于存在于 RabbitMQ Server 中的消息,一旦被消费者所获取,那么这个消费者就不能将消息再次发送出去,同时该消息只能被当前的消费者消费,其他的消费者均不能获取到该消息并消费。和消息发送一样,无论消费者有没有将该消息进行消费,都会给客户端返回一个确认应答。

如果消息没有被当前的消费者所消费,那么 RabbitMQ Server 会将该消息置于另一个消息队列当中,等待此时刻的所有消息都已经被消费之后,再回过头来对该消息进行消费,直到消费者正常消费掉了该消息。

相反,如果消息被当前的消费者所消费,那么,当生产者生产出了一个相同的消息之后,消费者也不会再对该消息进行消费了,因为同样的消息,消费者已经进行了消费,并返回了信息消费的确认性应答。

因此,幂等性这一概念贯穿 RabbitMQ 消息处理的始终,那么具体又是怎么搞的呢?

消息发送确认与返回机制

两种机制就是 RabbitMQ 自带的补偿机制,在我们开发应用程序时,如果遇到了对应的业务场景,直接进行相应的配置就行

消息发送确认机制

消息确认机制就是,确认消息是否已经从生产者发送到了 RabbitMQ Server 中

消息在被成功发送到 RabbitMQ Server 中之后,RabbitMQ Server 就会给生产端返回一个确认应答,这个确认应答会包含两种结果

一种就是消息发送到了 RabbitMQ Server ,RabbitMQ Server 收到了该消息,这时会给生产者返回 ack 的确认应答, 表示消息已经被接收。

另一种就是消息没有发送到 RabbitMQ Server ,RabbitMQ Server 没有收到该消息,这时会给生产者返回一个 nack 的确认应答,即 no ack , 表示没有接收到该消息。

上代码

cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
cachingConnectionFactory.setPublisherReturns(true);// 确认消息发送到队列,如未被队列接收时返回

这里对新版本的这个ConfirmType的三个值解释一下:

  • NONE值是禁用发布确认模式,是默认值
  • CORRELATED值是发布消息成功到交换器后会触发回调方法
  • SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
    这里开始我就在yml里面添加配置,所以都注释上了
spring:
  rabbitmq:
    host: 192.168.64.5
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        #消费者最小数量
        concurrency: 5
        #消费者最大数量
        max-concurrency: 5
        #消费者每次最大处理量
        prefetch: 1
    template:
      retry:
        #发布者重试功能,默认是false,默认1秒,最大次数3,重试间隔时间默认10秒
        enabled: truez
     #开启发布消息成功到RabbitMQServer后触发回调方法
    publisher-confirm-type: correlated
    #开启确认消息发送到队列,如未被队列接收时返回,而不是丢弃
    publisher-returns: true

添加配置类,编写发布消息成功或者失败到RabbitMQServer后触发的自定义回调方法

@Slf4j
@Configuration
public class ConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // spring框架的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
    }
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData!=null?correlationData.getId():"";
        if(ack){
            log.info("RabbitMQServer收到了ID为:{}的消息",id);
        }else {
            log.info("RabbitMQServer收到了ID为:{}的消息,由于原因:{}",id,cause);
        }
    }

}注意测试的时候你要么把配置类嘎了,用yml,要么就把配置类注释掉的那两段代码打开,不然没用,随便访问个测试接口,测试结果如下

消息返回机制

消息返回机制描述了一种 RabbitMQ Server 中的不可达消息与生产端的关系

消息在被成功发送到RabbitMQ消息队列中之后,如果消息在经过当前配置的 exchangeName 或 routingKey 没有找到指定的交换机,或没有匹配到对应的消息队列,

那么这个消息就被称为不可达的消息,如果此时配置了消息返回机制,那么此时RabbitMQ消息队列会返回给生产者一个信号,信号中包括消息不可达的原因,以及消息本身的内容。

上面的yml已经开启了消息返回机制

 publisher-returns: true

上代码

添加配置类,编写不可达消息的回调函数

老版写法

@Configuration
public class ReturnCallbackConfig implements RabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct 
        rabbitTemplate.setReturnCallback(this);
    }

    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
        System.out.println("returnCallback ..............");
        System.out.println(message);// 消息本身    
        System.out.println(s2);// 队列名称
    }
}

新版写法,RabbitTemplate.ReturnsCallback多了一个s,大概就是把原本的这些参数封装成了一个对象

@Slf4j
@Configuration
public class ReturnCallbackConfig implements RabbitTemplate.ReturnsCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @PostConstruct
    public void init() {
        rabbitTemplate.setReturnsCallback(this);
    }
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.error("消息没有被成功投递到队列\n投递的消息是:{},\n被交换机:{}退回,\n退回原因:{},路由key:{}",
                new String(returned.getMessage().getBody()), returned.getExchange(),
                returned.getReplyText(), returned.getRoutingKey());
    }
}

注意,测试的时候修改一下路由key就能看到结果了

消费者ACK与重回队列机制

消费者 ACK机制

描述消息与消费者之间的一种确认关系,其主要内容就是用来监听消息是否已经被消费者成功消费了

当消息成功被发送到 RabbitMQ Server 中,并且经交换机和频道,被路由到了相应的消息队里之后,这些消息就需要等待合适的消费者来接收这些消息,并最终将这些消息进行消费。消费者 ACK 就是在这个过程中间充当了一种监听器的作用,主要就是用来监听这些消息被消费者进行消费的结果,并将消息消费的结果返回给消费者端。

返回消费者端的ACK信号有两种

  • 第一种 ACK 信号返回结果就是消息已经被成功消费了,这个时候返回给消费端的是 ack 信号,即消息消费成功的确认信号
  • 另一种 ACK 信号返回结果是消息没有被消费成功,这个时候返回给消费端的是 nack 信号,即消息没有被消费者消费成功。
    第一种 ACK 返回信息是 RabbitMQ Server 中的消息一旦被成功消费后就会返回给消费端,这个过程是自动的;而另一种 ACK 信号,由于消息在没有被成功消息后,RabbitMQ 会有自带的解决措施,所以此种 ACK 信号需要我们手动来选择,到底是使用 RabbitMQ 自带的解决措施,还是使用消费者 ACK 机制,因此这就是我们常说的关闭自动ACK,改为手动ACK的原因

签收方式有三种:

  1. basicAck 消息确认
  1. basicNack 消息回退
  1. basicReject 消息拒绝

重回队列机制

消息重回队列机制,是描述那些没有被成功消费的消息与消费端之间的一种保障策略, 其主要目的就是为了存储那些没有被消费者成功消费掉的消息,即在 RabbitMQ 诸多的消息队列中,专门用来存储那些没有被消费者成功消费掉的消息的队列,这种队列就被称为重回队列。

添加一个消费者

@RabbitListener(queues = "我是路由队列一的消息")
public void receive8(String msg, Channel channel, Message message) throws IOException {
    try {
        // 消息
        log.info("msg = " + msg);
        //为了测试用,故意抛个异常
        //throw new RuntimeException("消费者故意抛出异常......");
    } catch (Exception e) {
        e.printStackTrace();
        log.error("消息未被消费,重回队列中...");
        /**
         * 出现异常,把消息重新投递回队列中,如一直有异常会一直循环投递
         * deliveryTag:表示消息投递序号。
         * multiple:是否批量确认。
         * requeue:值为 true 消息将重新入队列。
         */
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
    }
    /**
     * 消息确认 ACK
     * deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加
     * multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。
     */
   log.info("ACK手动确认中.....");

    // 消息确认 basicAck
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    log.info("消息已成功消费");
    // 消息拒绝 basicReject
    //channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);

}

TTL机制

TTL,全称为 Time to Live ,即生存时间

在 RabbitMQ 中,TTL 这一概念是作用于消息和消息队列上,即为消息以及消息队列规定了一个生存时间,当消息或消息队列的生存时间超过了 TTL 所规定的生存时间之后,消息就会失效,且不会被消费。

  • 对于消息来说,一旦消息的生存时间超过了 TTL 所规定的消息生存时间,那么,这条消息会立即失效,并且不会被任何消费者消费,且会变成一种死信,并最终会被 RabbitMQ 放入死信队列中
  • 对于消息队列来说,如果消息队列的生存时间超过了 TTL 所规定的消息队列的生存时间,那么消息队列会立即失效,且该消息队列中的消息也会随着消息队列的失效而失效。 
    因此,可以根据业务需求自己去灵活使用TTL机制

TTL队列

添加TTL对列配置类

@Configuration
public class TTLConfig {
    @Autowired
    private RabbitMQDirectConfig rabbitMQDirectConfig;
    
    public Queue ttlDirectQueue(){
        Map<String, Object> map = new HashMap<>();
        // 队列设置存活时间,单位ms,必须是整形数据。
        map.put("x-message-ttl",5000);
        /** @params1 : 队列名称
         *  @params2 : 是否持久化(true:重启后不会消失)
         *  @params3 : 是否独占队列(true:仅限于此连接使用)
         *  @params4 : 是否自动删除(队列内最后一条消息被消费后,队列将自动删除)
         *  @params5 : ttl队列存活时间
         */
        Queue queue = new Queue("ttl.direct.queue",true,false,false,map);
        return queue;
    }
    @Bean
    public Binding ttlDirectBinding(){
        return BindingBuilder.bind(ttlDirectQueue()).to(rabbitMQDirectConfig.directExchange()).with("ttl");
    }

}

这里注意,我们直接用的是之前创建好了的路由交换机,这里不能再新建一个路由交换机,不然控制台会报错,已存在某某类型的交换机
为了测试TTL队列,所以就不要搞消费者,只来一个生产者,上面设置的时间是5秒,如果五秒过了,队列里面的所有消息将会GG

添加一个生产者

//测试ttl队列
public void ttlTest(String msg){
    log.info("TTL队列消息为:{}",msg);
    String s = UUID.randomUUID().toString();
    // 发布消息
    rabbitTemplate.convertAndSend("我是路由模式的交换机","ttl",msg,new CorrelationData(s));

}测试接口

//测试TTL
@RequestMapping("/mq/ttl")
@ResponseBody
public void ttlTest() {
sender.ttlTest("哟西");
}

TTL消息

针对于某一条消息设置TTL则需要我们在发送消息的时候设置expiration的属性。

添加一个生产者

//测试ttl消息
public void ttlMessageTest(String msg){

    log.info("TTL队列消息为:{}",msg);
    String s = UUID.randomUUID().toString();
    //设置消息相关属性
    MessageProperties messageProperties = new MessageProperties();
    //设置过期时间
    messageProperties.setExpiration("3000");
    byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
    Message message = new Message(bytes, messageProperties);
    // 发布消息
    rabbitTemplate.convertAndSend("我是路由模式的交换机","ttl",message,new CorrelationData(s));
}

添加测试接口

//测试消息TTL
@RequestMapping("/mq/ttlmsg")
@ResponseBody
public void ttlMessageTest() {
    sender.ttlMessageTest("TTL消息");
}

如果同时设置了TTL队列过期时间和TTL消息过期时间,那么设置了TTL消息过期时间的消息会触发它自己的过期时间,其他消息则触发TTL队列设置的过期时间

死信队列

当消息被拒绝访问,即 RabbitMQ Server 返回 nack 的信号时、消息的 TTL 过期时、消息队列达到最大长度,消息不能入队时,就会产生死信

死信队列就是用于储存死信的消息队列,在死信队列中,有且只有死信构成,不会存在其余类型的消息。

死信队列在 RabbitMQ 中并不会单独存在,往往死信队列都会绑定这一个普通的消息队列,当所绑定的消息队列中,有消息变成死信了,那么这个消息就会重新被交换机路由到指定的死信队列中去,我们可以通过对这个死信队列进行监听,从而手动的去对这一消息进行补偿

配置死信队列也很简单,首先添加一个死信队列配置类,这里写另外一种配置写法,用QueueBuilder创建队列,点出对应的方法就可以配置了

@Configuration
public class DeathQueConfig {

    public static final String DEAD_LETTER_EXCHANGE = "dead.exchange";

    @Bean("deadExchange")
    public DirectExchange directExchange() {
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }                                                                                                                           

    //把延迟队列和死信队列绑定
    @Bean
    public Queue deathQueue() {
        return QueueBuilder.durable("死信队列")
                //   .withArgument("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE)
                //    .withArgument("x-dead-letter-routing-key","death")
                .build();
    }

    @Bean
    public Binding deathBinding(
            @Qualifier("deadExchange") DirectExchange exchange
    ) {
        return BindingBuilder.bind(deathQueue()).to(exchange).with("death");
    }



}

这里就测试消息过期过后,出现在死信队列里面的配置
打开之前的TTLConfig

//绑定死信队列
map.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
map.put("x-dead-letter-routing-key", "death");

加上这两句话就行了,然后测试,往之前的延迟队列发消息,时间到了过后会自动发到死信队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值