二、RabbitMQ 核心

一、RabbitMQ 介绍
二、RabbitMQ 核心
三、RabbitMQ 扩展
四、RabbitMQ 集群


RabbitMQ 核心

在这里插入图片描述


一、Hello World

  • 发送单个消息的生产者 和 接收消息并打印出来的消费者。
    在这里插入图片描述
  • 在图中,P 是我们的生产者,C 是我们的消费者。
  • 中间是一个队列,RabbitMQ 代表保留的消息缓冲区。

1. 消费者

/**
 * @author: wy
 * describe: 消费者
 */
public class Consumer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.137.155");
        factory.setUsername("guest");
        factory.setPassword("guest");
        try {
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            /**
             * 消费者消费消息
             * 1. 消费那个队列
             * 2. 消费成功之后,是否自动应答(false手动应答)
             * 3. 消费者成功消费的回调
             * 4. 消费者取消消费的回调
             */
            channel.basicConsume(QUEUE_NAME, true,
                    // 接收的消息
                    (consumerTag, message) -> {
                        System.out.println(message);
                        System.out.printf("接收到消息: %s\r\n", new String(message.getBody()));
                    },
                    // 取消的消息
                    consumerTag -> {
                        System.out.printf("中断消费消息: %s\r\n", consumerTag);
                    });
            System.out.println("等待接收消费...");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

2. 生产者

/**
 * @author: wy
 * describe: 生产者
 * 1. Hello World
 */
public class Producer {

    // 队列名称
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) {
        // 创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂IP,连接RabbitMQ的队列
        factory.setHost("192.168.137.155");
        // 用户名
        factory.setUsername("guest");
        // 密码
        factory.setPassword("guest");
        try {
            // 创建连接
            Connection connection = factory.newConnection();
            // 获取信道
            Channel channel = connection.createChannel();
            /**
             * 声明一个队列
             * 1. 队列名称
             * 2. 队列里面的消息是否持久化(默认消息存储在内存中)
             * 3. 该队列是否进行共享,否则只供一个消费者进行消费(true可以多个消费者消费)
             * 4. 最后一个消费者断开连接以后,该队列是否自动删除(true自动删除)
             * 5. 其他参数(延迟消息、死信队列)
             */
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            // 消息
            String message = "Hello World!";
            /**
             * 生产者发送一个消息
             * 1. 发送到那个交换机(""代表默认交换机)
             * 2. 路由的key是那个(队列名称)
             * 3. 其他的参数信息
             * 4. 发送的消息体
             */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("消息发送完毕");
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

二、工作队列(Work Queues)绑定同一个队列

  • 工作队列(又称任务队列)的主要思想是:避免立即执行资源密集型任务,而不得不等待它完成。
  • 相反我们安排任务在之后执行。
  • 把任务封装为消息并将其发送到队列。
  • 在后台运行的工作线程,将弹出的任务最终执行消费。
  • 当有多个工作线程时,这些工作线程将一起处理这些任务。

1. 消费者

消费者一 和 消费者二。

/**
 * @author: wy
 * describe: 消费者
 * 1. 轮询分发消息
 */
public class Consumer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtil.getChannel();
        // 消费者消费消息
        channel.basicConsume(QUEUE_NAME, true,
                // 接收的消息
                (consumerTag, message) -> {
                    System.out.printf("接收到消息: %s\r\n", new String(message.getBody()));
                },
                // 取消的消息
                consumerTag -> {
                    System.out.printf("中断消费消息: %s\r\n", consumerTag);
                });
        System.out.println("Consumer, 2,等待接收消息...");
    }
}

2. 生产者

/**
 * @author: wy
 * describe: 生产者
 * 1. 轮询分发消息
 */
public class Producer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 获取信道
        Channel channel = RabbitMQUtil.getChannel();
        // 声明一个队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 从控制台接收消息
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入消息: ");
        while (scanner.hasNext()) {
            String message = scanner.next();
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.printf("发送消息: %s\r\n", message);
        }
    }
}

3. 启动两个消费者,默认轮训分发(prefetchCount = 0

  • 在这个案例中,我们会启动 两个消息消费线程 和 一个消息发送线程,看看两个工作线程是如何工作的。
    在这里插入图片描述

4. 不公平分发(prefetchCount = 1

  • 在最开始的时候我们知道 RabbitMQ 分发消息采用的 轮训分发,但是在某种场景下这种策略并不是很好。
  • 比方说有两个消费者在处理任务,其中有个 消费者一 处理任务的速度非常快,而另外一个 消费者二 处理任务速度却很慢,这个时候我们还是采用 轮训分发 的话,就会导致处理速度快的这个消费者很大一部分时间处于空闲状态,而处理速度慢的那个消费者却一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。
/*
4. 不公平分发:
prefetchCount = 0 轮询(默认)
prefetchCount = 1 不公平分发,能者多劳
prefetchCount = 2 预取值
 */
int prefetchCount = 1;
// int prefetchCount = 2;
channel.basicQos(prefetchCount);

在这里插入图片描述
在这里插入图片描述

  • 意思就是如果这个任务我还没有处理完,或者我还没有应答你,你先别分配给我新任务,我目前只能处理一个任务,然后 RabbitMQ 就会把新任务分配给其他空闲的消费者。
  • 当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 Worker 消费者 或者改变其他存储任务的策略。

6. 预取值(prefetchCount = 预取值

  • 本身消息就是异步发送的,所以在任何时候 Channel 上肯定不止只有一个消息,另外来自消费者的手动确认本质上也是异步的。
  • 因此这里就存在一个 未确认的消息缓冲区,因此希望开发人员能 限制此缓冲区的大小(预取值就是缓冲区大小),以避免缓冲区无限制添加未确认消息的问题
  • 这个时候就可以通过使用 basicQos 方法设置 预取值 来完成的。
  • 该值定义通道上允许的未确认消息的最大数量
  • 一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非有未处理的消息被确认。

  • 例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的 预取值 为 4,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ACK。
  • 比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知这个情况到并再发送一条消息。

  • 消息应答 和 Qos 预取值 对用户吞吐量有重大影响。
  • 通常增加预取值将提高向消费者传递消息的速度。
  • 虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器),应该小心使用具有 无限预处理的自动确认模式 或 手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载预取值的取值也不同,100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。
  • 预取值为 1 是最保守的。
  • 当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,在消费者连接等待时间较长的环境中。
  • 对于大多数应用来说,稍微高一点的值将是最佳的。
    在这里插入图片描述

三、发布订阅(Fanout 交换机)

  • 在上一节中,我们创建了一个工作队列,假设的是工作队列背后,每个任务都恰好交付给一个消费者(工作进程)。
  • 在这一部分中,我们将做一些完全不同的事情,将消息传达给多个消费者。这种模式称为 发布订阅(广播)。

  • 为了说明这种模式,我们将构建一个简单的日志系统。
  • 它将由两个程序组成:第一个程序是生产日志消息第二个程序是消费日志消息

  1. 其中我们会启动两个消费者,其中一个消费者接收到消息后把日志存储在磁盘。
  2. 另外一个消费者接收到消息后把日志打印在控制台上。
  • 事实上第一个程序发出的消息将 广播 给所有消费者。

四、路由(Direct 交换机)


五、主题(Topic 交换机)


六、发布确认


1. 发布确认原理

  • 生产者将信道设置成 Confirm 模式,一旦信道进入 Confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始)。
  • 一旦消息被投递到所有匹配的队列之后,Broker 就会发送一个确认给生产者(包含消息的唯一 ID)。
  • 这就使得生产者知道消息已经正确到达目的队列了。
  • 如果 消息 和 队列 是可持久化的,那么确认消息会在将消息写入磁盘之后发出,Broker 回传给生产者的确认消息中 deliveryTag 域包含了确认消息的序列号。
  • 此外 Broker 也可以设置 basicAck 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。

  • Confirm 模式 最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时,继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息。
  • 如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

2. 开启发布确认的方法

  • 发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect
  • 每当你要想使用发布确认,都需要在 Channel 上调用该方法。
// 开启发布确认
channel.confirmSelect();

3. 发布确认策略


3.1 单个确认发布
  • 这是一种简单的确认方式,它是一种 同步确认发布 的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布。
  • waitForConfirmsOrDie(long) 这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认,那么它将抛出异常。

这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

/**
 * 1. 单个确认发布(同步确认发布)
 */
private static void publishMessageIndividually() throws IOException, TimeoutException, InterruptedException {
    Channel channel = RabbitMQUtil.getChannel();
    // 队列声明
    String queueName = UUID.randomUUID().toString();
    channel.queueDeclare(queueName, true, false, false, null);
    // 开启发布确认
    channel.confirmSelect();

    // 开始时间
    long begin = System.currentTimeMillis();
    for (int i = 0; i < MESSAGE_COUNT; i++) {
        String message = String.format("单个确认发布: %d", i);
        channel.basicPublish("", queueName, null, message.getBytes("UTF-8"));
        // 单个消息确认
        boolean flag = channel.waitForConfirms();
        if (flag) {
            System.out.printf("%s, 确认\r\n", message);
        }
    }
    System.out.printf("%d个消息,单个确认发布,耗时: %s毫秒", MESSAGE_COUNT, System.currentTimeMillis() - begin).println();
}

3.2 批量确认发布
  • 上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量。
  • 当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。
  • 当然这种方案仍然是同步的,也一样阻塞消息的发布。
/**
 * 2. 批量确认发布
 */
private static void publishMessageBatch() throws IOException, TimeoutException, InterruptedException {
    Channel channel = RabbitMQUtil.getChannel();
    // 队列声明
    String queueName = UUID.randomUUID().toString();
    channel.queueDeclare(queueName, true, false, false, null);
    // 开启发布确认
    channel.confirmSelect();

    // 批量确认消息数量
    int batchSize = 100;
    // 开始时间
    long begin = System.currentTimeMillis();
    for (int i = 0; i < MESSAGE_COUNT; i++) {
        String message = String.format("批量确认发布: %d", i);
        channel.basicPublish("", queueName, null, message.getBytes("UTF-8"));
        // 判断发送消息达到100条,批量确认一次
        if (i % batchSize == 0) {
            channel.waitForConfirms();
            System.out.printf("%s, 确认\r\n", message);
        }
    }
    System.out.printf("%d个消息,批量确认发布,耗时: %s毫秒", MESSAGE_COUNT, System.currentTimeMillis() - begin).println();
}

3.3 异步确认发布
  • 异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说。
  • 他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否发送成功。
    在这里插入图片描述
/**
 * 3. 异步确认发布
 */
public static void publishMessageAsync() throws IOException, TimeoutException, InterruptedException {
    Channel channel = RabbitMQUtil.getChannel();
    // 队列声明
    String queueName = UUID.randomUUID().toString();
    channel.queueDeclare(queueName, true, false, false, null);
    // 开启发布确认
    channel.confirmSelect();

    /*
    线程安全有序的一个哈希表,适用于高并发
    1. 将序号与消息关联
    2. 批量删除条目,只要给序号
    3. 支持高并发(多线程)
     */
    ConcurrentSkipListMap<Long, String> map = new ConcurrentSkipListMap<>();
    // 消息确认监听器(异步通知)
    channel.addConfirmListener(
            // 监听那些消息成功
            (deliveryTag, multiple) -> {
                System.out.printf("成功的消息: %s", deliveryTag).println();
                // 2. 删除已确认的消息
                if (multiple) {
                    // 批量清除
                    ConcurrentNavigableMap<Long, String> concurrentNavigableMap = map.headMap(deliveryTag);
                    concurrentNavigableMap.clear();
                } else {
                    map.remove(deliveryTag);
                }
            },
            // 监听那些消息失败
            (deliveryTag, multiple) -> {
                System.out.printf("失败的消息: %s", deliveryTag).println();
                System.out.println(map.get(deliveryTag));
            });
    // 开始时间
    long begin = System.currentTimeMillis();
    for (int i = 0; i < MESSAGE_COUNT; i++) {
        String message = String.format("异步确认发布: %d", i);
        channel.basicPublish("", queueName, null, message.getBytes("UTF-8"));
        // 1. 记录所有发送的消息
        map.put(channel.getNextPublishSeqNo(), message);
    }
    System.out.printf("%d个消息,异步确认发布,耗时: %s毫秒", MESSAGE_COUNT, System.currentTimeMillis() - begin).println();
}

3.3.1 处理异步未确认的消息
  • 最好的解决的解决方案,就是把未确认的消息放到一个基于内存的能被发布线程访问的队列。
  • 比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

3.4 三种发布确认对比
发布确认优缺点
单独发布消息同步等待确认,简单,但吞吐量非常有限
批量发布消息批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题
异步处理最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些

七、发布确认高级

  • 在生产环境中由于一些不明原因,导致 RabbitMQ 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。
  • 于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?
  • 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?

1. 交换机确认机制方案

在这里插入图片描述


1.1 架构图

在这里插入图片描述


1.2 application.properties
# 发布消息成功到交换机后会触发回调方法
spring.rabbitmq.publisher-confirm-type=correlated
参数解释
NONE禁用发布确认模式(默认值)
CORRELATED发布消息成功到交换器后会触发回调方法
SIMPLE经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法,等待 Broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 Channel,则接下来无法发送消息到 Broker

1.3 Config
/**
 * @author: wy
 * describe: 发布确认高级
 */
@Configuration
public class ConfirmBackupConfig {

    // 确认交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    // 确认队列
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    // 确认RoutingKey
    public static final String CONFIRM_ROUTING_KEY = "key1";

    /**
     * 直接交换机
     */
    @Bean
    public DirectExchange confirmExchange() {
//        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
        // 但指定了备份交换机就不返回了(备份交换机优先级高)
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                // 是否持久化
                .durable(true)
                // 指定备份交换机
                .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME).build();
    }

    @Bean
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    @Bean
    public Binding confirmQueueBindingConfirmExchange(@Qualifier("confirmQueue") Queue confirmQueue,
                                        @Qualifier("confirmExchange") DirectExchange confirmExchange) {
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
    }
}

1.4 消费者
/**
 * @author: wy
 * describe: 确认消费者
 */
@Slf4j
@Component
public class ConfirmConsumer {

    @RabbitListener(queues = ConfirmBackupConfig.CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message) {
        log.info("确认消费者接收到消息: {}", new String(message.getBody()));
    }
}

1.5 回调接口
/**
 * @author: wy
 * describe: 确认回调
 * 1. 交换机确认回调接口RabbitTemplate.ConfirmCallback
 *  1.1 需要配置`spring.rabbitmq.publisher-confirm-type=correlated`发布消息成功到交换机后会触发回调方法
 */
@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 1. 实例化MyCallBack
     * 2. 注入RabbitTemplate
     * 3. 调用init方法
     */
    @PostConstruct
    public void init() {
        // 将当前回调类注入RabbitTemplate
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1. 交换机成功接收到消息
     *  1.1 correlationData: 保存回调消息的Id及相关信息
     *  1.2 ack: true代表交换机收到消息
     *  1.3 cause: 失败原因(成功为null)
     * 2. 交换机没有接收到消息
     *  2.1 correlationData: 保存回调消息的Id及相关信息
     *  2.2 ack: false代表交换机收到消息
     *  2.3 cause: 失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已收到Id为: {}的消息", id);
        } else {
            log.error("交换机未收到Id为: {}的消息,原因: {}", id, cause);
        }
    }
}

1.6 生产者
/**
 * @author: wy
 * describe: 发布确认高级
 */
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProduceController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 3. 发布确认高级
     * http://127.0.0.1:8080/confirm/sendMsg/%E4%BD%A0%E5%A5%BD%E5%91%80
     */
    @GetMapping("/sendMsg/{msg}")
    public String sendMsg(@PathVariable String msg) {
        log.info("发送消息: {}", msg);
        CorrelationData correlationData = new CorrelationData("1");
        // 1. 成功确认消息
//        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME, ConfirmConfig.CONFIRM_ROUTING_KEY, msg, correlationData);
        // 2. 未知的交换机
//        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME + "123", ConfirmConfig.CONFIRM_ROUTING_KEY, msg, correlationData);
        // 3. 未知的routingKey
        rabbitTemplate.convertAndSend(ConfirmBackupConfig.CONFIRM_EXCHANGE_NAME, ConfirmBackupConfig.CONFIRM_ROUTING_KEY + "123", msg, correlationData);
        return "success";
    }
}
  • 可以看到,发送了两条消息,第一条消息的 RoutingKey 为 key1,第二条消息的 RoutingKey 为 key1 + 123,两条消息都成功被交换机接收,也收到了交换机的确认回调。
  • 但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。

2. 回退消息


2.1 Mandatory 参数
  • 在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息。
  • 如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。
  • 那么如何让无法被路由的消息,帮我想办法处理一下?
  • 最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中,不可达目的地时将消息返回给生产者。

2.2 回调接口
/**
 * @author: wy
 * describe: 确认回调
 * 1. 交换机确认回调接口RabbitTemplate.ConfirmCallback
 *  1.1 需要配置`spring.rabbitmq.publisher-confirm-type=correlated`发布消息成功到交换机后会触发回调方法
 * 2. 消息不可达回退接口RabbitTemplate.ReturnCallback
 *  2.1 需要配置`spring.rabbitmq.publisher-returns=true`当消息传递过程中"不可达目的地"时将消息"返回给生产者",成功不返回
 */
@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 1. 实例化MyCallBack
     * 2. 注入RabbitTemplate
     * 3. 调用init方法
     */
    @PostConstruct
    public void init() {
        // 将当前回调类注入RabbitTemplate
        rabbitTemplate.setConfirmCallback(this);
        /**
         * true: 交换机无法将消息进行路由时,会将该消息返回给生产者
         * false: 如果发现消息无法进行路由,则直接丢弃
         */
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1. 交换机成功接收到消息
     *  1.1 correlationData: 保存回调消息的Id及相关信息
     *  1.2 ack: true代表交换机收到消息
     *  1.3 cause: 失败原因(成功为null)
     * 2. 交换机没有接收到消息
     *  2.1 correlationData: 保存回调消息的Id及相关信息
     *  2.2 ack: false代表交换机收到消息
     *  2.3 cause: 失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已收到Id为: {}的消息", id);
        } else {
            log.error("交换机未收到Id为: {}的消息,原因: {}", id, cause);
        }
    }

    /**
     * 当消息传递过程中"不可达目的地"时将消息"返回给生产者",成功不返回
     * 但指定了备份交换机就不返回了(备份交换机优先级高)
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息: {}, 被{}交换机退回, 退回原因: {}, RoutingKey: {}",
                new String(message.getBody()), exchange, replyText, routingKey);
    }
}

3. 备份交换机

  • 有了 Mandatory 参数回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。
  • 但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。
  • 而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。
  • 而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。
  • 如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?

  • 前面在设置死信队列的文章中,我们提到,可以为队列设置 死信队列 来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
  • 在 RabbitMQ 中,有一种 备份交换机 的机制存在,可以很好的应对这个问题。什么是备份交换机呢?

  • 备份交换机可以理解为 RabbitMQ 中 交换机的备胎,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理。
  • 通常 备份交换机 的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。
  • 当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

3.1 架构图

在这里插入图片描述

/**
 * @author: wy
 * describe: 发布确认高级
 */
@Configuration
public class ConfirmBackupConfig {

    // 确认交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    // 确认队列
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    // 确认RoutingKey
    public static final String CONFIRM_ROUTING_KEY = "key1";

    /**
     * 直接交换机
     */
    @Bean
    public DirectExchange confirmExchange() {
//        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
        // 但指定了备份交换机就不返回了(备份交换机优先级高)
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                // 是否持久化
                .durable(true)
                // 指定备份交换机
                .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME).build();
    }

    @Bean
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    @Bean
    public Binding confirmQueueBindingConfirmExchange(@Qualifier("confirmQueue") Queue confirmQueue,
                                        @Qualifier("confirmExchange") DirectExchange confirmExchange) {
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
    }

    // 备份交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    // 备份队列
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    // 报警队列
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    /**
     * 扇出交换机
     */
    @Bean
    public FanoutExchange backupExchange() {
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    @Bean
    public Queue backupQueue() {
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    @Bean
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    @Bean
    public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,
                                                      @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(backupQueue).to(backupExchange);
    }

    @Bean
    public Binding warningQueueBindingBackupExchange(@Qualifier("warningQueue") Queue warningQueue,
                                                      @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(warningQueue).to(backupExchange);
    }
}

3.2 报警消费者
/**
 * @author: wy
 * describe: 报警消费者
 */
@Slf4j
@Component
public class WarningConsumer {

    @RabbitListener(queues = ConfirmBackupConfig.WARNING_QUEUE_NAME)
    public void receiveMsg(Message message) {
        log.warn("报警消费者接收到消息: {}", new String(message.getBody()));
    }
}

4. 测试注意事项

  • 问题:Mandatory参数 与 备份交换机 可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高?
  • 结果:经过上面结果显示答案是 备份交换机优先级高

八、其他


1. 消息应答

  • 消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个很长的任务,并且只完成了部分任务突然就挂掉了,会发生什么情况。
  • RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。
  • 在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。
  • 以及后续再发送给该消费者的消息,它都无法接收到。

  • 为了保证消息在发送过程中不丢失,RabbitMQ 引入了 消息应答机制
  • 消息应答机制就是:消费者在接收到消息,并且处理该消息之后,告诉 RabbitMQ 它已经处理,RabbitMQ 可以把该消息删除了

1.1 自动应答
  • 消息发送后立即被认为已经传送成功,这种模式需要在 高吞吐量 和 数据传输安全性方面 做权衡,因为这种模式如果消息在接收到之前,消费者那边出现 连接 或 Channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死。
  • 所以这种模式仅适用在消费者可以高效,并以某种速率能够处理这些消息的情况下使用

1.2 手动应答
  • 默认消息采用的是 自动应答,所以我们要想实现消息消费过程中不丢失。
  • 需要把 自动应答 改为 手动应答,修改消费者代码。

1.2.1 手动应答的方法
// 1. false手动应答
boolean autoAck = false;

// 用于肯定确认(告诉 RabbitMQ 该消息成功的被处理,可以将其丢弃了)
void basicAck(long deliveryTag, boolean multiple);
// 用于否定确认
void basicNack(long deliveryTag, boolean multiple, boolean requeue);
// 用于拒绝消息(不处理该消息了直接拒绝,可以将其丢弃了)
void basicReject(long deliveryTag, boolean requeue);
  • 手动应答的好处是:可以批量应答并且减少网络拥堵。
  • multiple 的 true 和 false 代表不同意思。
  1. true 代表批量应答 Channel 上未应答的消息。
    比如说 Channel 上有传送 tag 的消息 5, 6, 7, 8 当前 tag 是 8,那么此时 5-8 的这些还未应答的消息都会被批量确认收到消息应答。
  2. false 同上面相比。
    只会应答 tag=8 的消息,5, 6, 7 这三个消息依然不会被确认收到消息应答;
    在这里插入图片描述

1.2.2 消息自动重新入队
  • 如果消费者由于某些原因失去连接(通道已关闭、连接已关闭 或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。
  • 如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。
  • 这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
    在这里插入图片描述

1.2.3 消费者
  • 默认消息采用的是 自动应答
  • 所以我们要想实现消息消费过程中不丢失,需要把自动应答改为 手动应答
// 1. false手动应答
boolean autoAck = false;

/**
 * 消息应答
 * 1. 消息标记
 * 2. 是否批量应答(false代表只应答接收到的那个消息,true代表应答信道中的所有消息包括接收到的消息)
 */
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
/**
 * @author: wy
 * describe: 消费者
 * 1. 手动应答
 * 4. 不公平分发
 */
public class Consumer1 {

    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("Consumer1,睡1秒");
        // 获取信道
        Channel channel = RabbitMQUtil.getChannel();
        /*
        4. 不公平分发:
        prefetchCount = 0 轮询(默认)
        prefetchCount = 1 不公平分发,能者多劳
        prefetchCount = 2 预取值
         */
//        int prefetchCount = 1;
        int prefetchCount = 2;
        channel.basicQos(prefetchCount);
        // 1. false手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck,
                // 接收消息
                (consumerTag, message) -> {
                    try {
                        // 睡1秒
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("接收到消息: %s\r\n", new String(message.getBody(), "UTF-8"));
                    /**
                     * 消息应答
                     * 1. 消息标记
                     * 2. 是否批量应答(false代表只应答接收到的那个消息,true代表应答信道中的所有消息包括接收到的消息)
                     */
                    channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
                },
                // 取消消息
                consumerTag -> {
                    System.out.printf("中断消费消息: %s\r\n", consumerTag);
                });
    }
}

1.2.4 生产者
/**
 * @author: wy
 * describe: 生产者
 * 1. 消息应答
 * 2. 队列持久化
 * 3. 消息持久化
 * 4. 不公平分发
 *  4.1 prefetchCount = 0 轮询(默认)
 *  4.2 prefetchCount = 1 不公平分发,能者多劳
 *  4.3 prefetchCount = 2 预取值
 */
public class Producer {

    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 获取信道
        Channel channel = RabbitMQUtil.getChannel();
        // 2. 队列持久化
        boolean durable = true;
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
        // 从控制台接收消息
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入消息: ");
        while (scanner.hasNext()) {
            String message = scanner.next();
            // 3. 消息持久化
            AMQP.BasicProperties props = MessageProperties.PERSISTENT_TEXT_PLAIN;
            channel.basicPublish("", QUEUE_NAME, props, message.getBytes("UTF-8"));
            System.out.printf("发送消息: %s\r\n", message);
        }
    }
}

2. RabbitMQ 持久化

  • 刚刚我们已经看到了如何 处理任务不丢失 的情况,但是如何保障当 RabbitMQ 服务停掉以后,消息生产者发送过来的 消息不丢失
  • 默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。
  • 确保消息不会丢失需要做两件事:我们需要将 队列 和 消息 都标记为持久化

2.1 队列持久化(Features = D
  • 之前我们创建的队列都是非持久化的,RabbitMQ 如果重启的话,该队列就会被删除掉。
  • 如果要队列实现持久化,需要在声明队列的时候把 durable 参数设置为持久化。
// 2. 队列持久化
boolean durable = true;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
  • 以下为控制台中 持久化 与 非持久化 队列的 UI 显示区。
    在这里插入图片描述
  • 这个时候即使重启 RabbitMQ 队列也依然存在。

2.2 消息持久化
  • 要想让消息实现持久化,需要在消息生产者端添加这个属性:MessageProperties.PERSISTENT_TEXT_PLAIN
// 3. 消息持久化
AMQP.BasicProperties props = MessageProperties.PERSISTENT_TEXT_PLAIN;
channel.basicPublish("", QUEUE_NAME, props, message.getBytes("UTF-8"));
  • 将消息标记为持久化,并不能完全保证消息不会丢失。
  • 尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点。
  • 此时并没有真正写入磁盘。
  • 持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。
  • 如果需要更强有力的持久化策略,参考 发布确认

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值