springboot集成rabbitmq,开启手工确认保证消息100%投递

目录

首先在pom.xml中添加如下依赖

在application.yml 中添加rabbitmq相关参数

创建RabbitConfig

首先创建交换机,队列,并进行绑定

重新配置RabbitTemplate

设置消息监听

开启手动确认处理消息

创建消息的接收者

创建RabbitSenderController类进行测试

模拟各种错误的情况

producer ---> exchange 指定一个不存在的交换机

exchange --> queue 指定一个不存在的routingKey

注释掉RabbitReceiver手工确认代码

模拟消费出错,重新放入队列的情况.

遇到的问题

不足之处

源码地址

 

 


 

 

 


首先在pom.xml中添加如下依赖

       <dependency>
			<groupId>org.springframework.boot</groupId>
			    <artifactId>spring-boot-starter-web</artifactId>
			</dependency>
	   <dependency>
			   <groupId>org.springframework.boot</groupId>
			   <artifactId>spring-boot-starter-amqp</artifactId>
	    </dependency>
		<dependency>
	            <groupId>org.springframework.boot</groupId>
	            <artifactId>spring-boot-starter-test</artifactId>
	            <scope>test</scope>
	    </dependency>
	    <dependency>
	            <groupId>com.alibaba</groupId>
	            <artifactId>fastjson</artifactId>
	            <version>1.2.47</version>
	    </dependency>
	    <dependency>
	            <groupId>org.projectlombok</groupId>
	            <artifactId>lombok</artifactId>
	            <version>1.18.2</version>
	            <scope>provided</scope>
	    </dependency>

在application.yml 中添加rabbitmq相关参数

server:
  port: 9999
spring:
   rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    publisher-confirms: true
    publisher-returns: true

publisher-confirms:开启confirms回调 P -> Exchange

publisher-returns:  开启returnedMessage回调 Exchange -> Queue

创建RabbitConfig

首先创建交换机,队列,并进行绑定

Queue一共有5个参数,每个参数的含义如下:

Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)

  • name:队列名称
  • durable:队列是否持久化
  • exclusive:队列是否专属,专属的范围针对的是连接,也就是说,一个连接下面的多个信道是可见的.对于其他连接是不可见的
  • autoDelete:如果所有消费者都断开连接了,是否自动删除
  • arguments:队列配置

这里我们直接创建了一个主题交换机,参数和队列类似:

 TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)

  • name:交换机名称
  • durable:队列是否持久化
  • autoDelete:如果所有消费者都断开连接了,是否自动删除
  • arguments:交换机配置

BindingBuilder.bind 有三个参数,第一个是队列,第二个是交换机,第三个是binding key

    /**
     * 创建队列
     */
    @Bean
    public Queue queueForSingleMode() {
        return new Queue("quene.acknowledgemode.manualtest");
    }

    /**
     * 创建主题交换机
     */
    @Bean
    public TopicExchange createTopicExchange() {
        return new TopicExchange("opic.exchange.test");
    }

    /**
     * 将队列与交换机进行绑定
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queueForSingleMode()).to(createTopicExchange()).with("manualtest");
    }

重新配置RabbitTemplate

使用Jackson2JsonMessageConverter 消息传递后转对象

设置producer ---> exchange 消息发送确认回调方法   

设置exchange --> queue   消息发送确认回调方法   

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        /**
         * producer ---> exchange 消息发送确认回调方法   
         * 如果消息没有到exchange,则confirm回调,ack=false
         * 如果消息到达exchange,则confirm回调,ack=true
         */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            StringBuffer sBuffer = new StringBuffer();
            sBuffer.append("correlationId = ").append(correlationData.getId()).append(" ,是否消息发送确认成功  = ").append(ack)
                    .append("");

            if (!ack) {
                sBuffer.append("消息失败原因: ").append(cause);
            }
            logger.info("producer ---> exchange 消息confirm回调: {}", sBuffer.toString());
        });

        // 触发setReturnCallback回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
        rabbitTemplate.setMandatory(true);
        /**
         * exchange --> queue   消息发送确认回调方法   
         * exchange到queue成功,则不回调return
         * exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)
         */
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            StringBuffer sBuffer = new StringBuffer();
            sBuffer.append("return--message:").append(new String(message.getBody())).append(",replyCode:")
                    .append(replyCode).append(",replyText:").append(replyText).append(",exchange:").append(exchange)
                    .append(",routingKey:").append(",routingKey:").append(routingKey);
            logger.info("exchange --> queue 消息return回调: {}", sBuffer.toString());
        });

        return rabbitTemplate;
    }

设置消息监听

    /**
     *  AcknowledgeMode.AUTO ,它会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)
     *  如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
     *  当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
     *  当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
     *  其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,
     *  则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。
     *  可以通过 setDefaultRequeueRejected(默认是true)去设置
     * @param connectionFactory
     * @return
     */
    @Bean
    public SimpleMessageListenerContainer messageContainer(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setQueues(queueForSingleMode());// 监听的队列
        container.setExposeListenerChannel(true);
        container.setMaxConcurrentConsumers(30);//每个listener最大并发消费者数,对那种对消息的顺序有苛刻要求的场景不适合并发消费
        container.setConcurrentConsumers(1);//对每个listener在初始化的时候设置的并发消费者数量

        /**
         * prefetch是每次从一次性从broker里面取的待消费的消息的个数<br>
         * 在使用了acknowledge消息确认机制情况下,使用prefetch_count进行流量控制
         */
        container.setPrefetchCount(30);
        container.setConnectionFactory(connectionFactory);
        container.setMessageConverter(new Jackson2JsonMessageConverter());
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);// 程序手动确认
        container.setMessageListener(rabbitReceiver);
        return container;
    }

开启手动确认处理消息

    /**
     * 使用 JSON 序列化化消息
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        /**
         *开启手动确认处理消息
         */
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        return factory;
    }

创建消息的发送者

发送消息的时候使用雪花算法生成消息id

package com.example.rabbitdemo.producer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.rabbitdemo.entity.MessageEntity;
import com.example.rabbitdemo.util.SnowFlake;

@Service
public class RabbitSender {
    private static Logger  logger = LoggerFactory.getLogger(RabbitSender.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private SnowFlake      snowFlake;

    public void send(String message, String type, String exchangeName, String routeKey) {
        MessageEntity<String> messageModel = new MessageEntity<String>();
        logger.debug("exchangeName: {}  ,发送内容 ' {} '", exchangeName, message);
        messageModel.setType(type);
        messageModel.setMessage(message);
        CorrelationData correlationData = new CorrelationData(String.valueOf(snowFlake.nextId()));
        rabbitTemplate.convertAndSend(exchangeName, routeKey, messageModel, correlationData);

    }

}

创建消息的接收者

1.接收消息的时候需要判断是否重复消费,可以吧已经消费的id放入缓存中,如果缓存中已经存在相同的id就不在消费,只进行确认。如果不存在确认的同时进行消费。

2.如果消息消费失败,第一次重新放入到队列中。如果第二次还是失败,则拒绝该消息。这里可以添加一个监控,把错误信息保存到日志表中,进行人工干预。如果这里失败后不判断直接重新返回队列中消费者只有一个的情况下就会造成死循环。

package com.example.rabbitdemo.consumer;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.context.annotation.Configuration;

import com.alibaba.fastjson.JSON;
import com.example.rabbitdemo.entity.MessageEntity;
import com.rabbitmq.client.Channel;

@Configuration
public class RabbitReceiver implements ChannelAwareMessageListener {

    private static Logger logger = LoggerFactory.getLogger(RabbitReceiver.class);

    public void onMessage(Message message, Channel channel) throws IOException {
        try {
            logger.info("消费端接收到消息:" + message.getMessageProperties() + ":" + new String(message.getBody()));
            logger.info("DeliveryTag:" + message.getMessageProperties().getDeliveryTag());

            @SuppressWarnings("unchecked")
            MessageEntity<String> messageModel = (MessageEntity<String>) JSON.parseObject(new String(message.getBody()),
                    MessageEntity.class);
            //处理具体的业务逻辑 
            /**
             * 这里需要判断是否重复消费,可以吧已经消费的id放入缓存中,如果缓存中已经存在相同的id就不在消费
             */
            if (messageModel.getType().equals("savelog")) {
                logger.info("消费端接收到消息类型: {}, 消息: {}", messageModel.getType(), messageModel.getMessage());
            }
            /**false只确认当前一个消息收到,true确认所有consumer获得的消息*/
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            logger.info("错误信息:{}", e.getMessage());
            if (message.getMessageProperties().getRedelivered()) {
                logger.error("消息已重复处理失败,拒绝再次接收...");
                //第二个参数是是否放回queue中,requeue如果只有一个消费者的话,true将导致无限循坏
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
            } else {
                logger.warn("消息即将再次返回队列处理...");
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); // requeue为是否重新回到队列
            }

        }
    }

}

创建RabbitSenderController类进行测试

 

package com.example.rabbitdemo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.rabbitdemo.producer.RabbitSender;

@RestController
@RequestMapping("/rabbit")
public class RabbitSenderController {
    @Autowired
    RabbitSender rabbitSender;

    @PostMapping("/send")
    public void send(String message, String type, String exchangeName, String routeKey) {
        rabbitSender.send(message, type, exchangeName, routeKey);

    }
}

http://localhost:9999/rabbit//send?message=输入你要测试的信息&type=savelog&exchangeName=opic.exchange.test&routeKey=manualtest 

模拟各种错误的情况

producer ---> exchange 指定一个不存在的交换机

exchange --> queue 指定一个不存在的routingKey

注释掉RabbitReceiver手工确认代码

 // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

看后台已经成功消费了,我们看看rabbitmq的管理界面,发现还有一条信息

把注释打开,重新启动程序。后台日志看到刚刚没有被确认的又消费了一次,所以这个需要加上幂等的校验。

模拟消费出错,重新放入队列的情况.

重新放入队列后如果还是失败,则拒绝该消息。否则发生死循环,这里可以添加监控人工干预。

 

 

从日志可以看到发生错误后又重新进入了队列,然后第二次进来的时候就直接拒绝了

遇到的问题

Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - unknown delivery tag 1, class-id=60, method-id=120)

出现这种错误有两个原因:

  1. SimpleRabbitListenerContainerFactory 使用 JSON 序列化化消息覆盖了application.yml 开启手动确认配置,解决方法是重新设置setAcknowledgeMode(AcknowledgeMode.MANUAL)
  2. 一个消息由于某种原因被手工确认了两次。

 

不足之处

该程序只是一个简单的demo,仅供学习使用。程序中有很多硬编码的地方。比如创建队列,交换机及绑定,还有消息的处理的地方。如果用于生产可以考虑吧生产者,消费者与具体的业务代码进行剥离,创建两个单独的工程。还要考虑如何动态创建不同类型的队列,交换机及绑定,以及进行消息的处理。

源码地址

https://download.csdn.net/download/xinghui_liu/12850759

 

 

 

Guess you like

Origin blog.csdn.net/xinghui_liu/article/details/108614972