RabbitMQ从入门到精通

1 什么是MQ

arduino 复制代码
MQ(message queue),其实就是队列,我们知道队列都是有序的,FIFO先入先出,只不过这里队列中存放的是message而已。在互联网架构中,MQ是一种非常常见的"逻辑解耦+物理解耦"的消息通信服务,使用了MQ之后,消息发送的上游只需要依赖MQ,不用依赖其它服务。

2 MQ的好处

  1. 流量消峰: 举一个例子,就像是下大雨都快要淹了,这个时候就可以用水库把水存住,后面再慢慢的放,这里水库的作用就像是MQ,起到了一个缓冲的作用。再比如订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
  2. 应用的解耦: 以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常 。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
  3. 异步处理: 例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提 供一个 callback api, B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题, A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。

3 RabbitMq

3.1 RabbitMQ的概念

RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。

3.2 四大核心概念

  1. 生产者:负责发送消息
  2. 交换机:它一方面用来接收来自生产者的消息,另一方面是将消息推送到队列中。生产者把消息发送到交换机的时候需要给一个routingKey来告诉交换机希望把消息路由到哪一个队列中(队列绑定到交换机需要设置bindingKey),需要routingKey和bindingKey匹配。
  3. 队列:本质是一个消息缓冲区(Queue)
  4. 消费者:消费者需要不断的监听队列,一旦有消息就去处理。

4 RabbitMQ的安装

这里我提供的是在linux系统中安装

  1. 安装镜像 docker pull rabbitmq
  2. 运行MQ docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5673:5672 rabbitmq 需要注意的是-p 5673:5672(-p 外网端口:docker的内部端口),所以我们在springboot配置端口用的是5673,而RabbitMQ的客户端端口是15672
  3. 进入到MQ docker exec -it 容器id /bin/bash
  4. 开启客户端的功能,默认是关闭的 rabbitmq-plugins enable rabbitmq_management 注意:这里可能会遇到连接不上的问题,需要把15672还有5672的端口打开
  5. 防火墙 关闭防火墙systemctl stop firewalld 开机时防火墙不再开启systemctl enable firewalld

5 Springboot整合RabbitMQ

5.1 引入依赖

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>3.2.1</version>
</dependency>

5.2 配置文件

yaml 复制代码
spring:
  rabbitmq:
    port: 5673
    username: guest
    password: guest
    host: 192.168.189.120
    virtual-host: /
    publisher-confirm-type: correlated
    publisher-returns: true
server:
  port: 8001

这里publisher-confirm-type是配置生产者的确认机制,publisher-returns是当发送到交换机的消息无法路由到队列时候给生产者一个反馈,这两个后面会具体讲。

5.3 配置类

java 复制代码
package com.gd.producer.config;

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

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

@Configuration
public class RabbirmqConfig {
    public static final String EXCHANGE_X = "exchageX";
    public static final String QUEUE_A = "queueA";
    @Bean(value = "exchangeX")
    public DirectExchange exchangeX() {
        return new DirectExchange(EXCHANGE_X);
    }
    @Bean(value = "queueA")
    public Queue queueA() {
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange",EXCHANGE_Y);
        args.put("x-dead-letter-routing-key","YD");
        args.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }
    @Bean
    public Binding bindingxtoa(@Qualifier("queueA") Queue queueA, @Qualifier("exchangeX") DirectExchange exchangeX) {
        return BindingBuilder.bind(queueA).to(exchangeX).with("XA");
}

5.4 生产者

java 复制代码
package com.gd.producer.controller;

import com.gd.producer.config.RabbirmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@Slf4j
@RestController
@RequestMapping("/sentmsg")
public class SentMessageController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @RequestMapping("/dead/{message}")
    public void sentmsg(@PathVariable String message) {
        log.info("当前时间:{}, 发送一条信息给两个 TTL 队列:{}", new
                Date().toString(), message);
        rabbitTemplate.convertAndSend("exchageX","XA","消息来自TTL为10s的队列:"+message);
    }
}

5.5 消费者

java 复制代码
package com.gd.consumer.controller;

import com.gd.consumer.config.RabbirmqConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;

@Component
@Slf4j
public class DeadLetterQueueConsumer {
    @RabbitListener(queues = "queueA")
    public void receiveD(Message message, Channel channel) throws
            IOException {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
    }
}

6 如何保证消息的可靠传输

首先我们先看一下整个的链路

消息从生产者产生发送到Broker,Broker存储消息待消费者来拉取,消费者从Broker拉去消息。所以保证消息的不丢失,从整个链路来看需要保证三大流程中消息都不丢失。

  1. 发送
  2. 存储
  3. 消费
  • 生产者需要保证消息一定被完整的发送并存储至Broker中。
  • Broker需要保证已经存储的消息不会丢失
  • 消费者需要保证拉取的消息一定被消费,比如消息消费了一半重启了,需要保证未消费的消息后续也能被消费

下面将从这三点进行考虑

6.1 消息的可靠发送

我们首先给一个业务场景,比如下单就加积分。我们会在代码里先保存订单,然后发送消息让积分系统给对应的用户加积分:

java 复制代码
public boolean addOrder(xx) {
    saveOrder();//保存订单
    sendMessage();// 发送加积分消息
}

我们需要确保订单保存成功后,积分消息一定要发送成功。这里涉及了请求确认机制,即ack

当Broker接收到消息之后就给生产者发送一个ack,一旦生产者接收到ack就知道消息被Broker成功接收了。还有一种情况如果Broker没有返回ack该怎么办? 不管是网络还是什么原因,只要生产者超时等待没收到ack,那就需要重试,当然也不是无限重试,重试一定次数就返回错误

java 复制代码
public boolean addOrder(xx){
    saveOrder();
    try{
        boolean result = sentMessage();
        if(!result) {
            recordSend(); // 记录失败的消息
        }
    }catch(Exception) {
        log.error("发送消息失败");
        recordSend();// 记录失败消息
    }
}

这种情况下消息发送异常,但是不影响正常的下单流程,因此就记录下错误日志,然后把发送积分的消息保存到数据库,后续再通过定时任务来补偿这些没有加上的积分。 这个时候很多同学可能会直接报错,但是抛错意味着addOrder这个方法报错了,会使得体验变差,这里我们就需要注意:不能让非主流程的功能影响主流程的功能,下单是主流程,加积分是非主流程。 同步发送可以通过try-catch来捕获异常,异步发送的话,就需要处理onException的逻辑

java 复制代码
package com.gd.producer.config;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback{
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if(ack){
            log.info("交换机已经收到 Id 为:{} 的消息", id);
        } else {
            log.info("交换机还未收到 Id 为:{} 的消息,原因为:{}", id, cause);
        }
    }
}

这里提供一个处理未收到消息的逻辑,这里我们在发送消息的同时,把消息封装成一个BrokerMessageLog类存储到数据库中

java 复制代码
package com.sxw.entity;

import java.util.Date;

@Data
public class BrokerMessageLog {
    private String messageId;

    private String message;

    private Integer tryCount;

    private String status;

    private Date nextRetry;

    private Date createTime;

    private Date updateTime;

  
}

定义消息的发送状态

java 复制代码
public class Constants {
    public static final String ORDER_SENDING = "0"; //发送中

    public static final String ORDER_SEND_SUCCESS = "1"; //成功

    public static final String ORDER_SEND_FAILURE = "2"; //失败

    public static final int ORDER_TIMEOUT = 1; /*分钟超时单位:min*/
}

之后我们开启一个任务定时去查数据库,查询消息状态为0(发送中) 且已经超时的消息集合。之后对集合进行遍历,有两种情况,一种是消息重复发送的次数已经超过三次,就把status设置成2(失败);另一种就是重新的发送消息并把重试的次数加一。

java 复制代码
package com.sxw.springbootproducer.task;

import com.sxw.entity.BrokerMessageLog;
import com.sxw.entity.Order;
import com.sxw.springbootproducer.constant.Constants;
import com.sxw.springbootproducer.mapper.BrokerMessageLogMapper;
import com.sxw.springbootproducer.producer.RabbitOrderSender;
import com.sxw.springbootproducer.utils.FastJsonConvertUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;

@Component
public class RetryMessageTasker {


    @Autowired
    private RabbitOrderSender rabbitOrderSender;

    @Autowired
    private BrokerMessageLogMapper brokerMessageLogMapper;

    @Scheduled(initialDelay = 5000, fixedDelay = 10000)
    public void reSend(){
        System.out.println("-----------定时任务开始-----------");
        //pull status = 0 and timeout message
        List<BrokerMessageLog> list = brokerMessageLogMapper.query4StatusAndTimeoutMessage();
        list.forEach(messageLog -> {
            if(messageLog.getTryCount() >= 3){
                //update fail message
                brokerMessageLogMapper.changeBrokerMessageLogStatus(messageLog.getMessageId(), Constants.ORDER_SEND_FAILURE, new Date());
            } else {
                // resend
                brokerMessageLogMapper.update4ReSend(messageLog.getMessageId(),  new Date());
                Order reSendOrder = FastJsonConvertUtil.convertJSONToObject(messageLog.getMessage(), Order.class);
                try {
                    rabbitOrderSender.sendOrder(reSendOrder);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.err.println("-----------异常处理-----------");
                }
            }
        });
    }
}
  • 这里还有一个可能是消息已经发送到交换机,但是路由不到,这里也需要一个回调函数ReturnsCallback来处理
java 复制代码
package com.gd.producer.config;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ReturnsCallback{
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(this);
//true:交换机无法将消息进行路由时,会将该消息返回给生产者;false:如果发现消息无法进行路由,则直接丢弃
        rabbitTemplate.setMandatory(true);  // 或使用配置   spring.rabbitmq.template.mandatory: true
    }
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.error("消息 {}, 被交换机 {} 退回, 退回原因:{}, 路由 key :{}",
                new String(returnedMessage.getMessage().getBody()),
                returnedMessage.getExchange(),returnedMessage.getReplyText(),
                returnedMessage.getRoutingKey());
    }
}

小结:在生产阶段,利用请求机制保证消息发送成功,如果没收到ack就重试,重试还是失败,就需要看场景分析。一是可以直接将整个业务方法失败使得流程报错,这样业务没处理成功自然也不存在消息的丢失。但这种方式不够优雅。我们可以让业务正常处理,然后通过落库等手段保存这个消息,后续利用定时任务再发送或利用其他的手段。

6.2 存储流程

这里很简单,就需要队列和消息都设置持久化,也就是当把消息发送到队列之后,即使rabbitmq宕机了,重新开机时候数据依然存在。

6.3 消费流程

默认情况下RabbiMQ的消息分发策略为 轮询: 1、会将队列中的所有消息平均分配给所有消费者,消费者拿到消息后,自动回复ack 2、RabbitMQ收到ack后就会将队列中的消息删除 3、消费者一个一个处理消息 如果前2步成功完成,第3步还没开始,消费者宕机,这些消息将会彻底丢失。所以我们就需要手动的确认 ,此时我们需要开启手动确认的配置acknowledge-mode: manual

yml 复制代码
server:
  port: 8081
spring:
  rabbitmq:
    host: 49.235.99.193
    port: 5672
    username: admin
    password: 123
    virtual-host: test
    # 主要针对消费者的一些设置
    listener:
      simple: 
        prefetch: 1
        acknowledge-mode: manual
        concurrency: 1                  # 最少需要一个消费者来监听同一队列
        max-concurrency: 2              # 最大只能拥有2个消费者来监听同一队列
        default-requeue-rejected: true  # 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
less 复制代码
```java
@Component
public class Consumer {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MQConstant.QUEUE_PRODUCT, durable = "true"),
            exchange = @Exchange(value = MQConstant.EXCHANGE_DIRECT_PRODUCT),
            key = {MQConstant.ROUTING_KEY},
            ackMode = "MANUAL" 
    ))
    public void consumerMsg(@Payload String msg, Message message, Channel channel) throws IOException {
        System.out.println("consumer消费消息====>" + msg);
        try {
            String str = null;
            str.equals("aaa");
            /**
             * 没有异常就确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:当前消息在队列中的的索引;
             * multiple:为true的话就是批量确认
             */
 //       delieverTag也可以通过@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag写在请求里面获取 
          	channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {

            /**
             * 有异常就拒收消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * requeue:true为将消息重返当前消息队列,重新发送给消费者;
             *         false将消息丢弃
             */
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}

---这里还有一种情况是消息在队列中滞留的时间过长或者已经到期了,我们就需要使用死信队列进行处理,代码层面如下,给队列queueA绑定一个死信交换机exchageY

java 复制代码
package com.gd.producer.config;

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

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

@Configuration
public class RabbirmqConfig {
    public static final String EXCHANGE_X = "exchageX";
    public static final String EXCHANGE_Y = "exchageY";
    public static final String QUEUE_A = "queueA";
    public static final String QUEUE_D = "queueD";
    @Bean(value = "exchangeX")
    public DirectExchange exchangeX() {
        return new DirectExchange(EXCHANGE_X);
    }
    @Bean(value = "exchangeY")
    public DirectExchange exchangeY() {
        return new DirectExchange(EXCHANGE_Y);
    }
   
    @Bean(value = "queueD")
    public Queue queueD() {
        return new Queue(QUEUE_D);
    }
    @Bean(value = "queueA")
    public Queue queueA() {
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange",EXCHANGE_Y);
        args.put("x-dead-letter-routing-key","YD");
        args.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }
    @Bean
    public Binding bindingxtoa(@Qualifier("queueA") Queue queueA, @Qualifier("exchangeX") DirectExchange exchangeX) {
        return BindingBuilder.bind(queueA).to(exchangeX).with("XA");
    }
    @Bean
    public Binding bindingytod(@Qualifier("queueD") Queue queueD, @Qualifier("exchangeY") DirectExchange exchangeY) {
        return BindingBuilder.bind(queueD).to(exchangeY).with("YD");
    }
}

7 延迟队列

7.1 延迟队列的概念

延迟队列就是等一阵子再去处理这个消息

7.2 使用场景

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

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果 数据量比较少,确实可以这样做,比如:对于"如果账单一周内未支付则进行自动结算"这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单, 确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:"订单十分钟内未支付则 关闭",短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

7.3 在springboot中的整合

  1. 配置类代码
java 复制代码
@Configuration 
public class TtlQueueConfig { 
    public static final String X_EXCHANGE = "X"; 
    public static final String QUEUE_A = "QA"; 
    public static final String QUEUE_B = "QB"; 
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    public static final String DEAD_LETTER_QUEUE = "QD"; 
    // 声明 xExchange 
    @Bean("xExchange") 
    public DirectExchange xExchange(){ 
        return new DirectExchange(X_EXCHANGE); 
    } // 声明 yExchange 
    @Bean("yExchange") 
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE); 
    } 
    //声明队列 A ttl 为 10s 并绑定到对应的死信交换机 
    @Bean("queueA") public Queue queueA(){ 
        Map args = new HashMap<>(3); 
        //声明当前队列绑定的死信交换机 
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key 
        args.put("x-dead-letter-routing-key", "YD"); 
        //声明队列的 TTL 
        args.put("x-message-ttl", 10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build(); 
    } 
    // 声明队列 A 绑定 X 交换机 
    @Bean 
    public Binding queueaBindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA"); 
    } 
    //声明队列 B ttl 为 40s 并绑定到对应的死信交换机 
    @Bean("queueB") 
    public Queue queueB(){
        Map args = new HashMap<>(3); 
        //声明当前队列绑定的死信交换机 
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); 
        //声明当前队列的死信路由 
        key args.put("x-dead-letter-routing-key", "YD"); 
        //声明队列的 TTL 
        args.put("x-message-ttl", 40000); 
        return QueueBuilder.durable(QUEUE_B).withArguments(args).build(); 
    } 
    //声明队列 B 绑定 X 交换机 
    @Bean 
    public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queue1B).to(xExchange).with("XB"); 
    } 
    //声明死信队列 QD 
    @Bean("queueD") 
    public Queue queueD(){
        return new Queue(DEAD_LETTER_QUEUE); 
    } 
    //声明死信队列 QD 绑定关系 
    @Bean 
    public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD"); 
    } 
}
  1. 生产者
java 复制代码
@Slf4j 
@RestController 
@RequestMapping("/ttl") 
public class SendMsgController { 
    @Autowired 
    private RabbitTemplate rabbitTemplate;
    // 开始发消息 
    @GetMapping("/sendMsg/{message}") 
    public void sendMsg(@PathVariable String message){ 
        log.info("当前时间:{}, 发送一条信息给两个 TTL 队列:{}", new Date().toString(), message);
        rabbitTemplate.convertAndSend("X", "XA", "消息来自 TTL 为 10s 的队列:" + message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自 TTL 为 40s 的队列:" + message); 
    } 
}
  1. 消费者
less 复制代码
@Component
@Slf4j
public class DeadLetterQueueConsumer {
    @RabbitListener(queues = "queueD")
    public void receiveD(Message message, Channel channel) throws
            IOException {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
    }
}

上文是声明通过声明队列的 TTL args.put("x-message-ttl", 40000);来实现

7.4 优化

通过给消息加时间

java 复制代码
@Bean("queueC") 
public Queue queueB(){
    Map args = new HashMap<>(3); 
    //声明当前队列绑定的死信交换机 
    args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); 
    //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "YD"); 
    //没有声明 TTL 属性 
    return QueueBuilder.durable(QUEUE_C).withArguments(args).build(); 
}
---
@GetMapping("sendExpirationMsg/{message}/{ttlTime}") 
    public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
      rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{ correlationData.getMessageProperties().setExpiration(ttlTime); return correlationData; }); 
      log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message); 
   }

7.5 使用延迟插件

  1. 官网下载:www.rabbitmq.com/community-p...

2. 上传到plugins目录上

  • 进入到rabbitmq docker exec -it rabbitmq的container name /bin/bash
  • 进入到plugin目录 `cd plugins/
  • rabbitmq_delayed_message_exchange-3.12.0.ez上传到plugin目录下
  • 安装 rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  • 重启rabbitmq systemctl restart rabbitmq-server 重启之后rabbitmq的控制台如下显示

3. springboot中使用

  • 配置类
java 复制代码
@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }
    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
//自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayedmessage", true, false,
                args);
    }
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue
                                               queue,
                                       @Qualifier("delayedExchange")
                                       CustomExchange
                                               delayedExchange) {
        return
                BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).no
        args();
    }
}
  • 消息生产者
java 复制代码
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
// 基于插件的 开始发消息 消息 以及 延迟的时间
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message, @PathVariable Integer
        delayTime){
    log.info("当前时间:{}, 发送一条时长 {} 毫秒 信息给延迟队列delayed.queue :
    {}",
    new Date().toString(), delayTime, message);
    rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,
            DelayedQueueConfig.DELAYED_ROUTING_KEY, message, msg ->{
// 发送消息的时候 延长时长 单位是 ms
                msg.getMessageProperties().setDelay(delayTime);
                return msg;
            });
}
  • 消费者
java 复制代码
@Slf4j
@Component
public class DelayQueueConsumer {
    // 监听消息
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayQueue(Message message){
        String msg = new String(message.getBody());
        log.info("当前时间:{}, 收到延迟队列的消息:{}", new Date().toString(),
                msg);
    }
}

8 如何保证消息不被重复消费

8.1 消息一定会重复

首先是发送消息的流程

上面在消息的可靠传输里提到,生产者发送消息给broker后,需要等待broker响应的ack才能确保消息已经被broker存储了。

那么假设broker已经确实收到了这条消息并成功存储了,但返回给生产者ack的时候因为网络原因,生产者并没有收到这条消息的ack,那么为了保证消息不会丢失,生产者只能再次发送这次消息。

在这种情况下,同一条消息就被发送了两次,broker上就存在了两条一样的消息。

这里有同学就会想,既然为了保证消息不丢失,生产者没办法,只能多次发送同一条消息,那么就由broker来过滤这些消息吧!

理论上是可以实现的,但是实际应用消息的体量都会比较大,broker的负载本身就比较高,如果再加上去重的功能,就需要解析接收到的所有消息内容,然后进行对比,这会进一步加重broker的负担,高并发情况下会大大的降低性能。

所以,无法避免消息不重复发送和broker存储一样的数据。

从消费的角度看,就算是broker实现了消息去重的功能,也无法保证消息一定只被消费者消费一次。

因为消费者和broker之间也是基于ack的,和生产者一样,如果消息已经被消费者消费完了,在给broker发送ack的时候因为网络问题broker没接收到,就会把消息重新入队。

所以实际上消息无法保证不重复,但是我们可以保证它仅被幂等消费来达到仅消费一次的效果。

8.2 符合天然幂等写法

在项目初期或者新功能刚开始设计的时候,就需要考虑幂等的设计来满足业务的需求。比如对于update语句,利用天然的幂等写法来满足幂等的消费。

幂等写法主要有两种,1)一种是使用主键或者是唯一键确保每次在执行insert语句时不会插入相同的数据。2)使用乐观锁机制,通过向表中添加一个字段(比如status),每次在更新记录时候就检查字段是否符合预期。

比如有个消费消息的业务:给业务加积分,并且给订单的状态从待完成变成已完成。

首先我们要调整下执行的顺序,不是先加积分,而是先改变订单的状态,执行update order set status=1 where orderNo = 123 and status = 0

这里添加一个status=0的判断

8.3 数据库唯一索引

但是往往很多需求不是初期就有的,而是后面迭代,此时整个业务已经定型了,业务已经很复杂,不太好改造出上述uodate这样的语句。

此时可以利用数据库的唯一索引约束来保证消费的幂等消息,例如可以给消息加个事务ID,这个事务ID是全局唯一的,数据库表记录给这个事务ID添加唯一索引,当第一次处理完这条消息的时候,同时在数据库中存储这条消息的处理记录,如果后面有重复消息过来,那么插入一定是会抛错的,这样一来就能避免消息的重复消费实现消费幂等。

具体操作起来大致有两个方向:第一个方向是利用当前的已有的字段来作为唯一索引,比如订单的处理,那么订单的订单号肯定是唯一的,此时不需要再额外添加一个事务ID的字段。如果没有合适的已有字段,那么就扩展一个事务ID字段来满足要求。

8.4 redis唯一判断

同样redis 也能实现幂等的功能,相比数据库的唯一索引需要改表结构,或者新加一张流水表redis更加简单,利用 SETNX 这个命令就能实现幂等消费。

同样也是用全局唯一值来标记这条消息,例如订单号或者定义的事务D,每次在业务逻辑执行之前先利用 SETNX 来判断下,如果已经插入就直接返回,反之正常执行业务逻辑。

但是这里有个问题,如果唯一值插入redis 后,消费者直接宕机了,业务逻辑并没有执行成功,那么即使由另一个消费者顶上消费到这条消息,由于redis 还存储着这个唯一值,会使得这条消息被跳过,这样这条消息就跟丢了是一样的。 所以在异常情况下,这个方案会有这个问题,需要大家注意下。

9 解决消息的积压问题

这里一共有两点需要注意 1.因为rabbitmq默认是负载均衡的,也就是说消息是一替一的分发的。但不同的消费者的消费能力是不一样的,如果一个消费者的消费能力很强,此时就需要等到那个慢的处理完才能再把消息给它,这时候就会影响消费性能,所以这时候我们就可以让能者多劳

yml 复制代码
server:
  port: 8081
spring:
  rabbitmq:
    host: 49.235.99.193
    port: 5672
    username: admin
    password: 123
    virtual-host: test
    listener:
      simple:
        prefetch: 1 # 设置预取值为1(当为1时也被称为不公平分发)
相关推荐
zopple8 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001119 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本10 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji341610 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan11 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer12 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor35612 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor35612 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer13 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP13 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪