Springboot整合Rabbitmq实战


声明: 记录工作和学习内容,便于自己查看,也供大家参考

1.MQ

1.1概述

MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。

  • 常规服务调用
    在这里插入图片描述

  • 异步服务调用
    在这里插入图片描述

1.2MQ优势和劣势

  • 优势: 异步 解耦 削峰
  1. 异步: 访问速度更快,用户体验更好,提升系统吞吐量
    在这里插入图片描述

    在这里插入图片描述

第一种操作,总耗时920ms,第二种操作,总耗时25ms,大大提升了用户体验

  1. 解耦: 提升容错性和可维护性

在这里插入图片描述

在这里插入图片描述

第一种方式一个系统崩溃,导致关联系统同时崩溃,第二种方式,则不会,耦合性降低

  1. 浏量消峰: 可提升系统的稳定性
    请求过多时,将请求消息积压在MQ中,再依次少量的获取请求进行处理,避免了将系统压死的情况发生
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 劣势: 降低系统可用性,提高系统复杂度,一致性问题
    1. 降低系统可用性: 引入的外部依赖越多,出问题的几率就越大,故搭建MQ集群就很关键
    2. 提高系统复杂度
    3. 一致性问题: 只能达到最终一致

1.3常见MQ产品

在这里插入图片描述
常规开发使用rabbitmq原因: Erlang开发语言,不丢消息,消息延迟最低,虽然阿里的RocketMQ吞吐量大,但是不开源,如后期不维护了,存在风险,而rabbitMQ社区活跃也是开源的,是最好的选择,kafka主要是用于大数据领域准备的,功能没有这个完善

2.RabbitMq5种工作模式

  • AMQP :即Advanced Message Queuing Protocol(高级消息队列协议),网络协议,是应用层协议的一个开放标准 类比HTTP协议
  • JMS : 即JavaMessage Service 是一个java面向消息中间件的API接口 类比JDBC
    很多消息中间件都实现了JMS规范,如ActiveMQ. RabbitMQ没有实现,但是开源社区有
  • rabbitmq6提供了6种工作模式
    在这里插入图片描述
  • rabbitmq 基础架构图
    在这里插入图片描述

2.1 hello模式

一个发送者,一个消费者,一个队列,一对一的关系进行发送
在这里插入图片描述

  1. 创建rabbit-common工程(公共模块),用于定义公共的Bean

  2. 公共模块引入依赖

<!-- rabbitmq -->
<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 公共模块创建配置类,用于定义队列和交换机等
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Configuration
public class RabbitConfig {
    public static final String HELLO_QUEUE = "hello_queue";

    @Bean
    public Queue helloQueue() {
        // 两种创建方式,第二参数true为持久化
        //return new Queue(HELLO_QUEUE, true);
        
        // durable 即持久化
        return QueueBuilder.durable(HELLO_QUEUE).build();
    }
    
}
  1. 公共模块的resources下创建目录
META-INF\spring
  1. 并在改目录下创建文件,并指定需要自定加载的bean路径
org.springframework.boot.autoconfigure.AutoConfiguration.imports

在这里插入图片描述

在这里插入图片描述

上面两步解决公共引入公共模块加载不到bean的问题,方便消息发送方和消息消费方引入该公共模块,避免重复定义一样的Bean

  1. 创建发送方工程,引入公共模块
<dependency>
    <groupId>com.ruoyi</groupId>
    <artifactId>rabbit-common</artifactId>
</dependency>
  1. 发送方配置文件
server:
  port: 8001
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
  1. 编写发送消息方法
  • service接口
public interface MqPublisherService {

    void sendMsg(String queue,Object msg);
}
  • service实现
import com.ruoyi.rabbit.publisher.service.MqPublisherService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Service
public class MqPublisherServiceImpl implements MqPublisherService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void sendMsg(String queue, Object msg) {
        rabbitTemplate.convertAndSend(queue,msg);
    }
}
  • controller
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import com.ruoyi.rabbit.publisher.service.MqPublisherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@RestController
@RequestMapping("/mq/publisher")
public class MqPublisherController {

    @Autowired
    private MqPublisherService mqPublisherService;

    @GetMapping("/sendHelle")
    public String sendHelle() {
        mqPublisherService.sendMsg(RabbitConfig.HELLO_QUEUE,"hello world");
        return "ok";
    }
}
  1. 测试
    http://localhost:8001/mq/publisher/sendHelle
    有消息进入MQ在这里插入图片描述
    有对应的 hello_queue 队列
    在这里插入图片描述
  2. 消费方工程创建,引入公共模块依赖,同发送方
  3. 消费方配置,同发送方,server.port改为8002
  4. 消费方监听队列
import com.rabbitmq.client.Channel;
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Component
public class HelloListener {

    @RabbitListener(queues = {RabbitConfig.HELLO_QUEUE}) // 指定监听的队列
    public void helloReceiver(Message message, Channel channel, String msg) {
    	// Message对象中包含消息的各种信息也包括了这里的msg
    	// Channel对象用于调用手动确认等方法
    	// 会自动将消息转换为指定的类型
        System.out.println("hello 监听消息收到: " + msg);
    }
}
  1. 启动消费方项目,控制台打印
    在这里插入图片描述

2.2 work模式

同hello模式类似,只是多了一个或多个消费者对同一队列进行监听, 一条消息只能由一个消费者进行消费,默认使用轮询的方式
在这里插入图片描述

  1. 只需要在消费方加一个监听
import com.rabbitmq.client.Channel;
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Component
public class HelloListener {

    @RabbitListener(queues = {RabbitConfig.HELLO_QUEUE})
    public void helloReceiver(Message message, Channel channel, String msg) {
        System.out.println("hello 监听消息收到: " + msg);
    }

    // 新增一个对hello队列的监听
    @RabbitListener(queues = {RabbitConfig.HELLO_QUEUE})
    public void helloReceiver2(Message message, Channel channel, String msg) {
        System.out.println("hello2 监听消息收到: " + msg);
    }

}
  1. 测试
    多次调用发送消息 http://localhost:8001/mq/publisher/sendHelle
    在这里插入图片描述

2.3 fanout模式

广播模式,也称发布订阅模式,交换机会将消息发送给所有绑定的队列
在这里插入图片描述

  1. 在RabbitConfig中配置exchange,queue,binding
package com.ruoyi.rabbit.common.conf;

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

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Configuration
public class RabbitConfig {
    
    // fanout ====================================

    public static final String FANOUT_EXCHANGE = "fanout_exchange";
    public static final String FANOUT_QUEUE_1 = "fanout_queue_1";
    public static final String FANOUT_QUEUE_2 = "fanout_queue_2";

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE, true, false);
    }

    @Bean
    public Queue fanoutQueue1() {
        return new Queue(FANOUT_QUEUE_1, true);
    }

    @Bean
    public Queue fanoutQueue2() {
        return new Queue(FANOUT_QUEUE_2, true);
    }

    @Bean
    public Binding fanoutBinding1() {
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
    }

    @Bean
    public Binding fanoutBinding2() {
        return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
    }

    // fanout ====================================

}

  1. 发送者代码
@GetMapping("/sendFanout")
public String sendFanout() {
    rabbitTemplate.convertAndSend(RabbitConfig.FANOUT_EXCHANGE, null, "hello fanout");
    return "fanout ok";
}
  1. 接收者监听对应队列
import com.rabbitmq.client.Channel;
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Component
public class FanoutListener {

    @RabbitListener(queues = {RabbitConfig.FANOUT_QUEUE_1})
    public void directReceiver(Message message, Channel channel, String msg) {
        System.out.println("fanout1 监听消息收到: " + msg);
    }

    @RabbitListener(queues = {RabbitConfig.FANOUT_QUEUE_2})
    public void directReceiver2(Message message, Channel channel, String msg) {
        System.out.println("fanout2 监听消息收到: " + msg);
    }
}
  1. 测试
    两个监听器都收到了消息
    在这里插入图片描述

2.4 direct模式

发送者将消息发送到交换机,并指定对应的routing_key,由交换机根据routing_key去匹配对应的队列

在这里插入图片描述

  1. 在RabbitConfig中配置exchange,queue,binding
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Configuration
public class RabbitConfig {
    
    // direct ====================================

    public static final String DIRECT_EXCHANGE = "direct_exchange";
    public static final String DIRECT_QUEUE_1 = "direct_queue_1";
    public static final String DIRECT_QUEUE_2 = "direct_queue_2";
    public static final String DIRECT_ROUTING_KEY_1 = "direct_routing_key_1";
    public static final String DIRECT_ROUTING_KEY_1_2 = "direct_routing_key_1_2";
    public static final String DIRECT_ROUTING_KEY_2 = "direct_routing_key_2";

    @Bean
    public DirectExchange directExchange() {
        // 参数1,name:exchange的名称; 参数2,durable:持久化; 参数3:autoDelete:自动删除
        //return new DirectExchange(DIRECT_EXCHANGE, true, false);

        // 方式二
        return ExchangeBuilder.directExchange(DIRECT_EXCHANGE).durable(true).build();
    }

    @Bean
    public Queue directQueue1() {
        return new Queue(DIRECT_QUEUE_1, true);
    }

    @Bean
    public Queue directQueue2() {
        return new Queue(DIRECT_QUEUE_2, true);
    }

    @Bean
    public Binding bindingDirect1() {
        return BindingBuilder.bind(directQueue1()).to(directExchange()).with(DIRECT_ROUTING_KEY_1);
    }

    // directQueue1队列和directExchange交换机间绑定了两个routingKey
    @Bean
    public Binding bindingDirect1_2() {
        return BindingBuilder.bind(directQueue1()).to(directExchange()).with(DIRECT_ROUTING_KEY_1_2);
    }

    @Bean
    public Binding bindingDirect2() {
        return BindingBuilder.bind(directQueue2()).to(directExchange()).with(DIRECT_ROUTING_KEY_2);
    }

    // direct ====================================
}

说明: directQueue1 队列和 directExchange 交换机间绑定了两个routingKey,分别:direct_routing_key_1 和 direct_routing_key_1_2,当发送者往directExchange 交换机发送消息指定上面两个routingkey时,都会发送到 directQueue1 这个消息队列

  1. 发送者代码
// controller
@GetMapping("/sendDirect")
public String sendDirect() {
    mqPublisherService.sendMsg(RabbitConfig.DIRECT_EXCHANGE,RabbitConfig.DIRECT_ROUTING_KEY_1,"hello direct");
    return "direct ok";
}

// sevice
@Override
public void sendMsg(String exchange, String routingKey, Object msg) {
    rabbitTemplate.convertAndSend(exchange,routingKey,msg);
}
  1. 消费者代码: 监听器
import com.rabbitmq.client.Channel;
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Component
public class DirectListener {

    @RabbitListener(queues = {RabbitConfig.DIRECT_QUEUE_1})
    public void directReceiver(Message message, Channel channel, String msg) {
        System.out.println("direct1 监听消息收到: " + msg);
    }

    @RabbitListener(queues = {RabbitConfig.DIRECT_QUEUE_2})
    public void directReceiver2(Message message, Channel channel, String msg) {
        System.out.println("direct2 监听消息收到: " + msg);
    }
}

2.5 topic模式

同direct模式,只是routing_key可以使用通配符
*星号:表示有且仅有一个单词
#井号:表示任意个数单词
在这里插入图片描述

  1. 在RabbitConfig中配置exchange,queue,binding
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Configuration
public class RabbitConfig {
    
    // topic ====================================

    public static final String TOPIC_EXCHANGE = "topic_exchange";
    public static final String TOPIC_QUEUE_MAN = "topic_queue_man";
    public static final String TOPIC_QUEUE_ALL = "topic_queue_all";
    public static final String TOPIC_ROUTING_KEY_MAN = "topic.man";
    public static final String TOPIC_ROUTING_KEY_WOMAN = "topic.woman";
    public static final String TOPIC_ROUTING_KEY_ALL = "topic.*";
    
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE, true, false);
    }

    @Bean
    public Queue topicQueueMan() {
        return new Queue(TOPIC_QUEUE_MAN, true);
    }
    
    @Bean
    public Queue topicQueueAll() {
        return new Queue(TOPIC_QUEUE_ALL, true);
    }

    @Bean
    public Binding topicBindingMan() {
        return BindingBuilder.bind(topicQueueMan()).to(topicExchange()).with(TOPIC_ROUTING_KEY_MAN);
    }
    
    @Bean
    public Binding topicBindingALL() {
        return BindingBuilder.bind(topicQueueAll()).to(topicExchange()).with(TOPIC_ROUTING_KEY_ALL);
    }
    
    
    // topic ====================================
}
  1. 发送者
@GetMapping("/sendTopic")
public String sendTopic() {
    //rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE, RabbitConfig.TOPIC_ROUTING_KEY_MAN, "hello topic");
    rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE, RabbitConfig.TOPIC_ROUTING_KEY_WOMAN, "hello topic");
    return "topic ok";
}
  1. 接收者
import com.rabbitmq.client.Channel;
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Component
public class TopicListener {

    @RabbitListener(queues = {RabbitConfig.TOPIC_QUEUE_MAN})
    public void directReceiver(Message message, Channel channel, String msg) {
        System.out.println("fanout man 监听消息收到: " + msg);
    }

    @RabbitListener(queues = {RabbitConfig.TOPIC_QUEUE_ALL})
    public void directReceiver2(Message message, Channel channel, String msg) {
        System.out.println("topic all 监听消息收到: " + msg);
    }
}
  1. 测试
    指定路由为man,两个队列都收到消息
    指定路由为woman时,只有all收到了消息
    在这里插入图片描述

3. 消息可靠性投递

作用: 发送方 保证消息发送成功

3.0 消息传递路径

producer—>rabbitmq broker—>exchange—>queue—>consumer

消息从 producer 到 exchange 则会返回一个 confirmCallback
消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。

3.1 发送方 – 保证发送成功

  • confirm 模式: 保证消息成功发送到 交换机
  • return 模式: 保证消息成功发送到 消息队列
  1. 修改发送方yml配置
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated # 开启确认模式
    publisher-returns: true # 开启退回模式
  1. 配置RabbitTemplate
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

/**
 * desc:
 *
 * @author qts
 * @date 2023/4/3 0003
 */
@Configuration
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnsCallback {
    private static final Logger log = LoggerFactory.getLogger(RabbitTemplateConfig.class);


    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /** 
    * 实现confirm回调,发送和没发送到exchange,都触发 (前提: 确认模式开启:yml中publisher-confirm-type: correlated)
    * CorrelationData数据可以在rabbitTemplate.convertAndSend时传入 并这种CorrelationData的setId参数,回调时能取到
    */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        // 参数说明:
        // correlationData: 相关数据,可以在发送消息时,进行设置该参数
        // ack: 结果
        // cause: 原因

        if (ack) {
            log.info("【ConfirmCallback】消息已经送达Exchange,ack已发");
        } else {
            log.warn("【ConfirmCallback】消息没有送达Exchange");
            // todo 做一些处理,让消息再次发送。 消息缓存或入库,邮件提醒运维
        }
    }

    // 实现return回调:当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行 ReturnCallBack (前提:退回模式开启:yml中publisher-returns: true)
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        // 返回参数说明
        //String exchange = returned.getExchange(); // 该消息指定的 exchange
        //String routingKey = returned.getRoutingKey(); // 该消息指定的 routingKey
        //Message message = returned.getMessage(); // 消息对象
        //int replyCode = returned.getReplyCode(); // 回应 code
        //String replyText = returned.getReplyText(); // 回应 内容

        log.warn("【ReturnsCallback】消息没有送到队列中");
        // todo 处理 邮件发送,缓存或存到数据库
    }
}

3.2 可靠性投递小结

一般需要对mq做集群保证消息中的exchange和queue不会挂掉从而导致无法发送消息到exchange 和 queue 的情况,避免发生此异常请求,我们需要启动对消息的发送进行监控,使用confirm模式和return模式监控消息到达exchange和queue的情况,出现异常则进行邮件通知运维,并记录消息内容到缓存或数据库

3.2.1 确认模式总结

  1. yml 配置开启确认模式
spring:
  rabbitmq:
    publisher-confirm-type: correlated
  1. rabbitTemplate.setConfirmCallback 设置确认回调函数, ack为true则发送成功, ack为false则发送失败,进行邮件通知运维,并记录数据到缓存或数据库,

3.2.2 退回模式总结

  1. yml配置退回模式
spring:
  rabbitmq:
    publisher-returns: true
  1. 设置rabbitTemplate.setMandatory(true) 消息从exchange发送到queue失败后,才会执行ReturnsCallback回调函数
  2. rabbitTemplate.setReturnsCallback设置退回的回调函数,进行处理

可靠性投递参考博客
参考博客

4. 防止消息丢失

作用: 确保消费方消费成功,确保消息不会因为网络问题,而导致消息丢失。

4.1 设置exchange和queue的持久化

定义exchange,和queue时,指定 durable 参数true

4.2 消费方的手动确认

  1. 消费方yml配置文件
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动确认(默认自动确认)
        concurrency: 1 # 监听器调用程序线程的最小数量(即每个@RabbitListener开启几个线程去处理。如有两个@RabbitListener指向同一队列,并且concurrency=2,则有4个线程同时处理4条消息)
        max-concurrency: 10 # 监听器调用程序线程的最大数量
        prefetch: 250 # 消费方限流:每个消费者可以处理的未确认消息的最大数量,提前拉取消息,不代表消费消息

  1. 消费方监听调用手动确认
    channel.basicAck 确认, basicNack拒绝确认(多一个multiple参数,批量拒绝), basicReject拒绝确认
import com.rabbitmq.client.Channel;
import com.ruoyi.rabbit.common.conf.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * desc:
 *
 * @author qts
 * @date 2023/3/31 0031
 */
@Component
public class HelloListener {

    @RabbitListener(queues = {RabbitConfig.HELLO_QUEUE})
    public void helloReceiver(Message message, Channel channel, String msg) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println("hello 监听消息收到: " + msg);

            // 正常处理后,手动进行确认 ,第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
            channel.basicAck(deliveryTag,false);
        } catch (Exception e) {
            //消费者处理出了问题,需要告诉队列信息消费失败,
            /**
              * 拒绝确认消息:<br>
              * channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) ; <br>
              * deliveryTag:该消息的index<br>
              * multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。<br>
              * requeue:被拒绝的是否重新入队列 <br>
              */
            channel.basicNack(deliveryTag,false,true);

            /**
            * 拒绝一条消息:<br>
            * channel.basicReject(long deliveryTag, boolean requeue);<br>
            * deliveryTag:该消息的index<br>
            * requeue:被拒绝的是否重新入队列
            */
            //channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            e.printStackTrace();
        }

    }

}

5. 消费方限流

作用: 防止系统一次处理过多请求,导致系统崩溃
操作: 使用两个参数进行控制 prefetchconcurrency

prefetch参数

  1. 是每个消费者一次性从broker中取出的消息个数,存入内存中的阻塞队列里,等待当前消费者进行消费,
  2. 好处是减少消息传输的用时, 一次拉取部分消息在内存中,挨着消费即可,不用消费一个拉取一个,但不会提升消息的处理速度,

concurrency参数

  1. 设置并发消费者的个数,可以进行初始化-最大值动态调整,并发消费者可以提高消息的消费能力,防止消息的堆积
  2. 可以在注解@RabbitListener 中设置 concurrency 参数(推荐),也可在yml配置文件中设置全局(不推荐,每个消费者处理情况不同)
// yml中是全局设置, 注解中是单个消费者设置,更推荐注解方式设置
// 线程个数可以动态伸缩 最小1,最多4 , 也可设置为 concurrency = "4" , 指定4个线程并发处理消息
// 买个线程等于一个监听, 每个监听都会拉取指定的prefetch个消息到自己的阻塞队列中等待消费
@RabbitListener(queues = {RabbitConfig.BATTLE_PAPER_QUEUE},concurrency = "1-4") 

参考博客:RabbitMQ并发消费者关键参数prefetch,concurrency

前提: 消费端是手动确认的 acknowledge= “manual”
配置: 在消费方yml配置中设置prefetch参数,默认250, 如prefetch=“1” 会在手动确后再拉取下一次,代表每个@RabbitListener监听每次预加载多少消息到内存中等待消费的最大值,不是每次都必须抓取prefetch所设置的值

参考博客
参考博客

6. 消息过期时间 TTL – 发送方

6.1 队列整体过期

时间到了,所有消息被删除
操作: 创建队列queue时,指定参数 x-message-ttl 并设置值
在这里插入图片描述
第5个参数通过map的方式进行设置,或者使用建造者模式创建

代码

// 过期时间 设置 ==============================

    @Bean
    public Queue ttlQueue() {
        // 方式一: new 方式
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl",5000);// 设置过期时间
        //return new Queue("ttlQueueName",true,false,false,args);

        // 方式二: 建造模式
        return QueueBuilder.durable("ttlQueueName").ttl(5000).build();

    }

    // 过期时间 设置 ==============================

等价与rabbitMQ中的如下操作
在这里插入图片描述

6.2 单条消息过期

时间到了,当消息在队列顶端时(即将消费时)再判断,并删除
操作: 发送消息时,通过参数 messagePostProcessor 指定处理器,并对Message对象中的属性进行修改

@GetMapping("sendTtlFanout")
    public String sendTtl() {
        
        // 该方法在消息转换成 Message对象后, 发送到交换机之前,进行回调
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置消息的到期时间
                message.getMessageProperties().setExpiration("5000"); 
                return message;
            }
        };

        // 使用MessagePostProcessor方式,就不用自己去传Message对象,covertAndSend内可以自动进行转换
        rabbitTemplate.convertAndSend(RabbitConfig.FANOUT_EXCHANGE,null,"ttl fanout",messagePostProcessor);
        return "ttl ok";
    }

6.3 两者都设置,以时间短的为准

7 死信队列

7.1概述

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机)
死信交换机: 就是一个普通的交换机,只是用来处理死信而已
死信: 无法被消费的消息
在这里插入图片描述

7.2 成为死信的情况

  1. 消息长度达到上限,以后的消息就会成为死信,直接进入死信队列
  2. 被拒收的消息,并设置了不重回队列,requeue 值为 false
  3. 过期的消息

7.3 应用场景

  1. 订单30分钟未支付,则取消订单并回滚库存
  2. 新用户注册成功7天后,短信回访
    在这里插入图片描述

7.4 代码

// 死信队列 ==================================
    public static final String DLX_EXCHANGE = "dlx_exchange";
    public static final String DLX_ROUTING_KEY = "dlx_routing_key";
    public static final String DLX_Queue = "dlx_queue";
    public static final String TTL_Queue = "ttl_queue";

	// 5秒过期后,会将消息发送到指定的 DLX_EXCHANGE 交换机上,并指定routingkey,消息会有 DLX_EXCHANGE 按照指定的 routingkey路由到对应的Queue上
    @Bean
    public Queue ttlQueue2() {

        return QueueBuilder.durable(TTL_Queue) //持久化
            .ttl(5000) // 5秒过期
            .deadLetterExchange(DLX_EXCHANGE) // 指定死信交换机
            .deadLetterRoutingKey(DLX_ROUTING_KEY) // 指定发送到死信交换机的 routing_key,用于dlx交换机定位 queue
            .build();

    }

    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE, true, false);
    }

	// 消费者上监听这个队列,则可以实现延迟队列功能,ttl时间到了之后,就会进入这个队列,
    @Bean
    public Queue dlxQueue() {
        return new Queue(DLX_Queue, true);
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(DLX_ROUTING_KEY);
    }

    // 死信队列 ==================================

7.5 死信队列小结

死信交换机就是一个普通交换机, 就是在创建一个Queue的时候,指定过期时间 (ttl) 和 过期后需要将消息转发到哪个交换机和用什么routingkey(即:指定deadLetterExchange() 和 deadLetterRoutingKey() )

8 消息补偿

保证消息一定发送成功
生产者与消费者之间应该约定一个超时时间,比如 5 分钟,对于超出这个时间没有得到响应的消息,可以设置一个定时重发的补偿机制:通过消息落库 + 定时任务来实现。

CREATE TABLE `t_cap_published_message` (
     `id` varchar(40) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '标识。',
     `version` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '版本',
     `exchange` varchar(200) COLLATE utf8mb4_bin DEFAULT '' COMMENT '交换机。',
     `topic` varchar(200) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '话题。',
     `content` longtext COLLATE utf8mb4_bin NOT NULL COMMENT '消息内容。',
     `retries` int(11) NOT NULL COMMENT '重试次数,一般为 3 次。',
     `expiry` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '过期时间。',
     `status` varchar(40) COLLATE utf8mb4_bin NOT NULL COMMENT '状态,成功则消息ack成功,其他状态都要重试。',
     `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间。',
     `last_modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间,可以用作数据版本。',
     PRIMARY KEY (`id`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='发布的消息。';

  1. 入库之后在发送消息;
  2. 如果在规定时间不能 ack 或者 ack=false ,即 confirmCallback 回调的 ack=false ,则按照定时规则重新发送消息;
  3. 然后对于发布成功的消息,如果业务操作完成,实际上它的作用已经发挥完成,一段时间对数据库做清理即可,根据业务的具体情况。

可参考https://blog.csdn.net/weixin_44399827/article/details/124317144

9 消息幂等性

防止同一消息被多次消费

9.1 产生的原因

消息的消费者成功消费了消息,并进行了手动应答 ack后,由于网络等原因,Rabbitmq没有收到消费者的ack ,导致将此消息发送给了其他消费者进行消费,则出现了重复消费同一消息的情况

9.2 解决方案

就是在消费者消费消息时,进行判断当前消息是否被消费了,消费了就不做业务处理直接决绝, 即需要一个唯一标识进行判断
怎么判断消息是否被消费了呢,通过一个全局的ID

  1. 方式一. 通过数据库存唯一标识: 建一张表记录消息ID,和消息内容。发送消息时设置消息ID,消费时查询数据库是否有当前 ID的消息;有,则直接决绝消费,没有,则进行业务处理,并记录消息数据到数据库
  2. 方式二.通过redis存唯一标识: 通过redis存消息ID和消息内容。发消息时设置消息ID,消费时redis使用setIfAbsent方法插入并判断是否存在值,返回false,则直接决绝消费,返回true,则进行业务处理
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值