11.RabbitMQ

一、RabbitMQ介绍

1.1 现存问题

  • 服务调用:两个服务调用时,我们可以通过传统的HTTP方式,让服务A直接去调用服务B的接口,但是这种方式是同步的方式,虽然可以采用SpringBoot提供的@Async注解实现异步调用,但是这种方式无法确保请求一定会访问到服务B的接口。那如何保证服务A的请求信息一定能送达到服务B去完成一些业务操作呢?| 如何实现异步调用

image

  • 海量请求:在我们在做一些秒杀业务时,可能会在某个时间点突然出现大量的并发请求,这可能已经远远超过服务器的并发瓶颈,这时我们需要做一些削峰的操作,也就是将大量的请求缓冲到一个队列中,然后慢慢的消费掉。如何提供一个可以存储千万级别请求的队列呢?

image

  • 在微服务架构下,可能一个业务会出现同时调用多个其他服务的场景,而且这些服务之间一般会用到Feign的方式进行轻量级的通讯,如果存在一个业务,用户创建订单成功后,还需要去给用户添加积分、通知商家、通知物流系统、扣减商品库存,而在执行这个操作时,如果任意一个服务出现了问题,都会导致整体的下单业务失败,并且会导致给用户反馈的时间延长。这时就造成了服务之间存在一个较高的耦合性的问题。如何可以降低服务之间的耦合性呢?

image

1.2 处理问题

RabbitMQ就可以解决上述的全部问题

  • 服务之间如何想实现可靠的异步调用,可以通过RabbitMQ的方式实现,服务A只需要保证可以把消息发送到RabbitMQ的队列中,服务B就一定会消费到队列中的消息只不过会存在一定的延时。| 异步访问

image

  • 忽然的海量请求可以存储在RabbitMQ的队列中,然后由消费者慢慢消费掉,RabbitMQ的队列本身就可以存储上千万条消息

image

  • 在调用其他服务时,如果允许延迟效果的出现,可以将消息发送到RabbitMQ中,再由消费者慢慢消费| 服务解耦

image

1.3 RabbitMQ介绍

百度百科:

RabbitMQ是实现了高级消息队列协议(AMQP:消息中间件的专用协议)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

首先RabbitMQ基于AMQP协议开发,所以很多基于AMQP协议的功能RabbitMQ都是支持的,比如SpringCloud中的消息总线bus

其次RabbitMQ是基于Erlang编写,这是也是RabbitMQ天生的优势,Erlang被称为面向并发编程的语言,并发能力极强,在众多的MQ中,RabbitMQ的延迟特别低,在微秒级别,所以一般的业务处理RabbitMQ比Kafka和RocketMQ更有优势。

最后RabbitMQ提供自带了图形化界面,操作方便,还自带了多种集群模式,可以保证RabbitMQ的高可用,并且SpringBoot默认就整合RabbitMQ,使用简单方便。

二、 安装RabbitMQ

docker安装

在linux的docker里拉取RabbitMQ镜像 并运行容器(management是带web的管理界面)。

docker pull rabbitmq:3.8.3-management
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 镜像id

5672是客户端和RabbitMQ进行通信的端口。

15672是管理界面访问web页面的端口。

进入容器环境

docker exec -it 容器id /bin/bash

在进入容器环境后开启rabbitmq_management 执行:

rabbitmq-plugins enable rabbitmq_management

在浏览器中输入http://192.168.1.201:15672/访问RabbitMQ的管理页面,用户名和密码默认guest。(192.168.1.201我linux的IP地址)

windows安装

安装erlang和rabbitmq后,进入rabbitmq安装目录的sbin目录执行下面命令

rabbitmq-plugins.bat enable rabbitmq_management

杀进程

tasklist | find /i "erl"taskkill /pid 7300 -t -f

启动服务

双击 rabbitmq-server.bat

访问 http://localhost:15672 用户名密码:guest

三、RabbitMQ构架

RabbitMQ的架构可以查看官方地址:https://rabbitmq.com/tutorials/amqp-concepts.html

官方简单架构

image

可以看出RabbitMQ中主要分为三个角色:

  • Publisher:消息的发布者,将消息发布到RabbitMQ中的Exchange
  • RabbitMQ服务:Exchange接收Publisher的消息,并且根据Routes策略将消息转发到Queue中
  • Consumer:消息的消费者,监听Queue中的消息并进行消费

官方提供的架构图相对简洁,我们可以自己画一份相对完整一些的架构图:

RabbitMQ架构图

image

可以看出Publisher和Consumer都是单独和RabbitMQ服务中某一个Virtual Host建立Connection的客户端

后续通过Connection可以构建Channel通道,用来发布、接收消息

一个Virtual Host中可以有多个Exchange和Queue,Exchange可以同时绑定多个Queue

在基于架构图查看图形化界面,会更加清晰

图形化界面信息

image

四、RabbitMQ通讯方式

RabbitMQ提供了很多中通讯方式,依然可以去官方查看:https://rabbitmq.com/getstarted.html

七种通讯方式

image


五、SpringBoot整合

5.1 Hello World

通讯方式

image

RabbitConfig

@Configuration
public class RabbitConfig {
   // 一个队列 一个生产者 一个消费者
    @Bean
    public Queue helloQueue(){
        // 参数1:mq中队列名称
        // 参数2:是否持久化队列,关闭服务后队列是否还存在  默认值false
        // 参数3:是否为独占连接队列 默认值 false
        // 参数4:服务器停止是否自动删除 默认值 false
        // 参数5:队列参数
        return new Queue("helloQueue");
    }
}

生产者:

@Component
public class Publisher {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void  send (String msg){
        rabbitTemplate.convertAndSend("helloQueue",msg);
    }
}

消费者:

@Component
public class Consumer {
    // @RabbitListener 自动监听指定队列,发现有消息会自动获取信息赋给方法形参
    @RabbitListener(queues = {"helloQueue"})
    public void consume(String msg){
        System.out.println("消费者收到消息:" + msg);
    }
}

控制器测试

@RestController
public class RabbitController {
    @Resource
    private Publisher publisher;

    @GetMapping("/hello")
    public String hello(String msg){
        publisher.send(msg);
        return "ok";
    }
}

5.2 Work Queues

WorkQueues需要学习的内容

image

RabbitConfig

// 一个队列 一个生产者 多个消费者
	@Bean
    public Queue workQueue(){
        return new Queue("workQueue");
    }

生产者

Component
public class Publisher {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void send(String msg){
        for (int i = 0; i < 100; i++) {
            rabbitTemplate.convertAndSend("workQueue",msg+i);
        }
    }
}

消费者

@Component
public class Consumer {

    @RabbitListener(queues = {"workQueue"})
    public void consume1(String msg) throws IOException {
        System.out.println("消费者1收到消息:" + msg);
    }

    @RabbitListener(queues = {"workQueue"})
    public void consume2(String msg) throws IOException {
        System.out.println("消费者2收到消息:" + msg);
    }
}

5.3 Publish/Subscribe

自定义一个交换机

image

Exchange交换机:只负责转发消息,不具备存储消息的能力,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型

  • Fanout:广播,将消息交给所有绑定到交换机的队列
  • Direct:定向,把消息交给符合指定routing key 的队列
  • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

config

	//一个生产者 -  一个交换机(Fanout) -  多个队列 - 多个消费者
    // 交换机收到消息,会把消息转发给绑定的所有队列
    @Bean
    public Queue fanoutQueueA(){
        return new Queue("fanoutQueueA");
    }
    @Bean
    public Queue fanoutQueueB(){
        return new Queue("fanoutQueueB");
    }
    @Bean
    public FanoutExchange fanoutExchange(){
        // 参数1:交换机名称
        // 参数2:是否持久化  默认值true
        // 参数3:是否自动删除 没有队列绑定时是否自动删除 默认值false
        return new FanoutExchange("fanoutExchange");
    }
    // 绑定交换机和队列
    @Bean
    public Binding fanoutQueueABinding(){
        return BindingBuilder.bind(fanoutQueueA()).to(fanoutExchange());
    }
    @Bean
    public Binding fanoutQueueBBinding(@Autowired Queue fanoutQueueB,
                                       @Autowired FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueueB).to(fanoutExchange);
    }

生产者

@Component
public class Publisher {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void send(String msg){
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend("fanoutExchange",null,msg+i);
        }
    }
}

消费者

@Component
public class Consumer {
    @RabbitListener(queues = {"fanoutQueueA"})
    public void consume1(String msg){
        System.out.println("消费者1收到 A 队列的消息:" + msg);
    }
    @RabbitListener(queues = {"fanoutQueueA"})
    public void consume2(String msg){
        System.out.println("消费者2收到 A 队列的消息:" + msg);
    }

    @RabbitListener(queues = {"fanoutQueueB"})
    public void consume3(String msg){
        System.out.println("消费者3收到 B 队列的消息:" + msg);
    }
    @RabbitListener(queues = {"fanoutQueueB"})
    public void consume4(String msg){
        System.out.println("消费者4收到 B 队列的消息:" + msg);
    }
}

5.4 Routing

DIRECT类型Exchange

image


生产者:在绑定Exchange和Queue时,需要指定好routingKey,同时在发送消息时,也指定routingKey,只有routingKey一致时,才会把指定的消息路由到指定的Queue

	@Bean
    public Queue directQueueA(){
        return new Queue("directQueueA");
    }
    @Bean
    public Queue directQueueB(){
        return new Queue("directQueueB");
    }
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("directExchange");
    }
    @Bean
    public Binding directQueueABinding(){
        return BindingBuilder.bind(directQueueA()).to(directExchange()).with("A");
    }
    @Bean
    public Binding directQueueBBinding(){
        return BindingBuilder.bind(directQueueB()).to(directExchange()).with("B");
    }
@Component
public class Publisher {
    @Resource
    private RabbitTemplate rabbitTemplate;
    public void send(String msg){
        for (int i = 0; i < 10; i++) {
            if(i % 3 == 0){
                rabbitTemplate.convertAndSend("directExchange","A",msg+i);
            }else if(i % 3 == 1){
                rabbitTemplate.convertAndSend("directExchange","B",msg+i);
            }else{
                rabbitTemplate.convertAndSend("directExchange","C",msg+i);
            }
        }
    }
}

5.5 Topic

Topic模式

image

生产者:TOPIC类型可以编写带有特殊意义的routingKey的绑定方式

config

	@Bean
    public Queue topicQueueA(){
        return new Queue("topicQueueA");
    }
    @Bean
    public Queue topicQueueB(){
        return new Queue("topicQueueB");
    }
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("topicExchange");
    }
    @Bean
    public Binding topicQueueABinding(){
        // * 段数必须一致,段内字符任意
        return BindingBuilder.bind(topicQueueA()).to(topicExchange()).with("A.*.*");
    }
    @Bean
    public Binding topicQueueBBinding(){
        // # 通配
        return BindingBuilder.bind(topicQueueB()).to(topicExchange()).with("B.#");
    }

生产者

@Component
public class Publisher {
    @Resource
    private RabbitTemplate rabbitTemplate;
    public void send(String msg,String routeingKey){
        rabbitTemplate.convertAndSend("topicExchange",routeingKey,msg);
    }
}

controller

    @GetMapping("/topic/{key}")
    public String topic(String msg, @PathVariable("key")String key){
        publisher.send(msg,key);
        return "ok";
    }

5.6 RPC(了解)

因为两个服务在交互时,可以尽量做到Client和Server的解耦,通过RabbitMQ进行解耦操作

需要让Client发送消息时,携带两个属性:

  • replyTo告知Server将相应信息放到哪个队列
  • correlationId告知Server发送相应消息时,需要携带位置标示来告知Client响应的信息

RPC方式

image


客户端:

package com.wang.rpc;

import com.wang.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;

import java.io.IOException;
import java.util.UUID;

public class Publisher {

    public static final String QUEUE_PUBLISHER = "rpc_publisher";
    public static final String QUEUE_CONSUMER = "rpc_consumer";

    @Test
    public void publish() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建队列
        channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
        channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);

        //4. 发布消息
        String message = "Hello RPC!";
        String uuid = UUID.randomUUID().toString();
        AMQP.BasicProperties props = new AMQP.BasicProperties()
                .builder()
                .replyTo(QUEUE_CONSUMER)
                .correlationId(uuid)
                .build();
        channel.basicPublish("",QUEUE_PUBLISHER,props,message.getBytes());

        channel.basicConsume(QUEUE_CONSUMER,false,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String id = properties.getCorrelationId();
                if(id != null && id.equalsIgnoreCase(uuid)){
                    System.out.println("接收到服务端的响应:" + new String(body,"UTF-8"));
                }
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });
        System.out.println("消息发送成功!");

        System.in.read();
    }

}

服务端:

package com.wang.rpc;

import com.wang.helloworld.Publisher;
import com.wang.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;

import java.io.IOException;

public class Consumer {

    public static final String QUEUE_PUBLISHER = "rpc_publisher";
    public static final String QUEUE_CONSUMER = "rpc_consumer";

    @Test
    public void consume() throws Exception {
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建队列
        channel.queueDeclare(QUEUE_PUBLISHER,false,false,false,null);
        channel.queueDeclare(QUEUE_CONSUMER,false,false,false,null);

        //4. 监听消息
        DefaultConsumer callback = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者获取到消息:" + new String(body,"UTF-8"));
                String resp = "获取到了client发出的请求,这里是响应的信息";
                String respQueueName = properties.getReplyTo();
                String uuid = properties.getCorrelationId();
                AMQP.BasicProperties props = new AMQP.BasicProperties()
                        .builder()
                        .correlationId(uuid)
                        .build();
                channel.basicPublish("",respQueueName,props,resp.getBytes());
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        channel.basicConsume(QUEUE_PUBLISHER,false,callback);
        System.out.println("开始监听队列");

        System.in.read();
    }
}

六、RabbitMQ保证消息可靠性

生产者端

6.1 保证消息一定送达到Exchange

Confirm机制:可以通过Confirm效果保证消息一定送达到Exchange,官方提供了三种方式,选择了对于效率影响最低的异步回调的效果

生产者把消息发给交换机,交换机通过一个回调函数告诉生产者消息是否送达,如果没有送达,生产者可以做出补偿,如消息重发,或者保存消息,无论成功与否都会触发回调函数

打开交换机的消息确认机制,默认是关闭的

spring:
  rabbitmq:
    host: localhost  #主机IP
    port: 5672 #端口号 15672是可视化界面的端口号
    username: guest #用户名
    password: guest #密码
    virtual-host: / #虚拟主机
    publisher-confirm-type: correlated #发布消息成功到交换器后会触发回调方法

生产者实现RabbitTemplate.ConfirmCallback接口,重写confirm方法

@Component
public class Publisher implements RabbitTemplate.ConfirmCallback{
    @Resource
    private RabbitTemplate rabbitTemplate;
    public void send(String msg,String routeingKey){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.convertAndSend("topicExchange11",routeingKey,msg);
    }

    //correlationData 生产者发送消息时封装的类,消息发送失败时,用这个类进行消息重发 或保存消息
    // flag 失败或成功的标识
    // reason 失败的原因
    @Override
    public void confirm(CorrelationData correlationData, boolean flag, String reason) {
        if(!flag){
            // 消息投递失败,做补偿措施,可以重发 或者 将消息保存到数据库
        }
        System.out.println(correlationData);
        System.out.println(flag);
        System.out.println(reason);
    }
}

更改交换机的name,制造一个投递交换机失败的错误,reason会打印出失败的结果

6.2 保证消息可以路由到Queue

Return机制

为了保证Exchange上的消息一定可以送达到Queue,通过队列的回调函数,只有发送失败才会触发回调函数

spring:
  rabbitmq:
    host: localhost  #主机IP
    port: 5672 #端口号 15672是可视化界面的端口号
    username: guest #用户名
    password: guest #密码
    virtual-host: / #虚拟主机
    publisher-confirm-type: correlated #交换机消息确认机制
    publisher-returns: true #队列的消息确认机制

生产者实现RabbitTemplate.ReturnsCallback接口,重写returnedMessage方法

@Component
public class Publisher implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    @Resource
    private RabbitTemplate rabbitTemplate;
    public void send(String msg,String routeingKey){
        rabbitTemplate.setReturnsCallback(this); // 使用ReturnsCallback
        rabbitTemplate.convertAndSend("topicExchange",routeingKey,msg);
    }

    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        System.out.println(returnedMessage.getMessage()); // 发送的消息内容
        System.out.println(returnedMessage.getReplyCode()); // 失败状态码
        System.out.println(returnedMessage.getReplyText()); // 失败原因
        System.out.println(returnedMessage.getExchange()); // 交换机名称
        System.out.println(returnedMessage.getRoutingKey()); // 路由
    }
}

消费者端

6.3 保证消费者可以正常消费消息

消费者从队列中获取消息,是将消息复制一份进行消费,消费完成后会给队列一个自动ack(应答),队列只要收到ack删除队列中已消费的消息,队列只管应答,不管消费是否正常完成,如果消费者端出现异常情况,消息没有被正常消费,队列依旧收到自动应答,删除消息,那么消息丢失,所以想解决这个问题,我们需要将自动应答改为手动ack,确保消费者正常消费之后才给队列应答,保证消息不丢失

spring:
  rabbitmq:
    host: localhost  #主机IP
    port: 5672 #端口号 15672是可视化界面的端口号
    username: guest #用户名
    password: guest #密码
    virtual-host: / #虚拟主机
    publisher-confirm-type: correlated #交换机消息确认机制
    publisher-returns: true #队列的消息确认机制
    listener:
      simple:
        acknowledge-mode: manual #开启消费者的手动ack
        prefetch: 1 #消费者一次获取的消息数量 默认是250

开启消费者手动ack之后,队列中的消息虽然被消费,但是还会存在消息,状态为Unacked未应答状态,表示消息还在队列中

    // 消息传输通道 RabbitMQ和生产者/消费者之间的Connection相当于是一个进程,
	// channel是进程中的线程,也是消息传输通道
    // Message 消息封装类 包含了消息体,以及消息的一些属性,如字符集 大小 过期时间 优先级等
    @RabbitListener(queues = {"topicQueueB"})
    public void consume2(String msg, Channel channel, Message message) {
        try {
            // int i = 1 / 0 ;
            System.out.println("消费者2收到 B 消息:" + msg);
            // 消费者手动应答
            // 参数1:消息的标识  参数2:是否批量确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            e.printStackTrace();
            try {
                // 消费者的拒绝应答  队列只接受应答不管是否是成功的应答,都会删除消息,所以需要确认是否放回队列
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),  // 消息标识
                        false, // 是否批量处理
                        true); // 是否将消息重新放回队列
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

6.4 保证Queue可以持久化消息

DeliveryMode设置消息持久化

DeliveryMode设置为2代表持久化,如果设置为1,就代表不会持久化。

//7. 设置消息持久化
AMQP.BasicProperties props = new AMQP.BasicProperties()
    .builder()
    .deliveryMode(2)
    .build();

//7. 发布消息
channel.basicPublish("","confirms",true,props,message.getBytes());

七、RabbitMQ死信队列&延迟交换机

7.1 什么是死信

死信&死信队列

image

死信队列的应用:

  • 基于死信队列在队列消息已满的情况下,消息也不会丢失
  • 实现延迟消费的效果。比如:下订单时,有15分钟的付款时间

7.2 实现死信队列

7.2.1 准备Exchange&Queue

package com.wang.config;

import com.rabbitmq.client.AMQP;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Classname DeadLetterConfig
 * @Description TODO
 * @Date 2025/4/1 17:08
 * @Created by pc
 */
@Configuration
public class DeadLetterConfig {

    public static final String NORMAL_EXCHANGE = "normal-exchange";
    public static final String NORMAL_QUEUE = "normal-queue";
    public static final String NORMAL_ROUTING_KEY = "normal.#";

    public static final String DEAD_EXCHANGE = "dead-exchange";
    public static final String DEAD_QUEUE = "dead-queue";
    public static final String DEAD_ROUTING_KEY = "dead.#";

    @Bean
    public Exchange normalExchange(){
        return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE).build();
    }

    @Bean
    public Queue normalQueue(){
        // 如果拒绝要指定死信交换机并重新指定死信路由
        return QueueBuilder.durable(NORMAL_QUEUE)
                .deadLetterExchange(DEAD_EXCHANGE). // 指定死信队列
            deadLetterRoutingKey(DEAD_ROUTING_KEY) // 指定死信路由
            .build();
    }
    @Bean
    public Binding normalBinding(Queue normalQueue,Exchange normalExchange){
        return BindingBuilder.bind(normalQueue).to(normalExchange).with(NORMAL_ROUTING_KEY).noargs();
    }

    @Bean
    public Exchange deadExchange(){
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE).build();
    }

    @Bean
    public Queue deadQueue(){
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }
    @Bean
    public Binding deadBinding(Queue deadQueue,Exchange deadExchange){
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
    }
}

7.2.2 实现效果

  • 基于消费者进行reject或者nack实现死信效果
package com.wang.dead;

import com.wang.config.DeadLetterConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

/**
 * @Classname DeadPublisher
 * @Description 消息生产者
 * @Date 2025/4/1 18:40
 * @Created by pc
 */
@SpringBootTest
public class DeadPublisher {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Test
    public void publish(){
        String message = "dead Letter";
        rabbitTemplate.convertAndSend(DeadLetterConfig.NORMAL_EXCHANGE,"normal.123",message);
    }
}

package com.wang.dead;

import com.rabbitmq.client.Channel;
import com.wang.config.DeadLetterConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class DeadListener {

    @RabbitListener(queues = DeadLetterConfig.NORMAL_QUEUE)
    public void consume(String mes, Channel channel, Message message) throws IOException {
        System.out.println("接受到normal队列的消息:" + message);
        // 方式一: 使用拒绝 并在requeue为false的情况
        // 获取消息标识 设置requeue 表示不需要重新放回队列中
        // channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
        // 方式二: 使用Nack方式
        // 第二个参数是否为批量操作
        channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
    }
}
  • 生存时间

    • 给消息设置生存时间
    @Test
    public void publishExpire(){
        String message = "dead Letter expire";
        rabbitTemplate.convertAndSend(DeadLetterConfig.NORMAL_EXCHANGE, "normal.123", message,
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        message.getMessageProperties()
                            .setExpiration("5000"); // 设置消息的过期时间为5000毫秒
                        return message;
                    }
                });
    }
    
    • 给队列设置消息的生存时间
    @Bean
    public Queue normalQueue(){
        return QueueBuilder.durable(NORMAL_QUEUE)
                .deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey("dead.123")
                .ttl(10000) // 给队列中的消息设置生存时间
                .build();
    }
    
  • 设置Queue中的消息最大长度

    @Bean
    public Queue normalQueue(){
        return QueueBuilder.durable(NORMAL_QUEUE)
                .deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey("dead.123")
                .maxLength(1) // 给队列
                .build();
    }
    

    只要Queue中已经有一个消息,如果再次发送一个消息,这个消息会变为死信!

7.3 延迟交换机

死信队列实现延迟消费时,如果延迟时间比较复杂,比较多,直接使用死信队列时,需要创建大量的队列还对应不同的时间,可以采用延迟交换机来解决这个问题。

使用场景

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺如果十天内没有上传过商品,则会自动发消息提醒;
  3. 账号注册成功,如果三天内没登陆则进行短信提醒
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

安装之前可以查看交换机可选类型

image

下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9

安装延迟交换机

docker

将下载插件传到虚拟机中,并拷贝到rabbitMQ容器中

docker cp /home/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/opt/rabbitmq/plugins

进入到容器中查看是否复制成功

docker exec -it rabbitmq /bin/bash
cd /opt/rabbitmq/plugins

跳到sbin目录下去执行这个插件

cd ../sbin/
rabbitmq-plugins enable  rabbitmq_delayed_message_exchange
docker restart rabbitmq

windows

  1. 把插件放入rabbitmq安装目录的plugins目录
  2. 进入rabbitmq 安装目录的sbin 目录
  3. 执行下面命令让改插件生效
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

image

重启RabbitMQ服务后,可以看到交换机中多了一种类型的交换机

image

代码实现

  • 构建延迟交换机

    	// 延迟队列
        @Bean
        public Queue delayedQueue(){
            return new Queue("delayedQueue");
        }
        // CustomExchange 自定义交换机
        @Bean
        public CustomExchange delayedExchange(){
            Map<String,Object> map = new HashMap<>();
            // 指定该交换机的类型
            map.put("x-delayed-type","topic");
            return new CustomExchange("delayedExchange", //交换机名称
                    "x-delayed-message", // 交换机类型
                    true, // 是否持久化 没有被消费的消息是否持久化
                    false, // 没有队列绑定到交换机是否删除
                    map); // 其他参数
        }
        @Bean
        public Binding delayedQueueBinding(){
            return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with("delay").noargs();
        }
    
  • 发送消息

@Component
public class Publisher {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void send(String msg,int timeout){
        rabbitTemplate.convertAndSend("delayedExchange", // 交换机名称
                "delay", // routingKey
                msg, // 消息内容
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        // 设置消息的过期时间 单位是毫秒
                        message.getMessageProperties().setDelay(timeout);
                        return message;
                    }
                }
        );
    }
}
  • 消费消息
@Component
public class Consumer {

    @RabbitListener(queues = {"delayedQueue"})
    public void consume(String msg, Channel channel, Message message){
        System.out.println("消费者收到延迟消息:" + msg);
        try {
            // 配置文件开启了手动ack 
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

八、RabbitMQ的集群

RabbitMQ的镜像模式

RabbitMQ的集群

image

高可用

提升RabbitMQ的效率

搭建RabbitMQ集群

  • 准备两台虚拟机(克隆)

  • 准备RabbitMQ的yml文件启动效果

    rabbitmq1:

    version: '3.1'
    services:
      rabbitmq1:
        image: rabbitmq:3.8.5-management-alpine
        container_name: rabbitmq1
        hostname: rabbitmq1
        extra_hosts:
          - "rabbitmq1:192.168.11.32"
          - "rabbitmq2:192.168.11.33"
        environment: 
          - RABBITMQ_ERLANG_COOKIE=SDJHFGDFFS
        ports:
          - 5672:5672
          - 15672:15672
          - 4369:4369
          - 25672:25672
    

    rabbitmq2:

    version: '3.1'
    services:
      rabbitmq2:
        image: rabbitmq:3.8.5-management-alpine
        container_name: rabbitmq2
        hostname: rabbitmq2
        extra_hosts:
          - "rabbitmq1:192.168.11.32"
          - "rabbitmq2:192.168.11.33"
        environment: 
          - RABBITMQ_ERLANG_COOKIE=SDJHFGDFFS
        ports:
          - 5672:5672
          - 15672:15672
          - 4369:4369
          - 25672:25672
    

    准备完毕之后,启动两台RabbitMQ

image


  • 让RabbitMQ服务实现join操作执行成功后

    需要四个命令完成join操作

    让rabbitmq2 join rabbitmq1,需要进入到rabbitmq2的容器内部,去执行下述命令

    rabbitmqctl stop_app
    rabbitmqctl reset
    rabbitmqctl join_cluster rabbit@rabbitmq1
    rabbitmqctl start_app
    

    执行成功后:

image

  • 设置镜像模式镜像模式

    在指定的RabbitMQ服务中设置好镜像策略即可

image

九、RabbitMQ其他内容

9.1 Headers类型Exchange

headers就是一个基于key-value的方式,让Exchange和Queue绑定的到一起的一种规则

相比Topic形式,可以采用的类型更丰富。

headers绑定方式

image

具体实现方式

package com.wang.headers;

import com.wang.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;

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

public class Publisher {

    public static final String HEADER_EXCHANGE = "header_exchange";
    public static final String HEADER_QUEUE = "header_queue";

    @Test
    public void publish()throws  Exception{
        //1. 获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2. 构建Channel
        Channel channel = connection.createChannel();

        //3. 构建交换机和队列并基于header的方式绑定
        channel.exchangeDeclare(HEADER_EXCHANGE, BuiltinExchangeType.HEADERS);
        channel.queueDeclare(HEADER_QUEUE,true,false,false,null);
        Map<String,Object> args = new HashMap<>();
        // 多个header的key-value只要可以匹配上一个就可以
        // args.put("x-match","any");
        // 多个header的key-value要求全部匹配上!
        args.put("x-match","all");
        args.put("name","jack");
        args.put("age","23");
        channel.queueBind(HEADER_QUEUE,HEADER_EXCHANGE,"",args);

        //4. 发送消息
        String msg = "header测试消息!";
        Map<String, Object> headers = new HashMap<>();
        headers.put("name","jac");
        headers.put("age","2");
        AMQP.BasicProperties props = new AMQP.BasicProperties()
                .builder()
                .headers(headers)
                .build();

        channel.basicPublish(HEADER_EXCHANGE,"",props,msg.getBytes());

        System.out.println("发送消息成功,header = " + headers);

    }
}

思维导图

image

posted @ 2025-05-30 11:50  icui4cu  阅读(12)  评论(0)    收藏  举报