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时也被称为不公平分发)
相关推荐
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠6 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#