RabbitMQ

消息丢失

[服务器-持久化]
将内存的数据(交换器 Exchange、队列 Queue、消息 Message)存储到硬盘中,防止 RabbitMQ 宕机导致数据丢失。
[生产者-发布确认]
[消费者-消息应答]

消费者完成一个任务需要一段时间,如果其中一个消费者处理一个长任务并且仅仅完成了部分,此时它宕机了,那么我们将丢失正在处理的消息,以及后续发送给该消费者的消息。
为了保证消息在发送过程中不丢失,RabbitMQ 引入了消息应答机制:消费者处理完成该消息后通知 RabbitMQ,RabbitMQ 才可以删除该消息。

消息应答分为手动应答和自动应答。

[手动应答](推荐)
boolean autoAck = false;
将消息发送给消费者并且等待处理完成后,才会删除队列中的消息。
如果消费者由于某些原因导致消息未发送 ACK 确认,RabbitMQ 会将消息放回队列,将其重新分发给其他消费者处理,确保不会丢失任何消息。
手动应答支持批量应答(multiple),但不推荐使用。
public class Consumer {
channel.basicAck(long deliveryTag, boolean multiple)
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)
channel.basicReject(long deliveryTag, boolean requeue)

[自动应答]
boolean autoAck = true;
只要将消息发送给消费者就删除队列中的消息,不管是否处理完成。
默认采用自动应答,所以要想实现消息不丢失,需要改为手动应答。

消息堆积

消息被拒绝后不再重新入队,而是使用死信队列接收。

消息幂等性

https://help.aliyun.com/document_detail/177412.html
在互联网应用中,尤其在网络不稳定的情况下,消息队列 RabbitMQ 版的消息有可能会出现重复。

1、发送时消息重复
当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。
如果此时 Producer 意识到消息发送失败并尝试再次发送消息,Consumer 后续会收到两条内容相同并且 Message ID 也相同的消息。
2、投递时消息重复
消息消费的场景下,消息已投递到 Consumer 并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。
为了保证消息至少被消费一次,消息队列 RabbitMQ 版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,Consumer 后续会收到两条内容相同并且 Message ID 也相同的消息。
3、负载均衡时消息重复(包括但不限于网络抖动、服务端重启以及 Consumer 应用重启)
当消息队列 RabbitMQ 版的服务端或客户端重启、扩容或缩容时,会触发 Rebalance,此时 Consumer 可能会收到重复消息。
setnx

消息顺序性

image

对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息,如消费者 A 执行了增加,消费者 B 执行了修改,消费者 C 执行了删除,但是消费者 C 执行比消费者 B 快,消费者 B 又比消费者 A 快,就会导致消费 binlog 执行到数据库的时候顺序错乱,本该顺序是增加、修改、删除,变成了删除、修改、增加。

image

RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。解决这个问题,我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,生产者发送消息的时候,同一个订单号的消息发送到同一个 queue 中,由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。

死信队列

死信,即无法消费的消息。

通常来源于:
1、消息过期
messageProperties.setExpiration("5000");
2、消息超出队列长度
maxLength(5)
3、消息被拒绝
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);

延迟队列

=====

下载安装

docker pull docker.io/rabbitmq:3.8-management

docker run -d -it \
--name rabbitmq5672 \
--restart=always \
--privileged=true \
-p 15672:15672 \
-p 5672:5672 \
docker.io/rabbitmq:3.8-management

docker exec -it rabbitmq5672 /bin/bash
rabbitmqctl add_user root rabbitmq
rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
rabbitmqctl set_user_tags root administrator
exit

wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.8.0/rabbitmq_delayed_message_exchange-3.8.0.ez
docker cp rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq5672:/opt/rabbitmq/plugins/
rm -rf rabbitmq_delayed_message_exchange-3.8.0.ez

docker exec -it rabbitmq5672 /bin/bash
cd /opt/rabbitmq/plugins/
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
exit

docker restart rabbitmq5672
访问 http://vmwarehost:15672/
账号 root 密码 rabbitmq

综合案例

<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
  rabbitmq:
    host: vmwarehost
    port: 5672
    # addresses: localhost:5672
    username: root
    password: rabbitmq
    # virtual-host: /test
    listener:
      simple:
        # 消息应答
        acknowledge-mode: manual
        default-requeue-rejected: false
      direct:
        # 消息应答
        acknowledge-mode: manual
        default-requeue-rejected: false
    # 发布确认
    template:
      mandatory: true
    publisher-confirm-type: correlated
    publisher-returns: true
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;

/**
 * @author 配置
 */
@Configuration
public class RabbitMQConfig {
    /**
     * 主题交换机 01
     */
    @Bean("exchange01")
    public TopicExchange exchange01() {
        return ExchangeBuilder
                // Topic
                .topicExchange("exchange01")
                // 持久化
                .durable(true)
                .build();
    }

    /**
     * 死信交换机
     */
    @Bean("exchange_dead")
    public TopicExchange exchangeDead() {
        return ExchangeBuilder
                // Topic
                .topicExchange("exchange_dead")
                // 持久化
                .durable(true)
                .build();
    }

    /**
     * 延迟交换机
     */
    @Bean("exchange_delayed")
    public CustomExchange exchangeDelayed() {
        Map<String, Object> arguments = new HashMap<>(1);
        // Topic
        arguments.put("x-delayed-type", "topic");
        // String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments
        return new CustomExchange("exchange_delayed", "x-delayed-message", true, false, arguments);
    }

    /**
     * 普通队列 01
     */
    @Bean("queue01")
    public Queue queue01() {
        return QueueBuilder
                // 持久化
                .durable("queue01")
                .deadLetterExchange("exchange_dead")
                .deadLetterRoutingKey("dead")
                .maxLength(5)
                .build();
    }

    /**
     * 普通队列 02
     */
    @Bean("queue02")
    public Queue queue02() {
        return QueueBuilder
                // 持久化
                .durable("queue02")
                .deadLetterExchange("exchange_dead")
                .deadLetterRoutingKey("dead")
                .maxLength(5)
                .build();
    }

    /**
     * 死信队列
     */
    @Bean("queue_dead")
    public Queue queueDead() {
        return QueueBuilder
                // 持久化
                .durable("queue_dead")
                .build();
    }

    /**
     * 延迟队列
     */
    @Bean("queue_delayed")
    public Queue queueDelayed() {
        return QueueBuilder
                // 持久化
                .durable("queue_delayed")
                .deadLetterExchange("exchange_dead")
                .deadLetterRoutingKey("dead")
                .maxLength(5)
                .build();
    }

    @Bean
    public Binding bindingQueue01AndExchange01(@Qualifier("queue01") Queue queue, @Qualifier("exchange01") TopicExchange topicExchange) {
        return BindingBuilder.bind(queue).to(topicExchange).with("*.abc");
    }

    @Bean
    public Binding bindingQueue02AndExchange01(@Qualifier("queue02") Queue queue, @Qualifier("exchange01") TopicExchange topicExchange) {
        return BindingBuilder.bind(queue).to(topicExchange).with("abc.#");
    }

    @Bean
    public Binding bindingQueueDeadAndExchangeDead(@Qualifier("queue_dead") Queue queue, @Qualifier("exchange_dead") TopicExchange topicExchange) {
        return BindingBuilder.bind(queue).to(topicExchange).with("dead");
    }

    @Bean
    public Binding bindingQueueDelayedAndExchangeDelayed(@Qualifier("queue_delayed") Queue queue, @Qualifier("exchange_delayed") CustomExchange customExchange) {
        return BindingBuilder.bind(queue).to(customExchange).with("delayed").noargs();
    }

    /**
     * spring-boot-starter-amqp 默认 Message 的 durable 是 true
     * AMQP.BasicProperties props = MessageProperties.PERSISTENT_TEXT_PLAIN;
     * channel.basicPublish(exchange, routingKey, props, body);
     */
}
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

/**
 * @author 生产者
 */
@RestController
@RequestMapping("/producer")
public class Producer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    @Resource
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/send/{msg}")
    public void send(@PathVariable String msg) {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.convertAndSend("exchange01", "1.abc", msg.getBytes(StandardCharsets.UTF_8),
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        MessageProperties messageProperties = message.getMessageProperties();
                        // 设置过期
                        messageProperties.setExpiration("5000");
                        return message;
                    }
                }
        );
        rabbitTemplate.convertAndSend("exchange01", "abc.123", msg.getBytes(StandardCharsets.UTF_8),
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        MessageProperties messageProperties = message.getMessageProperties();
                        // 设置过期
                        messageProperties.setExpiration("5000");
                        return message;
                    }
                }
        );
        rabbitTemplate.convertAndSend("exchange_delayed", "delayed", msg.getBytes(StandardCharsets.UTF_8),
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        MessageProperties messageProperties = message.getMessageProperties();
                        // 设置延迟
                        messageProperties.setDelay(10000);
                        return message;
                    }
                }
        );
        rabbitTemplate.convertAndSend("exchange_delayed", "delayed", msg.getBytes(StandardCharsets.UTF_8),
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        MessageProperties messageProperties = message.getMessageProperties();
                        // 设置延迟
                        messageProperties.setDelay(5000);
                        return message;
                    }
                }
        );
        System.out.println(LocalDateTime.now() + " 发送消息: " + msg);
    }

    /**
     * 1、消息发布到交换机失败,后续自然无法发布到队列。[触发回调]
     * 2、消息发布到交换机成功,但是发布到队列失败。[触发回调]
     * 3、消息发布到交换机和队列均成功。[触发回调]
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        final String prefix = "[生产者] 发布到 [交换机] 的确认";
        System.out.println(prefix + " correlationData: " + correlationData);
        System.out.println(prefix + " ack: " + ack);
        System.out.println(prefix + " cause: " + cause);
        // TODO 将丢失的消息存储至 Redis,随后开启定时任务重新发布消息
    }

    /**
     * 1、消息发布到交换机失败,后续自然无法发布到队列。
     * 2、消息发布到交换机成功,但是发布到队列失败。[触发回调]
     * 3、消息发布到交换机和队列均成功。
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        final String prefix = "[交换机] 发布到 [队列] 的确认";
        System.out.println(prefix + " message: " + new String(message.getBody(), StandardCharsets.UTF_8));
        System.out.println(prefix + " replyCode: " + replyCode);
        System.out.println(prefix + " replyText: " + replyText);
        System.out.println(prefix + " exchange: " + exchange);
        System.out.println(prefix + " routingKey: " + routingKey);
        // TODO 将丢失的消息存储至 Redis,随后开启定时任务重新发布消息
    }
}
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

/**
 * @author 消费者
 */
@Component
public class Consumer {
    @RabbitListener(queues = "queue01")
    public void handleMessageFromQueue01(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        if ("abc".equals(msg)) {
            // 应答信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            System.out.println("接收 queue01 消息: " + msg);
        } else {
            // 拒绝信息,不再重新入队
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }

    @RabbitListener(queues = "queue02")
    public void handleMessageFromQueue02(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        if (!"abc".equals(msg)) {
            // 应答信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            System.out.println("接收 queue02 消息: " + msg);
        } else {
            // 拒绝信息,不再重新入队
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }

    @RabbitListener(queues = "queue_dead")
    public void handleMessageFromQueueDead(Message message, Channel channel) throws IOException {
        // 应答信息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        System.out.println("接收 queue_dead 消息: " + msg);
    }

    @RabbitListener(queues = "queue_delayed")
    public void handleMessageFromQueueDelayed(Message message, Channel channel) throws IOException {
        // 应答信息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        System.out.println(LocalDateTime.now() + " 接收 queue_delayed 消息: " + msg);
    }
}
posted @   linycat  阅读(7)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示