SpringBoot整合RabbitMQ实现消息确认机制

注:这里记得给先我们用户授权virtualhost,由于我使用的virtualhost为'/',用户为admin,所以使用以下命令进行授权

rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"

生产者

1.导入相关依赖包

在创建SpringBoot项目的时候其实就可以选择相关依赖包进行导入

<!--springboot整合rabbitmq依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- lombok插件 -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.10</version>
</dependency>

2.修改application.yml文件

server:
  port: 8081

spring:
  application:
    name: rabbitmq-provider
  rabbitmq:
#rabbitmq集群配置方式如下
#addresses: 192.168.79.128:5672,192.168.79.135:5672 host: 192.168.111.129 port: 5672 username: admin password: 123 virtual-host: /

3.创建RabbitMQ配置文件

在SpringBoot中我们通过配置文件来实现交换机、队列的创建与绑定

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

@Configuration
public class RabbitExchangeAndQueueConfig {
    /**
     * 创建交换机
     * durable:是否持久化
     * autoDelete:是否自动删除,当没有生产者或消费者使用该交换机时,会自动删除
     */
    //创建fanout交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanout-exchange",true,false);
    }
    //创建direct交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("direct-exchange",true,false);
    }
    //创建topic交换机
    @Bean TopicExchange topicExchange(){
        return new TopicExchange("topic-exchange",true,false);
    }
    /**
     * 创建队列
     * durable:是否持久化
     * exclusive:默认false,只能在当前创建连接时使用,连接关闭后队列自动删除,该优先级高于durable
     * autoDelete:是否自动删除,当没有生产者或消费者使用该交换机时,会自动删除
     */
    @Bean
    public Queue emailQueue(){
        return new Queue("emailQueue",true);
    }
    @Bean
    public Queue smsQueue(){
        return new Queue("smsQueue",true);
    }
    /**
     * 绑定交换机和队列
     */
    //email队列绑定fanout交换机
    @Bean
    public Binding emailFanoutBinding(){
        return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
    }
    //sms队列绑定fanout交换机
    @Bean
    public Binding smsFanoutBinding(){
        return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
    }
    //sms队列绑定direct交换机
    @Bean
    public Binding smsDirectBinding(){
        return BindingBuilder.bind(smsQueue()).to(directExchange()).with("sms");
    }
    //email队列绑定topic交换机
    @Bean
    public Binding emailTopicBinding(){
        return BindingBuilder.bind(emailQueue()).to(topicExchange()).with("email.#");
    }
    //sms队列绑定topic交换机
    @Bean
    public Binding smsTopicBinding(){
        return BindingBuilder.bind(smsQueue()).to(topicExchange()).with("sms.#");
    }
}

4.测试

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqProviderApplicationTests {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Test
    void testFanout(){
        String msg = "fanout";
        rabbitTemplate.convertAndSend("fanout-exchange",null,msg);
    }

    @Test
    void testDirect() {
        String msg = "direct";
        rabbitTemplate.convertAndSend("direct-exchange","sms",msg);
    }

    @Test
    void testTopic(){
        String msg = "email-topic";
        String msg1 = "sms-topic";
        rabbitTemplate.convertAndSend("topic-exchange","email.chen",msg);
        rabbitTemplate.convertAndSend("topic-exchange","sms.chen",msg1);
    }
}

消费者

1.导入相关依赖包

在创建SpringBoot项目的时候其实就可以选择相关依赖包进行导入

<!--springboot整合rabbitmq依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- lombok插件 -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.10</version>
</dependency>

2.修改application.yml文件

server:
  port: 8082

spring:
  application:
    name: rabbitmq-consumer
  rabbitmq:
    host: 192.168.111.129
    port: 5672
    username: admin
    password: 123
    virtual-host: /

3.创建Service接收消息

 

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;

import org.springframework.stereotype.Service;

@Service
public class DirectReceiver {
    
    @RabbitHandler
    @RabbitListener(queues = "emailQueue")  //监听的队列名称
    public void emailProcess(String testMessage){
        System.out.println("emailQueue: " + testMessage.toString());
    }

    @RabbitHandler
    @RabbitListener(queues = "smsQueue")  //监听的队列名称
    public void smsProcess(String testMessage){
        System.out.println("smsQueue: " + testMessage.toString());
    }
}

消息确认机制

基于上方例子做以下操作

生产者

1.修改application.yml文件

新增最后两项

server:
  port: 8081

spring:
  application:
    name: rabbitmq-provider
  rabbitmq:
    host: 192.168.111.129
    port: 5672
    username: admin
    password: 123
    virtual-host: /

    #确保消息已发送到交换机
    publisher-confirm-type: correlated
    #确保消息没有指定到对应queue时,将消息返回,而不是丢弃
    publisher-returns: true

2.创建一个新的RabbitMQ配置文件

对RabbitTemplate的配置

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitTemplateConfig {
    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        //指定消息在没有被队列接收时是否强行退回还是直接丢弃
        //该项通常与yml配置文件中的publisher-returns配合一起使用,若不配置该项,setReutrnCallback将不会有消息返回
        rabbitTemplate.setMandatory(true);

        //消息发送成功的回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            System.out.println("ConfirmCallback:     "+"相关数据:"+correlationData);
            System.out.println("ConfirmCallback:     "+"确认情况:"+ack);
            System.out.println("ConfirmCallback:     "+"原因:"+cause);
        });

        //发生异常时的消息返回提醒
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            System.out.println("ReturnCallback:     "+"消息:"+message);
            System.out.println("ReturnCallback:     "+"回应码:"+replyCode);
            System.out.println("ReturnCallback:     "+"回应信息:"+replyText);
            System.out.println("ReturnCallback:     "+"交换机:"+exchange);
            System.out.println("ReturnCallback:     "+"路由键:"+routingKey);
        });
        return rabbitTemplate;
    }
}

可以看到上面写了两个回调函数,一个是ConfirmCallback,一个是ReturnCallback,那么这两种回调函数什么时候触发呢。

推送消息存在四种情况:

  1. 消息推送到Server,但是找不到交换机
  2. 消息推送到Server,找到交换机了,但是找不到队列
  3. 消息推送到Server,交换机和队列都没找到
  4. 消息推送成功

经过测试,情况1,3,4都是触发ConfirmCallbak,情况2是会同时触发ConfirmCallback和ReturnCallback

消费者

1.修改application.yml文件

新增最后项

server:
  port: 8082

spring:
  application:
    name: rabbitmq-consumer
  rabbitmq:
    host: 192.168.111.129
    port: 5672
    username: admin
    password: 123
    virtual-host: /

    listener:
      simple:
        # NONE 自动确认 RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
        #      所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
        #      一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。
        # MANUAL 手动确认 需要人为地获取到channel之后调用方法向server发送ack(或消费失败时的nack)信息。
        # AUTO 由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到server端。
        acknowledge-mode: manual #手动ACK

2.修改Service接收信息项

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;

import org.springframework.stereotype.Service;

import java.io.IOException;


@Service
public class DirectReceiver {

    @RabbitHandler
    @RabbitListener(queues = "emailQueue")  //监听的队列名称
    public void emailProcess(Channel channel, Message testMessage) throws IOException {
        try{
            System.out.println(new String(testMessage.getBody(),"UTF-8"));
            //成功接收到消息后,确认消息,要不确认消息将会进入unacked,false只确认当前一个消息收到,true确认所有consumer获得的消息
            channel.basicAck(testMessage.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //若是消息没有成功接收,第二个参数设置为true的话,代表重新放回队列中,false则为丢弃,在此也可以做成放置死信队列的操作
            channel.basicReject(testMessage.getMessageProperties().getDeliveryTag(),false);
        }
    }

    @RabbitHandler
    @RabbitListener(queues = "smsQueue")  //监听的队列名称
    public void smsProcess(Channel channel, Message testMessage) throws IOException {
        try{
            System.out.println(new String(testMessage.getBody(),"UTF-8"));
            channel.basicAck(testMessage.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //第二个参数,true代表会重新放回队列
            channel.basicReject(testMessage.getMessageProperties().getDeliveryTag(),false);
        }
    }
}

 

posted @ 2022-04-20 16:22  RFAA  阅读(353)  评论(0编辑  收藏  举报