Springboot集成rabbitmq——消息持久化

目录

1.rabbitmq简介

2.消息持久化

3.发布确认

4.备份交换机

5.优先级比较

1.rabbitmq简介

 MQ(message queue),从字面意思上看,本质是个队列,遵从先入先出的规则,只不过队列中存放的内容是 message 而已,是一种跨进程的通信机制,用于上下游传递消息。RabbitMq是开发中常用的一种消息中间件,由于实现服务之间的消息转发。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。

它主要基于四大核心概念:生产者、交换机、队列、消费者。其工作原理如下:

 简单来说,工作原理如下:

生产者——>生成消息——>建立连接——>交换机——>队列——>建立连接——>消费者

2.消息持久化

在生产环境中偶尔会由于一些原因导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败,会 导致消息的丢失,需要手动处理和恢复。队列和交互机可以进行持久化操作,即rabbitmq重启不会导致队列和交换机的丢失。那么同样,我们也需要对消息进行持久化操作,保证消息的可靠性。

3.发布确认

我们可以通过发布确认的方式达到消息持久化的目的:

当消息发布出去后,没有收到交换机返回的确认信息时(即ack为false),我们即认为没有此次消息发布失败,即需要启用缓存机制缓存该消息。过一段时间后再次进行消息的发布。直至收到交换机返回的确认消息,从缓存中删除该消息。

具体实现代码如下:

1.编写配置文件:

spring:
  rabbitmq:
    host: 127.0.0.1   #主机地址
    port: 5672    #端口号
    username: guest   #用户名
    password: guest   #密码
    publisher-confirm-type: correlated    #发布确认模式:correlated即交换机收到消息后触发回调方法

2.编写rabbitmq配置类,在类中声明交换机及队列,其架构及代码如下:

package com.seven.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";


    //声明直接交换机Exchange
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    //声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定交换机和队列
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }

}

 此处声明直接交换机confirm.exchange及队列confirm.queue,并通过routing key :key1绑定二者。

3.编写监听队列confirm.queue的消费者代码:

package com.seven.rabbitmq.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import static com.seven.rabbitmq.config.ConfirmConfig.CONFIRM_QUEUE_NAME;

@Component
@Slf4j
public class ConfirmConsumer {
    @RabbitListener(queues = CONFIRM_QUEUE_NAME)
    public void receiveConfirmMsg(Message message){
        log.info("接收到confirm.queue队列消息:"+ new String(message.getBody()));
    }
}

 4.在controller中编写生产者代码:

    @GetMapping("/confirm/send/{message}")
    public void sendMsgAndConfirm(@PathVariable String message){

        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,"key1",""+message,new CorrelationData("1"));
        log.info("当前时间:{}, 发送信息:{}", new Date(), message);
    }

 代码中的 new CorrelationData("1") 代码用于给消息指定id

5.编写回调类:

package com.seven.rabbitmq.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

@Component  //step1
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback{

    @Resource   //step2
    private RabbitTemplate rabbitTemplate;

    @PostConstruct  //step3
    //后置注入,在创建完容器后,执行初始化方法前注入
    public void init(){
        //将自定义回调类注入RabbitTemplate中
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机不管是否收到消息的一个回调方法
     * @param correlationData 消息相关数据
     * @param ack             交换机是否收到消息
     * @param cause           未收到消息的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已经收到 id 为:{}的消息", id);
        } else {
            log.info("交换机还未收到 id 为:{}消息,原因:{}", id, cause);
        }
    }

}

在callback类中继承 RabbitTemplate.ConfirmCallback类,并实现里面的confirm方法。在confirm方法可以根据ACK的值进行对应的业务操作,比如:为false时,启用redisTemplate将该消息存入redis缓存中;为true时,调用数据库信息,进行操作等。此处可根据业务需求自信修改。

实现以上代码后,当交互机名称错误,或是交换机不存在等问题时,会调用回调方法,进行里面的逻辑调用,实现消息持久化:

(如上图,为交换机名称错误) 

而当routing key错误时,则不会触发上述错误。即在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。

此问题我们可以通过设置回退消息解决:

1.在配置文件中添加配置:

spring:
  rabbitmq:
    host: 127.0.0.1   #主机地址
    port: 5672    #端口号
    username: guest   #用户名
    password: guest   #密码
    publisher-confirm-type: correlated    #发布确认模式:correlated即交换机收到消息后触发回调方法
    publisher-returns: true   #回退消息,当找不到routing key对应的队列时,是否回退信息

2. 在回调类中添加代码,继承 RabbitTemplate.ReturnCallback类:

(此处注意:RabbitTemplate.ReturnCallback为旧版本使用类,

新版本请使用RabbitTemplate.ReturnsCallback)

package com.seven.rabbitmq.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

@Component  //step1
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback{

    @Resource   //step2
    private RabbitTemplate rabbitTemplate;

    @PostConstruct  //step3
    //后置注入,在创建完容器后,执行初始化方法前注入
    public void init(){
        //将自定义回调类注入RabbitTemplate中
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 交换机不管是否收到消息的一个回调方法
     * @param correlationData 消息相关数据
     * @param ack             交换机是否收到消息
     * @param cause           未收到消息的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已经收到 id 为:{}的消息", id);
        } else {
            log.info("交换机还未收到 id 为:{}消息,原因:{}", id, cause);
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息:{},被交换机 {} 退回,原因:{},路由key:{},code:{}",
                new String(message.getBody()), exchange,
                replyText, routingKey, replyCode);
    }
}

 同理,在returnedMessage方法中实现对应的业务逻辑。

在设置了消息确认和回退消息后,我们获得了对无法投递消息的感知能力,在生产者的消息无法被投递时发现并处理。

4.备份交换机

我们通常可以为队列设置死信队列来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。

在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进 入这个队列了。

同时,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

其主要架构如下:

修改rabbitmq配置类代码,声明备用交互机,备用队列及告警队列,并对直接交换机绑定其备用交换机 backup.exchange

package com.seven.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    //关于备份的
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    public static final String WARNING_QUEUE_NAME = "warning.queue";

//    //声明直接交换机Exchange
//    @Bean("confirmExchange")
//    public DirectExchange confirmExchange() {
//        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
//    }

    //声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    //绑定交换机和队列
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }


    //声明备份 Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    //声明确认 Exchange 交换机的备份交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                .durable(true)
                //设置该交换机的备份交换机
                .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return exchangeBuilder.build();
    }


    // 声明警告队列
    @Bean("warningQueue")
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    // 声明报警队列绑定关系
    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
                                  @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

    // 声明备份队列
    @Bean("backQueue")
    public Queue backQueue() {
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    // 声明备份队列绑定关系
    @Bean
    public Binding backupBinding(@Qualifier("backQueue") Queue queue,
                                 @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }
}

 编写监听消费者:

package com.seven.rabbitmq.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import static com.seven.rabbitmq.config.ConfirmConfig.BACKUP_QUEUE_NAME;
import static com.seven.rabbitmq.config.ConfirmConfig.WARNING_QUEUE_NAME;

@Component
@Slf4j
public class BackupConsumer {

    @RabbitListener(queues = BACKUP_QUEUE_NAME)
    public void receiveConfirmMsg(Message message){
        log.info("备份队列接收到confirm.queue队列消息:"+ new String(message.getBody()));
    }

    @RabbitListener(queues = WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message) {
        log.error("报警发现不可路由消息:{}", new String(message.getBody()));
    }
}

若之前已写过 confirm.exchange 交换机,由于更改配置,需要删掉,不然会报错 

编写消费者,测试效果:

    @GetMapping("/confirm/send/{message}")
    public void sendMsgAndConfirm(@PathVariable String message){

        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,"key1",""+message,new CorrelationData("1"));
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,"key2",""+message,new CorrelationData("2"));
        log.info("当前时间:{}, 发送信息:{}", new Date(), message);
    }

运行效果:

 可以看到routing key : key1正确发到直接交换机中,并发送到key1对应的队列中。

routing key : key2不存在,发生告警,启用备份交换机。备份队列和告警队列都收到备份交换机的信息,告警队列发出警告;备份队列进行业务处理。

两个消息都准确发到直接交换机中,收到发布确认消息。

5.优先级比较

由上述测试中可以看出,当交换机发布确认与备份交换机可以一起使用的时候,如果两者同时开启,备份交换机优先级更高

即当备份交换机宕机或是其他意外无法使用后,才会启用回退消息,将消息回退并缓存;否则启用备份交换机服务,由备份队列进行处理。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值