MessageQueue --- RabbitMQ可靠传输

MessageQueue --- RabbitMQ可靠传输

  • 可能会发生的问题
  • [确保消息Publish成功 --- 事务机制 (Transnational)](#确保消息Publish成功 --- 事务机制 (Transnational))
  • [确保消息Publish成功 --- 发布者确认 (Publisher Confirms)](#确保消息Publish成功 --- 发布者确认 (Publisher Confirms))
  • [确保消息不会在Broker丢失 --- 持久化 (Persistence)](#确保消息不会在Broker丢失 --- 持久化 (Persistence))
  • [确保消息被消费成功 --- Message Acknowledgment](#确保消息被消费成功 --- Message Acknowledgment)
  • [确保被丢弃的消息可以被处理 --- 死信 (Dead Letter)](#确保被丢弃的消息可以被处理 --- 死信 (Dead Letter))
  • 推荐配置

可能会发生的问题

发送消息时丢失 --- 解决方法:事务机制或者发布者确认

  • 消息到达 MQ 后未找到 Exchange
  • 消息到达 MQ 的 Exchange 后,未找到合适的 Queue
  • 生产者发送消息时因为网络问题消息丢失

在Broker丢失 --- 解决方法:持久化

  • 消息到达 MQ,保存到队列后,尚未消费就突然宕机。

消费者处理消费时丢失 --- 解决方法: 消费者确认

  • 因为网络原因,消息未送达
  • 消息接收后尚未处理突然宕机
  • 消息接收后处理过程中抛出异常

其他情况 --- 解决方法:死信

  • queue满了
  • 消息超时未处理

确保消息Publish成功 --- 事务机制 (Transnational)

  • 在AMQP协议中为保障消息能够正确的被推送到RabbitMQ服务器的队列中,它提供了一种事务机制,以确保没有推送成功的消息可以进行回滚。
  • 开启事务
    我们需要在Channel(信道)中开启事务,使Channel(信道)处于transactional模式,发送者使用该模式向队列中推送消息。
  • 提交事务
    提交当前的事务。
  • 事务回滚
    发送消息出现异常后,进行事务回滚
java 复制代码
 // 开启事务机制
 channel.txSelect();
 // 提交事务
 channel.txCommit();
 // 回滚事务
 channel.txRollback();

Example

java 复制代码
package rabbitmq.ced.confirm;
 
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
 
import java.io.IOException;
 
/**
 * 事务机制 创建生产者
 *
 */
public class TransactionProducer {
 
    public static void main(String[] args) {
        // 1.创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2. 设置连接属性
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
 
        Connection connection = null;
        Channel channel = null;
 
        try {
            // 3. 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 3. 在连接中创建信道,RabbitMQ中的所有操作都是在信道中完成的
            channel = connection.createChannel();
            // 声明队列,如果队列不存在会创建队列
            channel.queueDeclare("hello", false, false, true, null);
            // 准备要被发送的消息内容
            String message = "Hello";
 
            // 开启事务机制
            channel.txSelect();
 
            channel.basicPublish("", "hello", null, message.getBytes());
 
            // 提交事务
            channel.txCommit();
 
            System.out.println("消息发送成功");
        } catch (Exception e) {
            e.printStackTrace();
            try {
                if (null != channel) {
                    // 回滚事务
                    channel.txRollback();
                }
            } catch (IOException ioe) {
                ioe.printStackTrace();
                System.out.println("消息发送异常,回滚异常");
            }
            System.out.println("消息发送异常");
        } finally {
            // 释放关闭连接信道与连接
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
  • 因事务机制在每次发送消息的时候,都要在Channel中进行开启事务操作,所以会在与MQ交互时带来巨大的多余的开销,导致消息的吞吐量下降很多,据说会下降到250%。不推荐使用,所以在RabbitMQ中还提供了另一种解决方案,那就是发布确认模式(Confirm)
  • 注意发布者确认不能和事务同时开启

确保消息Publish成功 --- 发布者确认 (Publisher Confirms)

  • 发布者确认(Publisher Confirms)是 RabbitMQ 对 AMQP 0.9.1 协议的扩展,用于确保消息成功到达 Broker。它通过返回NACK或者ACK的方式,让生产者知道消息是否被 Broker 正确接收
  • 于路由到持久化队列(durable queues)的持久化消息(persistent messages),这意味着消息会被写入磁盘以后才会被视为成功投递。
  • 对于quorum queues,则意味着消息需被多数副本(quorum replicas)接受并由选举出的领导者(leader)确认后,才会被视为成功投递。

如何启用

  • 默认是不启用的,需要在 Channel 级别启用,每个 Channel 只需调用一次 confirmSelect:
java 复制代码
Channel channel = connection.createChannel();
channel.confirmSelect(); // 启用发布者确认

三种确认策略

  • 策略 1:单条消息同步确认(低吞吐,简单)
  • 特点:每条消息发送后同步等待确认。
  • 缺点:吞吐量低(每秒仅几百条),适合低并发场景。
java 复制代码
while (hasMessages()) {
    channel.basicPublish(exchange, queue, properties, body);
    channel.waitForConfirmsOrDie(5_000); // 阻塞等待确认,超时或失败抛异常
}
  • 策略 2:批量消息同步确认(中等吞吐,需使用内存)
  • 特点:批量发送后统一确认,吞吐量提升 20-30 倍。
  • 缺点:失败时需整批重发,所以需要存在内存备份, 内存占用较高。
java 复制代码
int batchSize = 100;
int outstandingMessageCount = 0;
while (thereAreMessagesToPublish()) {
    byte[] body = ...;
    BasicProperties properties = ...;
    channel.basicPublish(exchange, queue, properties, body);
    outstandingMessageCount++;
    if (outstandingMessageCount == batchSize) {
        channel.waitForConfirmsOrDie(5_000); //阻塞等待确认,失败抛出异常
        outstandingMessageCount = 0;
    }
}
if (outstandingMessageCount > 0) {
    channel.waitForConfirmsOrDie(5_000);
}
  • 策略 3:异步确认(高吞吐,复杂但高效)
java 复制代码
Channel channel = connection.createChannel();
channel.confirmSelect();
//mutiple是bool,
//若为 false,表示仅当前一条消息被确认(ack)或拒绝(nack);
//若为 true,则表示所有序列号 ≤ 当前值的消息均被批量确认或拒绝。
channel.addConfirmListener((sequenceNumber, multiple) -> {
    // code when message is confirmed
}, (sequenceNumber, multiple) -> {
    // code when message is nack-ed
});
java 复制代码
// code when message is confirmed
channel.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
    String body = outstandingConfirms.get(sequenceNumber);
    System.err.format(
      "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
      body, sequenceNumber, multiple
    );
    cleanOutstandingConfirms.handle(sequenceNumber, multiple);
});
java 复制代码
// code when message is nack-ed
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
    if (multiple) {
        ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
          sequenceNumber, true
        );
        confirmed.clear();
    } else {
        outstandingConfirms.remove(sequenceNumber);
    }
};

性能对比

消息到达 MQ 后未找到 Exchange

  • 向不存在的交换机(Exchange)发布消息会导致通道(Channel)错误,抛出异常,该错误会直接关闭通道,使其无法再进行任何消息发布或其他操作

消息到达 MQ 的 Exchange 后,未找到合适的 Queue

  • 当已发布的消息无法被路由到任何队列(例如目标交换机未定义任何绑定规则),且发布者将消息的 mandatory 属性设为 false(默认值)时:
  • 消息会被直接丢弃;
  • 如果配置了备用交换机(Alternate Exchange),则消息会转发至备用交换机。
  • 如果开启了发布者确认,生产者仍会收到 ack 确认(因为消息已成功到达 Exchange,符合确认机制的定义), 生产者误以为消息已成功处理,需额外逻辑确保业务一致性
  • 当消息无法路由到任何队列,且发布者将 mandatory 属性设为 true 时:
  • 消息将被退回给发布者;
  • 发布者必须预先设置消息退回处理器(Returned Message Handler),以处理退回的消息(例如记录错误日志、换交换机重发等)
  • 如果开启了发布者确认,生产者通过 ReturnListener 收到退回消息后,仍会收到 nack(否定确认)(部分客户端实现)或 无确认(需超时检测)。

确保消息不会在Broker丢失 --- 持久化 (Persistence)

交换机持久化

  • 交换机的持久化是我们在声明交换机的时候,将 durable 的参数设置为 true 实现的。也就是将交换机的内部属性在服务器的内部保存,当 MQ 服务器发生重启之后,不需要去重新建立交换机,交换机会根据服务器中保存的交换机的属性来自动创建。
  • 如果交换机不设置持久化,那么当 RabbitMQ 服务重启之后,交换机的元数据就会丢失,那么要想再使用这个交换机就只能重新创建这个交换机。
java 复制代码
channel.exchangeDeclare("my_exchange", "direct", true);

队列持久化

  • 队列的持久化也是我们在声明队列的时候设置 durable 的参数来实现的。如果队列不设置持久化,那么当我们的 RabbitMQ Server 重启的时候,这些未设置持久化的队列就会丢失,那么队列中的消息也就会丢失(不管队列里面的消失是否设置了持久化)。
  • 队列的持久化能保证队列的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失,要确保消息不会丢失,还需要设置消息为持久化。
java 复制代码
channel.queueDeclare("my_queue", true, false, false, null);

消息持久化

  • 实现消息持久化,需要把消息的投递模式(MessageProperties 中的 deliveryMode)设置为2
java 复制代码
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
    .deliveryMode(2) // 1=非持久化,2=持久化
    .build();
channel.basicPublish("exchange", "routingKey", props, message.getBytes());

只有队列和消息都设置为持久化才能真正实现消息的持久化

  • 如果队列不持久化,消息持久化,那么当 RabbitMQ 服务重启的时候,队列就会消失,更不用说里面的消息了

问题

  • 在持久化的消息正确存入 RabbitMQ 之后,还需要一段时间(虽然很短,但是也不能忽视)才能存入磁盘中,RabbitMQ 并不会为每条消息都进行同步存盘(调用内核的 fsync 方法)的处理,如果在这个时间段内 RabbitMQ 发生了宕机、重启等异常,那么消息还没来得及落盘,那么这些消息就会丢失, 解决方法:
  • 引入RabbitMQ的仲裁队列,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave),这样有效地保证了高可用性。除非整个集群都挂掉(此方法也不能保证100%可靠,但是配置了仲裁队列要比没有配置仲裁队列的可靠性要高很多。实际生产环境中的关键业务队列一般都会设置仲裁队列)。
  • 还可以在发送端引入事务机制或者发送方确认机制来确保消息已经正确地发送并存储至RabbitMQ中

确保消息被消费成功 --- Message Acknowledgment

  • RabbitMQ 的消费者确认机制(Consumer Acknowledgement)是确保消息可靠消费的核心机制,通过显式确认(ACK)或拒绝(NACK)来控制消息的移除或重投递。以下是完整的工作流程和配置策略:

确认模式类型

  • 自动确认 autoAck=true 消息一旦发送给消费者 (written to a TCP socket),立即从队列删除(无论消费是否成功,高风险, 所以实际上是不安全的)。同时增加消费者压力, 谨慎使用
  • 手动确认 autoAck=false + 显式调用 需消费者显式调用 basicAck 或 basicNack,Broker 才移除消息或重投递(高可靠)。

ACK

java 复制代码
// this example assumes an existing channel instance

boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // positively acknowledge all deliveries up to
             // this delivery tag
             channel.basicAck(deliveryTag, true);
         }
     });

NACK

  1. 单条消息拒绝(basic.reject)
java 复制代码
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                 Envelope envelope,
                                 AMQP.BasicProperties properties,
                                 byte[] body) throws IOException {
             long deliveryTag = envelope.getDeliveryTag();
             // 情况1:拒绝并丢弃消息(requeue=false)
             // 如果配置了Dead Letter,则会被放进DL,反之则丢弃
             channel.basicReject(deliveryTag, false);
             
             // 情况2:拒绝并重新入队(requeue=true)
             channel.basicReject(deliveryTag, true);
         }
     });
  1. 批量消息拒绝(basic.nack)
java 复制代码
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                 Envelope envelope,
                                 AMQP.BasicProperties properties,
                                 byte[] body) throws IOException {
             long deliveryTag = envelope.getDeliveryTag();
             // 批量拒绝所有未确认消息(multiple=true)并重新入队(requeue=true)
             channel.basicNack(deliveryTag, true, true);
             // 批量拒绝所有未确认消息(multiple=true)并丢弃
             // 如果配置了Dead Letter,则会被放进DL,反之则丢弃
             channel.basicNack(deliveryTag, true, false);
         }
     });

Notes

  • 重新入队位置
    消息会尽可能回到原始位置,若因并发操作失败则插入队列头部附近。
  • 若消费者持续因临时故障拒绝消息,会导致消息反复入队
  • 建议采用requeue = false,并配置死信队列
  • Automatic acknowledgement mode or manual acknowledgement mode with unlimited prefetch should be used with care. 通常prefetch设为 100~300,平衡吞吐与内存占用. prefetch详解

确保被丢弃的消息可以被处理 --- 死信 (Dead Letter)

  • 死信(Dead Letter)是指在消息队列中无法被正常消费和处理的消息。当消息满足一定的条件时,它们可以被标记为死信并被发送到专门的死信队列中,以便进一步处理或分析
  • 死信来源
  • 消息 TTL 过期, RabbitMQ 消息的 TTL 仅在 Ready 状态时计时,Unacked 状态暂停计时。过期消息,得不断扫描整个队列,代价太大,所以等到消息即将被推送给消费者时在判断是否过期,如果过期就删除,是一种惰性处理策略
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false(不再重新入队)
  • 死信队列(Dead Letter Queue)是一个特殊的队列,用于接收死信消息。一旦消息被发送到死信队列,就可以根据需要进行进一步的处理,例如重新投递、持久化、记录日志或者进行分析。
  • 使用死信机制的好处包括:
  • 错误处理:当消息无法被正常处理时,可以将其发送到死信队列,以便进一步处理错误情况,例如记录日志或者通知管理员。
  • 重试机制:如果消息在一定时间内未能被消费成功,可以将其发送到死信队列,并设置重试策略,例如延时重试或者指数退避重试。
  • 延迟消息:通过结合延迟队列和死信队列,可以实现延迟消息的功能。当消息的延迟时间到达时,将其发送到死信队列,然后再从死信队列中重新投递到目标队列,实现延迟消息的效果。
java 复制代码
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class DeadLetterExample {
    private static final String EXCHANGE_NAME = "normal_exchange";
    private static final String QUEUE_NAME = "normal_queue";
    private static final String DLX_EXCHANGE_NAME = "dlx_exchange";
    private static final String DLX_QUEUE_NAME = "dlx_queue";
    private static final String DLX_ROUTING_KEY = "dlx_routing_key";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            // 创建普通交换机和队列
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

            // 创建死信交换机和队列
            channel.exchangeDeclare(DLX_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            channel.queueDeclare(DLX_QUEUE_NAME, false, false, false, null);
            channel.queueBind(DLX_QUEUE_NAME, DLX_EXCHANGE_NAME, DLX_ROUTING_KEY);

            // 设置普通队列的死信参数
            Map<String, Object> arguments = new HashMap<>();
            arguments.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
            arguments.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
            channel.queueDeclare(QUEUE_NAME, false, false, false, arguments);

            // 定义消息处理函数
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println("Received message: " + message);
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };

            // 消费消息
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {});

            System.out.println("Press any key to exit.");
            System.in.read();
        }
    }
}

推荐配置

对于关键业务队列(如订单支付、交易流水),需通过以下配置组合确保消息不丢失、不重复,并平衡吞吐量与可靠性:

相关推荐
郭涤生2 小时前
Chapter 10: Batch Processing_《Designing Data-Intensive Application》
笔记·分布式
风铃儿~4 小时前
RabbitMQ
java·微服务·rabbitmq
郭涤生4 小时前
微服务系统记录
笔记·分布式·微服务·架构
西岭千秋雪_7 小时前
Sentinel核心源码分析(上)
spring boot·分布式·后端·spring cloud·微服务·sentinel
dengjiayue9 小时前
消息队列(kafka 与 rocketMQ)
分布式·kafka·rocketmq
东阳马生架构11 小时前
zk基础—4.zk实现分布式功能二
分布式
ChinaRainbowSea11 小时前
8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
IT成长日记12 小时前
【Kafka基础】Kafka高可用集群:2.8以下版本超详细部署指南,运维必看!
分布式·zookeeper·kafka·集群部署
码界筑梦坊12 小时前
基于Spark的酒店数据分析系统
大数据·分布式·python·信息可视化·spark·毕业设计·个性化推荐