RabbitMQ高级特性的使用(基于springboot实现)

652 阅读10分钟

序言

RabbitMQ 是一款开源的消息代理软件,基于 AMQP(高级消息队列协议)实现,具有高效、可靠、灵活等特点,广泛应用于分布式系统中,用于实现应用间的异步通信、负载均衡、服务解耦等功能。
这篇文章使用java语言,RabbitMQ管控台插件(直观的学习mq使用流程)进行展示

1.消费端限流

概念:

在此之前我们需要了解的一个概念是“削峰填谷” ,假设我们的系统每秒只能承载1000请求,如果请求瞬间增多到每秒5000,则会造成系统崩溃。使用了MQ之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。 微信图片_20250119194150.jpg

实现方法:

消费端限流: 即通过消费端限流的方式限制消息的拉取速度,达到保护消费端的目的。

微信图片_20250119193823.jpg

使用消费端限流的前提:在消费端配置限流机制,需要配置手动签收,自动签收的话所有都消息会被签收再进行消费,会堆积到消费者中,起不到限流的作用

示例代码:

yml中消费端配置限流机制:

spring:
  rabbitmq:
   host: 192.168.0.162
   port: 5672
   username: zzzzzt
   password: zzzzzt
   virtual-host: /
   listener:
    simple:
    # 限流机制必须开启手动签收
     acknowledge-mode: manual
    # 消费端最多拉取5条消息消费,签收后不满5条才会继续拉取消息。
     prefetch: 5

消费者监听队列:

@Component
public class QosConsumer{
  @RabbitListener(queues = "my_queue")
  public void listenMessage(Message message, Channel channel) throws IOException, InterruptedException {
    // 1.获取消息
    System.out.println(new String(message.getBody()));
    // 2.模拟业务处理
    Thread.sleep(3000);
    // 3.手动签收消息
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
   }
}

2.实现不公平分发

概念:

在RabbitMQ中,多个消费者监听同一条队列,则队列默认采用的轮询分发。但是在某种场景下这种策略并不是很好,例如消费者1处理任务的速度非常快,而其他消费者处理速度却很慢。此时如果采用公平分发,则消费者1有很大一部分时间处于空闲状态。

实现方法:

在yml中将prefetch的值设置为1,根据不同消费者的消费速度不同,实现不公平分发

示例代码:
spring:
  rabbitmq:
   host: 192.168.0.162
   port: 5672
   username: zzzzzt
   password: zzzzzt
   virtual-host: /
   listener:
    simple:
    # 限流机制必须开启手动签收
     acknowledge-mode: manual
    # 消费端最多拉取1条消息消费,这样谁处理的快谁拉取下一条消息,实现了不公平分发
     prefetch: 1

3.消息存活时间

概念:

RabbitMQ可以设置消息的存活时间(Time To Live,简称TTL),当消息到达存活时间后还没有被消费,会被移出队列。RabbitMQ可以对队列的所有消息设置存活时间,也可以对某条消息设置存活时间。

实现方法:

对队列的所有消息设置存活时间:创建队列时设置参数值ttl

设置单条消息存活时间:通过消息属性设置存活时间

示例代码:

对队列的所有消息设置存活时间:

Bean("bootQueue2")
public Queue getMessageQueue2(){
    return QueueBuilder
            .durable(QUEUE_NAME)
            .ttl(10000) //队列的每条消息存活10s
            .build();
}

设置单条消息存活时间:

@Test
public void testSendMessage() {
    //设置消息属性
    MessageProperties messageProperties = new MessageProperties();
    //设置存活时间
    messageProperties.setExpiration("10000");
    // 创建消息对象
    Message message = new Message("send message...".getBytes(StandardCharsets.UTF_8), messageProperties);
    // 发送消息
    rabbitTemplate.convertAndSend("my_topic_exchange", "my_routing", message);
}
注意事项:

1.如果设置了单条消息的存活时间,也设置了队列的存活时间,以时间短的为准

2.消息过期后,并不会马上移除消息,只有消息消费到队列顶端时,才会移除该消息

例如:

@Test
public void testSendMessage2() {
    for (int i = 0; i < 10; i++) {
        if (i == 5) {
            // 1.创建消息属性
            MessageProperties messageProperties = new MessageProperties();
            // 2.设置存活时间
            messageProperties.setExpiration("10000");
            // 3.创建消息对象
            Message message = new Message(("send message..." + i).getBytes(), messageProperties);
            // 4.发送消息
            rabbitTemplate.convertAndSend("my_topic_exchange", "my_routing", message);
        } else {
            rabbitTemplate.convertAndSend("my_topic_exchange", "my_routing", "send message..." + i);
        }
    }
}

在以上案例中,i=5的消息才有过期时间,10s后消息并没有马上被移除,但该消息已经不会被消费了,当它到达队列顶端时会被移除。

4.优先级队列

使用场景:

假设在电商系统中有一个订单催付的场景,即客户在一段时间内未付款会给用户推送一条短信提醒,但是系统中分为大型商家和小型商家。比如像苹果,京东这样大商家一年能给我们创造很大的利润,所以在订单量大时,他们的订单必须得到优先处理,此时就需要为不同的消息设置不同的优先级,此时我们要使用优先级队列。

概念:

优先级队列允许根据消息的优先级进行处理,优先级高的消息会被优先消费。

实现方法:

创建队列时与创建消息属性时可以设置优先级

示例代码:
//创建队列
@Bean("QUEUE_NAME")
public Queue priorityQueue(){
    return QueueBuilder
            .durable(QUEUE_NAME)
            .maxPriority(10)
            //设置队列的最大优先级,最大可以设置到255,官网推荐不要超过10,,如果设置太高比较浪费资源
            .build();
}
@Test
public void testPrority(){
    for (int i = 0; i < 10; i++) {
    // i为5时消息的优先级较高
        if(i == 5){
            MessageProperties messageProperties= new MessageProperties();
            messageProperties.setPriority(9);
            Message message= new Message(("send message..."+i).getBytes(), messageProperties);
            rabbitTemplate.convertAndSend("priority_exchange","my_routing",message);
        }else {
            rabbitTemplate.convertAndSend("priority_exchange","my_routing","send message..."+i);
        }
    }
}
效果实现:

微信图片_20250120153937.jpg

5.死信队列

概念:

在MQ中,当消息成为死信(Dead message)后 ,消息中间件可以将其从当前队列发送到另一个队列中, 另一个队列就是死信队列(专门存放失效的消息 。而在RabbitMQ中,由于有交换机的概念,实际是将死信发送给了死信交换机(Dead Letter Exchange,简称DLX)。死信交换机和死信队列和普通的没有区别。

微信图片_20250120160233.png

消息成为死信的情况:

  1. 队列消息长度到达限制。
  2. 消费者拒签消息,并且不把消息重新放入原队列
  3. 消息到达存活时间未被消费。
示例代码:

创建死信队列并绑定:

// 普通队列
@Bean("NORMAL_QUEUE")
public Queue normalQueue(){
    return QueueBuilder
            .durable(NORMAL_QUEUE)
            .deadLetterExchange(DEAD_EXCHANGE) // 绑定死信交换机
            .deadLetterRoutingKey("dead_routing") // 死信队列路由关键字
            .ttl(10000) // 消息存活10s
            .maxLength(10) // 队列最大长度为10
            .build();
}

测试死信队列:

@Test
public void testDlx(){
    // 存活时间过期后变成死信
    rabbitTemplate.convertAndSend("normal_exchange","my_routing","测试死信队列");

    // 超过队列长度后变成死信
    for (int i = 0; i < 15; i++) {
        rabbitTemplate.convertAndSend("normal_exchange","my_routing","测试死信队列");
    }

    // 消息拒签但不返回原队列后变成死信
    rabbitTemplate.convertAndSend("normal_exchange","my_routing","测试死信");
}
//测试死信队列---消费者拒收消息
@Component
public class DlxConsumer {
    @RabbitListener(queues = "normal_queue")
    public void listenMessage(Message message, Channel channel) throws IOException {
        //拒签消息
        channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,false);
    }
}

:测试上述代码时3种情况尽量分开测试,否则达不到预期效果

6.延迟队列

概念:

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。但RabbitMQ中并未提供延迟队列功能,我们可以使用死信队列实现延迟队列的效果

微信图片_20250120163333.jpg

示例代码:
@Bean(ORDER_QUEUE)
public Queue normalQueue(){
    return QueueBuilder
            .durable(ORDER_QUEUE)
            .ttl(20000) // 存活时间为20s,模拟30min
            .deadLetterExchange(EXPIRE_EXCHANGE)  // 绑定死信交换机
            .deadLetterRoutingKey("expire_routing")  // 死信交换机的路由关键字
            .build();
}

效果实现

当order_queue收到生产者发来的消息,当存活时间到了以后会将消息放到死信队列,达到延时效果 通过观察管控台:

消息存活时: 微信图片_20250120164822.png 消息进入死信队列等待被消费:

微信图片_20250120164829.png

7.延迟队列_使用插件实现延迟队列

概念:

在使用死信队列实现延迟队列时,会遇到一个问题:RabbitMQ只会移除队列顶端的过期消息,如果第一个消息的存活时长较长,而第二个消息的存活时长较短,则第二个消息并不会及时执行。

微信图片_20250120171432.png RabbitMQ虽然本身不能使用延迟队列,但官方提供了延迟队列插件,安装后可直接使用延迟队列。 延迟队列收到消息后直接将消息送至消费者,等待消息过了存活时间再进行消费

微信图片_20250120171621.png

安装延时队列插件

1.使用rz将插件上传至虚拟机(官网)

2.安装插件:

将插件放入RabbitMQ插件目录中

mv rabbitmq_delayed_message_exchange-3.9.0.ez /usr/local/rabbitmq/plugins/

启用插件:

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

3.重启RabbitMQ服务

此时登录管控台可以看到交换机类型多了延迟消息:

微信图片_20250120174212.png

示例代码:

创建延迟队列

@Configuration
public class RabbitConfig2 {
    public final String DELAYED_EXCHANGE = "delayed_exchange";
    public final String DELAYED_QUEUE = "delayed_queue";

    //1.延迟交换机
    @Bean(DELAYED_EXCHANGE)
    public Exchange delayedExchange() {
        // 创建自定义交换机
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic"); // topic类型的延迟交换机
        return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }

    //2.延迟队列
    @Bean(DELAYED_QUEUE)
    public Queue delayedQueue() {
        return QueueBuilder
                .durable(DELAYED_QUEUE)
                .build();
    }


    // 3.绑定
    @Bean
    public Binding bindingDelayedQueue(@Qualifier(DELAYED_QUEUE) Queue queue, @Qualifier(DELAYED_EXCHANGE) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("order_routing").noargs();
    }
}

8.搭建RabbitMQ集群

概念

在生产环境中,当单台RabbitMQ服务器无法满足消息的吞吐量及安全性要求时,需要搭建RabbitMQ集群。

1.设置两个RabbitMQ服务:

//关闭RabbitMQ服务:
rabbitmqctl stop

//设置服务一:
RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server start -detached

//设置服务二:
RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start -detached

2.将两个服务设置到同一集群中
//关闭服务2
rabbitmqctl -n rabbit2 stop_app
//重新设置服务2
rabbitmqctl -n rabbit2 reset
//将服务2加入服务1中
rabbitmqctl -n rabbit2 join_cluster rabbit1@主机名
//启动服务2
rabbitmqctl -n rabbit2 start_app

集群搭建之后访问这2个端口的访问台都能看到2个RabbitMQ节点,队列等信息是同步的 0969967276814a68936a4be4f1d5c79.png

9.RabbitMQ集群_镜像队列

搭建了集群后,虽然多个节点可以互相通信,但队列只保存在了一个节点中,如果该节点故障,则整个集群都将丢失消息。 需要引入镜像队列机制,它可以将队列消息复制到集群中的其他节点上。如果一个节点失效了,另一个节点上的镜像可以保证服务的可用性。
在管控台点击Admin--->Policies设置镜像队列
:但新增镜像队列之后,原来的数据并不会新增至镜像队列

10.RabbitMQ集群_负载均衡

无论是生产者还是消费者,只能连接一个RabbitMQ节点,而在我们使用RabbitMQ集群时,如果只连接一个RabbitMQ节点,会造成该节点的压力过大。我们需要平均的向每个RabbitMQ节点发送请求,此时需要一个负载均衡工具帮助我们分发请求,接下来使用Haproxy做负载均衡:
1.使用yum安装Haproxy
yum -y install haproxy
2.配置Haproxy
vim /etc/haproxy/haproxy.cfg
添加如下内容:
#以下为修改内容
defaults
//修改为tcp
mode tcp

#以下为添加内容
listen rabbitmq_cluster
# 对外暴露端口
bind 0.0.0.0:5672
mode tcp
balance roundrobin
# 代理RabbitMQ的端口
server node1 127.0.0.1:5673 check inter 5000 rise 2 fall 2
server node2 127.0.0.1:5674 check inter 5000 rise 2 fall 2

listen stats
# Haproxy控制台路径
bind 虚拟机ip:8100
mode http
option httplog
stats enable
stats uri /rabbitmq-stats
stats refresh 5s
3.启动Haproxy
haproxy -f /etc/haproxy/haproxy.cfg 4.访问Haproxy控制台:http://虚拟机ip:8100/rabbitmq-stats

481376b8d33339705a431468c0668b5.png 最后可以使用原生java代码让生产者连接Haproxy发送消息,进行验证