RabbitMQ 消息队列

定义


RabbitMQ 是一种消息中间件,用来接收、存储、转发数据库。类似一个快递站,我们把快递给到这个快递站、快递站暂存我的快递,然后转发到别的地方。

核心概念


四大组成部分

生产者(Producer):产生消息的程序;

交换机(Exchange) : 生成者产生的消息首先会发到交换机中,然后把消息推送到队列中,可以指定推送到多个队列、单个队列或者丢弃消息;

队列(Queue):接收来自交换机的消息,本质是一个消息缓冲区,消息真正存储在队列中;

消费者(Consumner):处理来自指定队列的消息;

核心名称

RabbitMQ 整体框架

Broker接收和转发消息的应用,RabbitMQ Server 就是 Broker Server;

Connection :Producer 与 Consumer 和 Broker 之间的 TCP 连接;

Channel :每一次访问 Broker 都会建立一个 Channel,Channel 是在 Connection 中建立的逻辑连接,AMQP method 包含了 channel id 帮助客 户端和 message broker 识别 channel,所以 channel 之间是完全隔离的,极大减少了建立 TCP 连接的开销;

Exchange :message 到达 Broker 的第一站,根据 routing key 的分发规则把消息分发到不同的 Queue 中

Queue:消息存储的地方,等待被 consumer 取走;

Binding:exchange 与 queue 之间的虚拟连接,bingding 中包含 routing key ,routing key 放在 exchange 的查询表中,用于 message 的分发;

消息分发模式

简单模式

简单模式是最基本的工作模式,一个生产者将消息发送到一个队列,一个消费者从队列中获取消息进行消费

工作队列模式

工作队列模式是一个任务在多个消费者之间并发处理,一个消息只能被一个消费者消费,这种模式可以提供系统的吞吐量和处理能力

发布订阅模式

发布订阅模式是实现一个消息同时被多个消费者处理,生产者将消息发送到交换机,交换机消息广播 到所有的绑定的队列上,每个队列对应一个消费者,这种模式适合一个消息被多个消费者消费

路由模式

路由模式能够实现将消息传递给指定的队列去消费,实现不同的消息发送到不同的队列中去处理

主题模式

主题模式能够实现复杂的消息的分发,生产者发送消息到交换机,然后根据路由键指定的匹配规则将消息分发到不同的队列中

RPC 模式

RPC 是一种实现分布式系统中远程调用的工作模式,指的是通过 RabbitMQ 来实现的一种 RPC 的能力

整合 SpringBoot


编写配置 yml 文件

yaml 复制代码
rabbitmq:
  host: localhost
  port: 5672
  username: admin
  password: 123456
  publisher-confirm-type: correlated  # 交换机回退消息
  template:
    mandatory: true # 队列回退消息

引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  	<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
		<version>2.3.12.RELEASE</version>	
</dependency>

Config 文件绑定交换机和队列

在 RabbitMqConfig.java 编写注册交换机和队列,以及配置两者之间的关系

kotlin 复制代码
@Configuration
public class RabbitMqConfig {

    /**
     * 注册交换机
     * @return 交换机
     */
    @Bean("lotteryActivityResultExchange")
    public DirectExchange lotteryActivityResultExchange() {
        return ExchangeBuilder
                .directExchange("LOTTERY_ACTIVITY_RESULT_EXCHANGE")
                .durable(true)
                .build();
    }

    /**
     * 注册队列
     * @return 队列
     */
    @Bean("lotteryActivityResultQueue")
    public Queue lotteryActivityResultQueue() {
        return QueueBuilder
                .durable("LOTTERY_ACTIVITY_RESULT_QUEUE")
                .build();
    }

    /**
     * 配置绑定关系
     * @param exchange 交换机
     * @param queue 队列
     * @return 绑定关系
     */
    @Bean
    public Binding lotteryActivityResultBinding(@Qualifier("lotteryActivityResultExchange") DirectExchange exchange,
                                                @Qualifier("lotteryActivityResultQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("LOTTERY_ACTIVITY_RESULT");
    }
}

生产者 Producer

typescript 复制代码
@Component
public class LotteryActivityStockProducer implements RabbitTemplate.ConfirmCallback,
        RabbitTemplate.ReturnCallback {

    private final static Logger logger = LoggerFactory.getLogger(LotteryActivityStockProducer.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;


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

    public void sendUpdateActivityStock(Long goodId) {

        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(goodId.toString());

        Message message = new Message(ByteUtil.longToBytes(goodId));
        rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_STOCK_EXCHANGE,
                Code.RabbitMqMessage.LOTTERY_ACTIVITY_STOCK_RK, message, correlationData);
        logger.info("LotteryActivityStockProducer|sendUpdateActivityStock|发送消息,更新秒杀活动表的库存|activityId: {}", goodId);
    }

    /**
     * 消息到达交换器的确认回调
     *
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("LotteryActivityStockProducer|sendUpdateActivityStock|消息到达交换机,更新秒杀活动表的库存|activityId: {}", correlationData.getId());
            return;
        }

        logger.error("LotteryActivityStockProducer|sendUpdateActivityStock|消息未到达交换机,更新库存失败|activityId: {}", correlationData.getId());
    }

    /**
     * 消息到达队列的回调
     *
     * @param message
     * @param replyCode
     * @param replyText
     * @param exchange
     * @param routingKey
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

        logger.error("LotteryActivityStockProducer|sendUpdateActivityStock|未到达指定队列,更新库存失败|errMsg: {}, exchange: {}, routingKey: {}"
                , replyText, exchange, routingKey);
    }
}

消费者 Consumer

java 复制代码
@Component
public class LotteryActivityConsumer {

    private static final Logger logger = LoggerFactory.getLogger(LotteryActivityConsumer.class);

    @Resource
    private LotteryActivityMapper lotteryActivityMapper;

    @RabbitListener(queues = Code.RabbitMqMessage.LOTTERY_ACTIVITY_STOCK_QUEUE)
    public void resultLotteryActivityStockConsumer(Message message, Channel channel) {
        byte[] body = message.getBody();
        Long activityId = null;
        try {

            // 其实数据量太大也会拖垮数据库,可以使用 xxljob 定时任务去扫描缓存中的库存数量进行更新库粗
            activityId = ByteUtil.bytesToLong(body);

            logger.info("LotteryActivityConsumer|resultLotteryActivityConsumer|收到活动id|activityId: {}", activityId);

            int stock = lotteryActivityMapper.decreaseActivityStock(activityId);
            if (stock > 0) {
                long tag = message.getMessageProperties().getDeliveryTag();
                channel.basicAck(tag, false);
                logger.info("LotteryActivityConsumer|resultLotteryActivityConsumer|更新成功|activityId: {}", activityId);
                return;
            }
            logger.info("LotteryActivityConsumer|resultLotteryActivityConsumer|更新失败|activityId: {}", activityId);
            // ... 补偿措施
        } catch (Exception e) {
            logger.error("LotteryActivityConsumer|resultLotteryActivityConsumer|更新失败|activityId: {}", activityId, e);
            // ... 补偿措施
        }
    }
}

高级特性

消费模式

推模式

MQ 主动将消息推送给消费者,这种方法需要消费者设置一个缓冲区去接收消息,对于消费者而言,内存中总是有一堆需要处理的消息,效率较高

typescript 复制代码
@Component
public class ConsumerDemo {
    @RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)
    public void handle(String msg) {
        System.out.println("msg = " + msg);
    }
}

使用 @RabbitListener 注解主动接收来自生产者的消息

拉模式

消费者主动从队列中拉取消息,这种效率不是很高

java 复制代码
public void test01() throws UnsupportedEncodingException {
    Object o = rabbitTemplate.receiveAndConvert(RabbitConfig.JAVABOY_QUEUE_NAME);
   
}

receiveAndConvert 消费者主动从队列中拉取消息,如果结果为空则表示没有消息

RabbitMQ 发送的可靠性

消息发送成功要同时满足一下两个条件

  1. 消息发送到指定的 Exchange
  2. 消息发送到指定的 Queue

如果能达到这两步,就可以认为消息成功发送了,因为 Queue 有持久化机制

开启事务机制

基于 SpringBoot 的配置

设置事务管理器

typescript 复制代码
@Bean
RabbitTransactionManager transactionManager(ConnectionFactory connectionFactory) {
    return new RabbitTransactionManager(connectionFactory);
}

在消息生产者上面做两件事:添加事务注解并设置通信信道为事务模式:

java 复制代码
@Service
public class MsgService {
    @Autowired
    RabbitTemplate rabbitTemplate;

    @Transactional
    public void send() {
        rabbitTemplate.setChannelTransacted(true);
        rabbitTemplate.convertAndSend(RabbitConfig.JAVABOY_EXCHANGE_NAME,RabbitConfig.JAVABOY_QUEUE_NAME,"hello rabbitmq!".getBytes());
        int i = 1 / 0;
    }
}

这里注意两点:

  1. 发送消息的方法上添加 @Transactional 注解标记事务。
  2. 调用 setChannelTransacted 方法设置为 true 开启事务模式。

事务交互过程

  1. 客户端发送请求,把信道设置为事务模式;
  2. 服务端给出回复,同意将信道设置为事务模式;
  3. 客户端发送消息;
  4. 客户端提交事务;
  5. 服务端给出回应并且确认事务提交

发送方确认机制

基于 SpringBoot 的配置

ini 复制代码
spring.rabbitmq.publisher-confirm-type=correlated # Exchange 的回退消息
spring.rabbitmq.publisher-returns=true # 队列的回退消息

第一个是消息到达交换机的确认回调(Publisher Comfirm),无论成功或者失败都可以回调,其中有三个取值

  • none:表示禁用发布确认模式,默认值;
  • correlated:表示成功发布消息到交换机后执行回调方法;
  • simple:类似 correlated,并且支持 waitForConfirms()waitForConfirmsOrDie() 方法的调用

第二个是队列的确认回调(Publisher Return),若根据 routing key 没有找到指定的队列,则会执行该回调函数

代码实现

  • 实现接口:RabbitTemplate.ConfirmCallbackRabbitTemplate.ReturnCallback前者是交换机的确认回调接口,后者是队列的确认回调接口
  • 使用 @PostConstruct 把两个接口注入到 rabbitTemplate 中;
typescript 复制代码
@Component
public class LotteryActivityGoodStockProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private Logger logger = LoggerFactory.getLogger(LotteryActivityGoodStockProducer.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;


    public void sendUpdateActivityGoodStock(Long goodId) {

        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(goodId.toString());

        // 发送扣库存消息
        Message message = new Message(ByteUtil.longToBytes(goodId));
        // 消息持久化
        MessageProperties messageProperties = message.getMessageProperties();
        messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);

        rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_EXCHANGE,
                Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_RK, message, correlationData);

        logger.info("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|发送消息,更新秒杀活动表的库存|goodId: {}", goodId);
    }

    
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|消息到达交换机,更新秒杀活动表的库存|goodId: {}", correlationData.getId());
            return;
        }

        logger.error("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|消息未到达交换机,更新库存失败|goodId: {}", correlationData.getId());
    }

   
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

        logger.error("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|未到达指定队列,更新库存失败|errMsg: {}, exchange: {}, routingKey: {}"
                , replyText, exchange, routingKey);
    }
}

失败重试

自带失败重试

当发送方连接不上 MQ 时,Spring 会使用 retry 机制进行重试

ini 复制代码
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000ms
spring.rabbitmq.template.retry.max-attempts=10
spring.rabbitmq.template.retry.max-interval=10000ms
spring.rabbitmq.template.retry.multiplier=2

enabled:是否开启;

initial-interval:重试起始间隔时间

max-attempts:最大尝试次数

max-interval:最大重试间隔

multiplier:间隔时间乘数(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)

业务重试

在表中加入 MqState,MsgId 的字段

  • MqState:0-发送中 1-发送成功 2-发送失败
  • MsgId:消息的唯一标识,进行幂等处理

重试过程

  1. 当我们开始发送消息时,将 MqState 状态设置为 0,
  2. 在 confim 的回调函数中,如果返回成功,则把该状态设置为 1,若是失败则设置为 2;
  3. 启动一个定时器,定时捞起 MqState 值为 2 的记录,进行重新发送

利用 xxl-job 重试

在 admin 控制台创建 执行器

配置 xxl-job 的 XxlJobConfig.java 文件

kotlin 复制代码
@Configuration
public class XxlJobConfig {

    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;


    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

    /**
     * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
     *
     *      1、引入依赖:
     *          <dependency>
     *             <groupId>org.springframework.cloud</groupId>
     *             <artifactId>spring-cloud-commons</artifactId>
     *             <version>${version}</version>
     *         </dependency>
     *
     *      2、配置文件,或者容器启动变量
     *          spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
     *
     *      3、获取IP
     *          String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
     */


}

配置 xxl-job 的 yml 文件

yaml 复制代码
xxl:
  job:
    admin:
      addresses: http://112.74.73.126:8080/xxl-job-admin # admin 终端
    executor:
      appname: draw-result-order  # 执行器名称
      ip:  # 为空表示自动获取
      port: 9999 # 执行器端口
      logpath: /data/applogs/xxl-job/jobhandler # 日志
      logretentiondays: 30
    accessToken: default_token

编写定时任务 @XxlHandler

ini 复制代码
@XxlJob("scanDrawResultHandler")
public void scanDrawResultHandler() {

    // 每次捞10条
    List<Long> ids = userDrawResultMapper.selectResultByErrorMqState(beginId, pageSize);

    if (ids.size() == 0) {
        beginId = 1L;
        logger.info("DrawResultJob|scanDrawResultHandler|没有错误的记录|beginId: {}", beginId);
        return;
    }
    List<DrawResult> drawResults = userDrawResultMapper.selectDrawResultsByIds(ids);
    for (DrawResult drawResult : drawResults) {
        lotteryActivitySendOrderProducer.sendLotteryActivityOrderMessage(drawResult);
    }
    logger.info("DrawResultJob|scanDrawResultHandler|完成扫描,并重新发送|ids: {}", ids);
    if (ids.size() < pageSize) {
        beginId = 1L;
    } else {
        beginId = ids.get(pageSize - 1) + 1;
    }
}

将该 handler 注册到某个执行器中,并开启该任务

RabbitMQ 消费的可靠性

自动应答

消息发送后就是立即被认为发送成功了,在消息接收到之前,消费者出现连接 或者 connection 关闭,则消息就丢失了

在 SpringBoot 中,消息发送后立即被认为是已经传送成功了。通过 @Component 注解把该类注入到容器中,然后通过 @RabbitMQListener 注解标注某个消费方法,若发送消息过程中出现了异常,则该消息会重回队列重新消费。

手动应答

为了保证消息能够可靠的到达消息消费者,RabbitMQ 中提供了消息消费确认机制。当消费者去消费消息的时候,可以通过指定 autoAck 参数来表示消息消费的确认方式。

  • 当 autoAck 为 false 的时候,此时即使消费者已经收到消息了,RabbitMQ 也不会立马将消息移除,而是等待消费者显式的回复确认信号后,才会将消息打上删除标记,然后再删除。

  • 当 autoAck 为 true 的时候,此时消息消费者就会自动把发送出去的消息设置为确认,然后将消息移除(从内存或者磁盘中),即使这些消息并没有到达消费者。

当我们将 autoAck 设置为 false 的时候,对于 RabbitMQ 而言,消费分成了两个部分:

  • 待消费的消息
  • 已经投递给消费者,但是还没有被消费者确认的消息

肯定应答void basicAck(long deliveryTag, boolean multiple)

deliveryTag:消息的标记

multiple:是否批量确认消息

否定应答void basicNack(long deliveryTag, boolean multiple, boolean requeue)

deliveryTag:消息的标记

multiple:是否批量否定消息(true:拒绝所有消息,false:仅拒绝提供的标签的消息)

requeue:是否重新入队或者进入丢弃/死信队列(true:重新入队,false:丢弃/进入死信队列)

拒绝应答void basicReject(long deliveryTag, boolean requeue)

deliveryTag:消息的标记

requeue:是否重新入队或者进入丢弃/死信队列(true:重新入队,false:丢弃/进入死信队列)

否定应答批量拒绝,而拒绝应答只能拒绝一个消息

springboot 开启手动应答:spring.rabbitmq.listener.simple.acknowledge-mode=manual

推模式确认

typescript 复制代码
public void handle3(Message message,Channel channel) {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
        //消息消费的代码写到这里
        String s = new String(message.getBody());
        System.out.println("s = " + s);
        //消费完成后,手动 ack
        channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
        //手动 nack
        try {
            channel.basicNack(deliveryTag, false, true);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

拉模式确认

csharp 复制代码
public void receive2() {
    Channel channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(false);
    long deliveryTag = 0L;
    try {
        GetResponse getResponse = channel.basicGet(RabbitConfig.JAVABOY_QUEUE_NAME, false);
        deliveryTag = getResponse.getEnvelope().getDeliveryTag();
        System.out.println("o = " + new String((getResponse.getBody()), "UTF-8"));
        channel.basicAck(deliveryTag, false);
    } catch (IOException e) {
        try {
            channel.basicNack(deliveryTag, false, true);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

RabbitMQ 幂等性问题

幂等:用户通过同一操作发出的一次或者多次请求的结果是一致的,不会因为多次点击而产生副作用

场景:

消费者在成功消费了一条数据后,向 MQ 发送 ACK,但此时遭遇网络故障,MQ 没有接收到 ACK。网络恢复后,MQ 认为该消息没有成功发送,故向消费者再次推送这条消息,造成消息的重复消费

幂等通用解决方法

  1. 先加锁,一定要是互斥锁,分布式锁或者悲观锁都可以

  2. 判断是否存在幂等,基于流水表,唯一索引等

  3. 然后进行数据更新,即持久化

RabbitMQ 消息有效期

在默认情况下,队列中的消息是不会过期的,只要没有宕机(消息没有持久化处理),消息就一直会存在队列中

TTL

TTL(Time To Live),消息的存活时间,即消息的有效期。如果消息的存活时间超过了 TTL 且没有被消费,那么这条消息就会变成 死信 ,进入 死信队列 中

TTL 有两种设置方式(以最小的为准):

  1. 创建队列时,可以对队列设置一个 TTL,表示该队列中所有消息的过期时间;
  2. 生产者发送消息时,可以对该消息设置一个 TTL ,表示这个消息的过期时间;

TTL 删除的方式

  1. 对于第一种情况,TTL 到了之后直接删除,因为队列头部的就是最早过期的消息,每次只要判断该队列头部的消息即可;
  2. 对于第二种情况,TTL 到了之后是不会被直接删除,而是等到该消息投递给消费者后才会被删除,避免遍历整个队列判断消息是否过期(因为用户设置的消息过期时间是不同的,队头的消息过期的时间可能比对尾的还长)

消息 TTL 过期

ini 复制代码
public void sendLotteryActivityOrderMessage(DrawResult drawResult) {


        SendOrderMessage sendOrderMessage = new SendOrderMessage();
        sendOrderMessage.setGoodId(drawResult.getGoodId());
        sendOrderMessage.setGoodName(drawResult.getGoodName());
        sendOrderMessage.setGoodCounts(1);
        sendOrderMessage.setUserId(drawResult.getUserId());

        String jsonString = GsonUtil.ObjectToJsonString(sendOrderMessage);
        // 设置过期时间
        Message message = new Message(jsonString.getBytes(StandardCharsets.UTF_8));
        message.getMessageProperties().setExpiration("10000");
        
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(sendOrderMessage.getGoodId().toString());
        rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_ORDER_EXCHANGE,
                Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_ORDER_RK, message,correlationData);
    }

队列 TTL 过期

typescript 复制代码
@Bean
Queue queue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-message-ttl", 10000);
    return new Queue(JAVABOY_QUEUE_DEMO, true, false, false, args);
}

死信队列

死信交换机

死信交换机来接受死信消息的,死信消息有以下的情况:

  • 消息被拒绝且 requeue 为 false;
  • 消息过期
  • 队列已满

死信交换机本质上跟普通的交换机没有什么不同,只是用来接收死信消息的

死信队列

绑定了死信交换机的队列,我们可以指定任何队列绑定死信交换机

基于 SpringBoot 配置

typescript 复制代码
/**
 * 普通消息队列
 * @return
 */
@Bean
Queue javaboyQueue() {
    Map<String, Object> args = new HashMap<>();
    //设置消息过期时间
    args.put("x-message-ttl", 1000*10);
    //设置死信交换机
    args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
    //设置死信 routing_key
    args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
    return new Queue(JAVABOY_QUEUE_NAME, true, false, false, args);
}

/**
 * 普通交换机
 * @return
 */
@Bean
DirectExchange javaboyExchange() {
    return new DirectExchange(JAVABOY_EXCHANGE_NAME, true, false);
}

/**
 * 绑定普通队列和与之对应的交换机
 * @return
 */
@Bean
Binding javaboyBinding() {
    return BindingBuilder.bind(javaboyQueue())
            .to(javaboyExchange())
            .with(JAVABOY_ROUTING_KEY);
}

就两个参数:

  • x-dead-letter-exchange:配置死信交换机。
  • x-dead-letter-routing-key:配置死信 routing_key

将来发送到这个消息队列上的消息,如果发生了 nack、reject 或者过期等问题,就会被发送到 DLX 上,进而进入到与 DLX 绑定的消息队列上。

RabbitMQ 持久化

交换机持久化

kotlin 复制代码
 /**
 * 注册交换机
 * @return 交换机
 */
@Bean("decreaseApiUsedCountExchange")
public DirectExchange decreaseApiUsedCountExchange() {
    return ExchangeBuilder
            .directExchange("decreaseApiUsedCountExchange")
            .durable(true)
            .build();
}

队列持久化

如果 rabbit 重启会造成之前的队列被删除,如果要实现持久化就要把 durable 设置为 true,代表开启持久化

kotlin 复制代码
/**
 * 注册队列
 * @return 队列
 */
@Bean("lotteryActivityOrderQueue")
public Queue lotteryActivityOrderQueue() {
    return QueueBuilder
            .durable(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_ORDER_QUEUE)
            .build();
}

消息持久化

在消息生产者发布消息的时候,开启消息持久化

ini 复制代码
public void sendUpdateActivityGoodStock(Long goodId) {

    CorrelationData correlationData = new CorrelationData();
    correlationData.setId(goodId.toString());

    // 发送扣库存消息
    Message message = new Message(ByteUtil.longToBytes(goodId));
    // 消息持久化
    MessageProperties messageProperties = message.getMessageProperties();
    messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);

    rabbitTemplate.convertAndSend(Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_EXCHANGE,
            Code.RabbitMqMessage.LOTTERY_ACTIVITY_GOOD_STOCK_RK, message, correlationData);

    logger.info("LotteryActivityGoodStockProducer|sendUpdateActivityGoodStock|发送消息,更新秒杀活动表的库存|goodId: {}", goodId);
}

但也不是完全保持持久化,如果在持久化过程中宕机了,也是不会将消息完整持久化到磁盘中的

如何保证 RabbitMQ 消息的可靠性

RabbitMQ 的消息链路是生产者 -> 交换机 -> 队列 -> 消费者

生产者端,发送消息丢失的情况有两种:生产者 -> 交换机,交换机 -> 队列

在这两个过程中,我们可以分别开启消息 confirm 机制和消息 return 机制,分别判断消息是否到达交换机和队列

其中 confirm 机制能够返回发送成功还是没有发送成功的回退消息,而 return 机制是在 exchange 没有找到对应的 queue 时才会执行

传输过程 中,可能面临 RabbitMQ 宕机的问题,一旦宕机 RabbitMQ 中的交换机、队列、消息都会消失,这是就需要我们开启持久化机制

RabbitMQ 对交换机、队列、消息都提供了持久化机制的实现。在创建交换机或者队列时,我们可以通过 durable 方法实现持久化,而消息的持久化则需要在消息发送时选择 PERSISTENT Mode。但是引入持久化机制可能会增加磁盘的 I/O

但是持久化机制不能 100% 保证消息不丢失,如果在写入磁盘的过程中宕机,那么消息时也会丢失的

消费者端,我们可以通过消息的确认机制来保证消费者处理掉消息,即手动 ACK,当没有消费者没有发送 ACK , RabbitMQ 就会选择重新发送或者进入死信队列(由消费者端选择具体的处理方式)

注意保证消息的幂等

相关推荐
.生产的驴11 分钟前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛19 分钟前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
爱学的小涛20 分钟前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪24 分钟前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
爱码少年30 分钟前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip
2401_857622668 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589368 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没10 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch10 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码11 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端