Spring Boot 整合 RabbitMQ

1、依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-amqp</artifactId>
                <version>${rabbitmq.version}</version>
            </dependency>

2、配置

spring:
  rabbitmq:
    addresses: 127.0.0.1
    username: guest  # 改账户只有在本机(localhost)下可以使用,远程环境需要自己新增帐号
    password: guest
    port: 5672
    virtual-host: /
    publisher-confirm-type: correlated
    publisher-returns: true
    # 将ack 自动应答改为手动应答
    listener:
      simple:
        acknowledge-mode: manual
        retry:
          # 开启重试 只有在自动ack模式下有效
          enabled: true
          # 最大重试次数
          max-attempts: 10
          # 重试间隔时间
          initial-interval: 2000ms

2.1、配置类配置(主要配置交换机和队列之间的绑定关系)

package com.weinigb.config;

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

import java.util.HashMap;
import java.util.Map;

/**
 * @author Wenigb
 * @version V1.0
 * @Package com.weinigb.config
 * @date 2021/10/4 下午5:58
 * @Copyright © 每天都是开心的一天呢
 */
@Configuration
public class RabbitMqConfiguration {
    @Bean
    public FanoutExchange fanoutExchange(){
        // 交换机名字,是否持久花,是否重启删除
        return new FanoutExchange("fanout_test_exchange",true,false);
    }
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("direct_exchange",true,false);
    }

    @Bean
    public Queue directQueue() {
        Map<String,Object> params = new HashMap<>();
        // 设置队列内消息的自动过期时间
        params.put("x-message-ttl",5000);
        // 设置队列的死信队列
        params.put("x-dead-letter-exchange", "队列名");
        // 设置死信队列的路由key
        params.put("x-dead-letter-routing-key","routekey");
        // 设置队列的最大消息数量
        params.put("x-max-length",5);
        // 队列名字 是否持久化
        return new Queue("direct_queuq", true);
    }
    @Bean
    public Queue testQueue(){
        return new Queue("test_queue", true);
    }
    @Bean
    public Binding binding(){
        // 队列名.to 交换机名字
        return BindingBuilder.bind(testQueue()).to(fanoutExchange());
    }

    @Bean
    Binding directBinding() {
        // with(路由key)
        return            BindingBuilder.bind(directQueue()).to(directExchange()).with("routeKey");
    }

}

2.2、使用注解进行关系绑定

// 在类上加注解实现关系绑定
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "队列名",durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = "交换机名称",type = ExchangeTypes.TOPIC),
        key = "路由key"
))
public class TopicSMSCOnsumer(){
    
}

3、调用方法

3.1、生产者

package com.weinigb;

import com.alibaba.nacos.common.utils.UuidUtils;
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;

import java.nio.charset.StandardCharsets;

/**
 * @author Wenigb
 * @version V1.0
 * @Package com.weinigb
 * @date 2021/10/4 下午6:06
 * @Copyright © 每天都是开心的一天呢
 */
@SpringBootTest
public class RabbitMqConnectTest {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Test
    public void sendOrder(){
        String orderId = UuidUtils.generateUuid();
        // 交换机名称
        String exchangeName = "fanout_test_exchange";
        // 路由key
        String routeKey = "";
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(exchangeName,routeKey,"Hello Spring Boot Rabbit Mq"+i);
        }
    }
}

3.2、消费者

package com.weinigb.service;

import jdk.internal.org.objectweb.asm.tree.analysis.Value;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Service;

/**
 * @author Wenigb
 * @version V1.0
 * @Package com.weinigb.service
 * @date 2021/10/4 下午6:17
 * @Copyright © 每天都是开心的一天呢
 */
@Service
//@RabbitListener(queues = {"test_queue"})
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "队列名",durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = "交换机名称",type = ExchangeTypes.TOPIC),
        key = "路由key"
))
public class RabbitMqTestService {
    @RabbitHandler
    public void reviceMessage(String message){
        System.out.println(message);
    }
}

4、消息可靠性配置

4.1、生产者

  • 利用发送消息之后mq返回的信息来进行回调判断。
package com.weinigb.config;

import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

/**
 * @author Wenigb
 * @version V1.0
 * @Package com.weinigb.config
 * @date 2021/10/5 上午10:33
 * @Copyright © 每天都是开心的一天呢
 */
@Configuration
public class RabbitMqTemplateConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * @PostConstruce 注解表示该类在生成放入ioc的时候会自动执行一遍该方法。
     * 在构造函数之后执行,在init方法执行之前执行
     */
    @PostConstruct
    public void init(){
        //指定 ConfirmCallback
        rabbitTemplate.setConfirmCallback(this);
        //指定 ReturnCallback
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    /**
     * 消息发送到交换机之后的异步调用
     * @param correlationData 响应数据 需要我们在发送消息的时候进行设定,一搬用来存放发送消息的关键数据
     * @param ack 发送消息是否成功 成功为ture 失败为fasle
     * @param cause 表示发送失败的时候返回的错误信息 成功为null
     * 注: 需要在配置文件(.yml)中开启配置     publisher-confirm-type: correlated
     */
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            System.out.println(correlationData.getId()+"  "+ack+"  "+cause);
    }

    @Override
    /**
     * 交换机投递消息到消息队列失败的异步回调
     * @param returned
     * 需要在配置文件重开启   
     #开启消息的return机制
    	publisher-returns: true
     #在需要使用消息的return机制时候,此参数必须设置为true
    	template:
       mandatory: true
     */
    public void returnedMessage(ReturnedMessage returned) {
        // 消息
        System.out.println(returned.getMessage());
        // 交换机
        System.out.println(returned.getExchange());
        // 路由key
        System.out.println(returned.getRoutingKey());
        // 错误码
        System.out.println(returned.getReplyCode());
        // 错误信息
        System.out.println(returned.getReplyText());
    }
}

4.2、消费者

  • 通过手动ack来进行消息的正确消费
  • 注意需要开启配置 具体配置看上面的 第二部分
package com.weinigb.service;

import com.rabbitmq.client.Channel;
import jdk.internal.org.objectweb.asm.tree.analysis.Value;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * @author Wenigb
 * @version V1.0
 * @Package com.weinigb.service
 * @date 2021/10/4 下午6:17
 * @Copyright © 每天都是开心的一天呢
 */
@Service
@RabbitListener(queues = {"test_queue"})
//@RabbitListener(bindings = @QueueBinding(
//        value = @Queue(value = "队列名",durable = "true",autoDelete = "false"),
//        exchange = @Exchange(value = "交换机名称",type = ExchangeTypes.TOPIC),
//        key = "路由key"
//))
public class RabbitMqTestService {
    /**
     *
     * @param message 队列中的消息;
     * @param channel 当前的消息队列;
     * @param tag 取出来当前消息在队列中的的索引,
     * 用这个@Header(AmqpHeaders.DELIVERY_TAG)注解可以拿到;
     * @throws IOException
     */
    @RabbitHandler
    public void reviceMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        System.out.println("long:->"+tag+"  消息:"+message);
        try {
            /**
             * 无异常就确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:取出来当前消息在队列中的的索引;
             * multiple:为true的话就是批量确认,如果当前deliveryTag为5,那么就会确认
             * deliveryTag为5及其以下的消息;一般设置为false
             */
            channel.basicAck(tag,true);
        } catch (IOException e) {
            /**
             * 有异常就绝收消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * requeue:true为将消息重返当前消息队列,还可以重新发送给消费者;
             *         false:将消息丢弃
             */
            channel.basicNack(tag,false,false);
            e.printStackTrace();
        }
    }
}

4.3、具体的实现逻辑

# 方案一:利用Confirm消息确认机制
	前面讲完了RabbitMQ自带的Confirm消息确认机制和Return机制,而实现消息可靠性投递的第一个方案就是利用该确认机制

实现思路如下:
	1、在生产端向消息队列发送消息前,首先将业务信息和对应的消息信息入库(如生成订单时,需要修改数据库中的订单表和订单消息表),其中订单消息表中有一个记录该消息是否发送成功的字段
	2、向消息队列发送该消息,并在发送前设置好Confirm消息的监听器
	3、如果收到Confirm消息,代表该消息已发送成功,那么就可以将订单消息表中的发送状态改为发送成功
	4、设置一定时任务去抓取订单消息表中没有没有发送成功的消息,并进行重新发送
	5、如果重新发送了几次后消息都没有发送成功,则将其状态修改为发送失败,后续进行人工补偿
	该方案的缺点是在发送消息前,需要进行两次落库操作(修改数据库中的订单表和订单消息表),因此会对性能造成一定影响
# 方案二:消息延迟投递,做二次确认,回调检查
实现思路如下:

	1、生产者消息发送前,只需要将业务信息入库(如修改订单表)
	2、向MQ发送该消息以及一条延迟消息(其中第一条消息由消费者接收,第二条消息由独立的Callback服务接收)
	3、消费者收到第一条消息后,将接收成功的消息回送给MQ(该回送消息也由Callback服务接收)
	4、Callback服务收到消费者接收成功的消息,将该消息入库
	5、一段时间后,Callback服务又收到生产者的延迟消息,它根据该延迟消息的id信息去查找数据库中有没有该条记录
	6、如果查到了,说明该消息已成功投递且被消费者成功消费
	7、如果没查到,说明该消息没有被消费者成功消费,可能是没有投递成功,这时Callback服务再去远程调用生产者告知其重新发送消息
	该方案优点是在生产者端只需要入库一次,而将消息的入库操作独立到了Callback服务中去,提升了生产者端的性能。但是该方案实现较复杂,里面还有很多的细节值得考虑