Java RabbitMQ 实战指南:从交换机、队列到可靠消息处理

45 阅读18分钟

简介

RabbitMQ 是一个消息代理,也常被叫作消息队列中间件。

它基于 AMQP 协议,常用于异步处理、系统解耦、流量削峰、任务分发和事件通知。

简单理解:

生产者
  |
  v
RabbitMQ
  |
  v
消费者

更完整一点:

Producer
  |
  v
Exchange
  |
  v
Queue
  |
  v
Consumer

一句话概括:

RabbitMQ 负责接收消息、路由消息、保存消息,并把消息交给消费者处理。

RabbitMQ 解决什么问题

一个下单接口可能包含很多动作:

创建订单
扣库存
发短信
发邮件
生成物流单
写操作日志

如果全部同步执行,接口耗时会变长。

如果短信服务或邮件服务短暂不可用,下单流程也容易被拖住。

引入 RabbitMQ 后,可以拆成:

订单服务
  |
  v
发送订单创建消息
  |
  v
RabbitMQ
  |
  +--> 短信服务消费消息
  +--> 邮件服务消费消息
  +--> 物流服务消费消息
  +--> 日志服务消费消息

常见收益:

  • 异步处理:主流程更快返回
  • 系统解耦:生产者不直接依赖消费者
  • 削峰填谷:突发流量先进入队列
  • 失败缓冲:消费者短暂异常时,消息可以暂存在队列
  • 任务分发:多个消费者共同处理同一类任务

核心概念

概念说明
Producer生产者,发送消息的应用
Consumer消费者,处理消息的应用
BrokerRabbitMQ 服务端
Exchange交换机,负责路由消息
Queue队列,真正保存消息
Binding交换机和队列之间的绑定关系
Routing Key路由键,生产者发送消息时携带
Binding Key绑定键,队列绑定交换机时使用
Virtual Host虚拟主机,用于逻辑隔离
Connection客户端到 RabbitMQ 的 TCP 连接
Channel连接里的轻量通信通道

一个消息从发送到消费,大致是:

Producer 发送消息到 Exchange
Exchange 根据 Routing Key 和 Binding 找到 Queue
Queue 保存消息
Consumer 从 Queue 获取消息
Consumer 处理完成后确认消息
RabbitMQ 删除已确认消息

Exchange 类型

RabbitMQ 常见交换机有几种。

类型路由规则常见场景
Directrouting key 精确匹配订单状态、指定业务事件
Fanout广播到所有绑定队列通知、缓存刷新、发布订阅
Topic通配符匹配 routing key业务事件总线、日志分类
Headers根据消息头匹配较少使用

Direct Exchange

Direct Exchange 按 routing key 精确匹配。

Exchange: order.exchange

Queue: order.created.queue
Binding Key: order.created

Queue: order.paid.queue
Binding Key: order.paid

发送:

routing key = order.created

消息进入:

order.created.queue

适合订单创建、订单支付、库存扣减这类明确事件。

Fanout Exchange

Fanout Exchange 会把消息发送给所有绑定队列。

它忽略 routing key。

notice.exchange
  |
  +--> sms.queue
  +--> email.queue
  +--> app.push.queue

适合广播通知。

比如用户注册成功后:

短信服务收到消息
邮件服务收到消息
积分服务收到消息

Topic Exchange

Topic Exchange 支持通配符。

规则:

通配符含义
*匹配一个单词
#匹配零个或多个单词

示例:

order.created
order.paid
order.cancelled
product.stock.changed

绑定:

order.*        匹配 order.created、order.paid
order.#        匹配 order.created、order.paid、order.refund.created
*.created      匹配 order.created、user.created
#              匹配所有 routing key

Topic 很适合做业务事件分发。

Docker 启动 RabbitMQ

本地学习可以用带管理页面的镜像。

docker run -d \
  --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  -e RABBITMQ_DEFAULT_USER=admin \
  -e RABBITMQ_DEFAULT_PASS=admin123 \
  rabbitmq:management

端口说明:

端口作用
5672AMQP 客户端连接端口
15672管理后台页面

管理后台:

http://localhost:15672

账号:

admin / admin123

原生 Java Client 依赖

非 Spring 项目可以直接使用 RabbitMQ Java Client。

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>${rabbitmq.client.version}</version>
</dependency>

版本建议交给 Maven 属性、BOM 或依赖管理统一控制。

原生 Demo:发送消息

这个 Demo 使用默认交换机。

默认交换机比较特殊,routing key 等于队列名时,消息会直接路由到对应队列。

package com.example.rabbitmq.raw;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.nio.charset.StandardCharsets;
import java.util.Map;

public class RawProducer {

    private static final String QUEUE_NAME = "demo.hello.queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin123");
        factory.setVirtualHost("/");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            Map<String, Object> arguments = Map.of("x-queue-type", "quorum");
            channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);

            String message = "Hello RabbitMQ";

            channel.basicPublish(
                    "",
                    QUEUE_NAME,
                    null,
                    message.getBytes(StandardCharsets.UTF_8)
            );

            System.out.println("消息已发送:" + message);
        }
    }
}

这里的参数:

channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);

含义:

参数说明
queue队列名
durable队列是否持久化
exclusive是否仅当前连接可用
autoDelete最后一个消费者断开后是否自动删除
arguments队列额外参数

x-queue-type=quorum 表示声明 quorum queue。

原生 Demo:消费消息

package com.example.rabbitmq.raw;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.nio.charset.StandardCharsets;
import java.util.Map;

public class RawConsumer {

    private static final String QUEUE_NAME = "demo.hello.queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin123");
        factory.setVirtualHost("/");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        Map<String, Object> arguments = Map.of("x-queue-type", "quorum");
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
        channel.basicQos(10);

        DeliverCallback callback = (consumerTag, delivery) -> {
            long deliveryTag = delivery.getEnvelope().getDeliveryTag();
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);

            try {
                System.out.println("收到消息:" + message);
                channel.basicAck(deliveryTag, false);
            } catch (Exception e) {
                channel.basicNack(deliveryTag, false, true);
            }
        };

        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, callback, consumerTag -> {
        });

        System.out.println("消费者已启动");
    }
}

这里使用手动确认:

boolean autoAck = false;

处理成功:

channel.basicAck(deliveryTag, false);

处理失败并重新入队:

channel.basicNack(deliveryTag, false, true);

Spring Boot 依赖

Spring Boot 项目通常使用 Spring AMQP。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

它会提供:

  • RabbitTemplate
  • @RabbitListener
  • 连接工厂自动配置
  • 消息转换
  • 监听容器
  • 队列、交换机、绑定声明能力

application.yml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: admin
    password: admin123
    virtual-host: /
    publisher-confirm-type: correlated
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 10

说明:

配置作用
publisher-confirm-type: correlated开启发布确认
publisher-returns: true路由失败时返回消息
acknowledge-mode: manual消费端手动确认
prefetch: 10单个消费者最多预取 10 条未确认消息

消息对象

示例使用订单创建事件。

package com.example.rabbitmq.order;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public record OrderCreatedEvent(
        Long orderId,
        String orderNo,
        Long userId,
        BigDecimal amount,
        LocalDateTime createdAt
) {
}

JSON 消息转换器

默认序列化方式不适合跨语言系统。

业务项目更常见的是 JSON。

package com.example.rabbitmq.config;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMessageConfig {

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

Direct 实战:订单创建消息

配置交换机、队列和绑定。

package com.example.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OrderRabbitConfig {

    public static final String ORDER_EXCHANGE = "order.exchange";
    public static final String ORDER_CREATED_QUEUE = "order.created.queue";
    public static final String ORDER_CREATED_ROUTING_KEY = "order.created";

    @Bean
    public DirectExchange orderExchange() {
        return ExchangeBuilder
                .directExchange(ORDER_EXCHANGE)
                .durable(true)
                .build();
    }

    @Bean
    public Queue orderCreatedQueue() {
        return QueueBuilder
                .durable(ORDER_CREATED_QUEUE)
                .quorum()
                .build();
    }

    @Bean
    public Binding orderCreatedBinding(
            Queue orderCreatedQueue,
            DirectExchange orderExchange
    ) {
        return BindingBuilder
                .bind(orderCreatedQueue)
                .to(orderExchange)
                .with(ORDER_CREATED_ROUTING_KEY);
    }
}

生产者:

package com.example.rabbitmq.order;

import com.example.rabbitmq.config.OrderRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

@Service
public class OrderEventProducer {

    private final RabbitTemplate rabbitTemplate;

    public OrderEventProducer(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public void sendOrderCreated(OrderCreatedEvent event) {
        rabbitTemplate.convertAndSend(
                OrderRabbitConfig.ORDER_EXCHANGE,
                OrderRabbitConfig.ORDER_CREATED_ROUTING_KEY,
                event
        );
    }
}

Controller:

package com.example.rabbitmq.order;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderEventProducer producer;

    public OrderController(OrderEventProducer producer) {
        this.producer = producer;
    }

    @PostMapping
    public String createOrder() {
        OrderCreatedEvent event = new OrderCreatedEvent(
                1001L,
                "O202606290001",
                2001L,
                new BigDecimal("99.80"),
                LocalDateTime.now()
        );

        producer.sendOrderCreated(event);
        return "订单创建消息已发送";
    }
}

消费者:

package com.example.rabbitmq.order;

import com.example.rabbitmq.config.OrderRabbitConfig;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class OrderCreatedConsumer {

    @RabbitListener(queues = OrderRabbitConfig.ORDER_CREATED_QUEUE)
    public void handle(
            OrderCreatedEvent event,
            Channel channel,
            @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag
    ) throws IOException {
        try {
            System.out.println("收到订单创建消息:" + event);

            // 执行业务逻辑:发短信、写日志、生成任务等

            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            channel.basicNack(deliveryTag, false, false);
        }
    }
}

这里失败时使用:

channel.basicNack(deliveryTag, false, false);

第三个参数 false 表示不重新入队。

如果配置了死信交换机,消息会进入死信队列。

Fanout 实战:广播通知

一个用户注册事件,同时通知多个服务。

user.registered.exchange
  |
  +--> sms.notice.queue
  +--> email.notice.queue

配置:

package com.example.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NoticeRabbitConfig {

    public static final String USER_REGISTERED_EXCHANGE = "user.registered.exchange";
    public static final String SMS_NOTICE_QUEUE = "sms.notice.queue";
    public static final String EMAIL_NOTICE_QUEUE = "email.notice.queue";

    @Bean
    public FanoutExchange userRegisteredExchange() {
        return ExchangeBuilder
                .fanoutExchange(USER_REGISTERED_EXCHANGE)
                .durable(true)
                .build();
    }

    @Bean
    public Queue smsNoticeQueue() {
        return QueueBuilder.durable(SMS_NOTICE_QUEUE).quorum().build();
    }

    @Bean
    public Queue emailNoticeQueue() {
        return QueueBuilder.durable(EMAIL_NOTICE_QUEUE).quorum().build();
    }

    @Bean
    public Binding smsNoticeBinding(Queue smsNoticeQueue, FanoutExchange userRegisteredExchange) {
        return BindingBuilder.bind(smsNoticeQueue).to(userRegisteredExchange);
    }

    @Bean
    public Binding emailNoticeBinding(Queue emailNoticeQueue, FanoutExchange userRegisteredExchange) {
        return BindingBuilder.bind(emailNoticeQueue).to(userRegisteredExchange);
    }
}

发送:

rabbitTemplate.convertAndSend(
        NoticeRabbitConfig.USER_REGISTERED_EXCHANGE,
        "",
        new UserRegisteredEvent(3001L, "zhangsan@example.com", "13800000000")
);

事件对象:

public record UserRegisteredEvent(
        Long userId,
        String email,
        String phone
) {
}

短信消费者:

@RabbitListener(queues = NoticeRabbitConfig.SMS_NOTICE_QUEUE)
public void sendSms(UserRegisteredEvent event) {
    System.out.println("发送短信:" + event.phone());
}

邮件消费者:

@RabbitListener(queues = NoticeRabbitConfig.EMAIL_NOTICE_QUEUE)
public void sendEmail(UserRegisteredEvent event) {
    System.out.println("发送邮件:" + event.email());
}

Fanout 会让两个队列都收到同一条消息。

Topic 实战:业务事件分发

Topic 适合做事件总线。

business.event.exchange
  |
  +--> order.event.queue    binding: order.#
  +--> all.event.queue      binding: #

配置:

package com.example.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BusinessEventRabbitConfig {

    public static final String BUSINESS_EVENT_EXCHANGE = "business.event.exchange";
    public static final String ORDER_EVENT_QUEUE = "order.event.queue";
    public static final String ALL_EVENT_QUEUE = "all.event.queue";

    @Bean
    public TopicExchange businessEventExchange() {
        return ExchangeBuilder
                .topicExchange(BUSINESS_EVENT_EXCHANGE)
                .durable(true)
                .build();
    }

    @Bean
    public Queue orderEventQueue() {
        return QueueBuilder.durable(ORDER_EVENT_QUEUE).quorum().build();
    }

    @Bean
    public Queue allEventQueue() {
        return QueueBuilder.durable(ALL_EVENT_QUEUE).quorum().build();
    }

    @Bean
    public Binding orderEventBinding(Queue orderEventQueue, TopicExchange businessEventExchange) {
        return BindingBuilder
                .bind(orderEventQueue)
                .to(businessEventExchange)
                .with("order.#");
    }

    @Bean
    public Binding allEventBinding(Queue allEventQueue, TopicExchange businessEventExchange) {
        return BindingBuilder
                .bind(allEventQueue)
                .to(businessEventExchange)
                .with("#");
    }
}

发送:

rabbitTemplate.convertAndSend(
        BusinessEventRabbitConfig.BUSINESS_EVENT_EXCHANGE,
        "order.paid",
        new OrderPaidEvent(1001L, "O202606290001")
);

order.event.queueall.event.queue 都会收到这条消息。

发布确认

消费端确认和发布端确认不是一回事。

发布确认:RabbitMQ 告诉生产者,消息是否到达 Broker
消费确认:消费者告诉 RabbitMQ,消息是否处理完成

开启配置:

spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true

配置 RabbitTemplate 回调:

package com.example.rabbitmq.config;

import jakarta.annotation.PostConstruct;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.ReturnedMessage;
import org.springframework.stereotype.Component;

@Component
public class RabbitTemplateCallbackConfig {

    private final RabbitTemplate rabbitTemplate;

    public RabbitTemplateCallbackConfig(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this::confirm);
        rabbitTemplate.setReturnsCallback(this::returned);
    }

    private void confirm(
            CorrelationData correlationData,
            boolean ack,
            String cause
    ) {
        if (ack) {
            System.out.println("消息到达 Broker:" + correlationData);
        } else {
            System.out.println("消息未到达 Broker:" + cause);
        }
    }

    private void returned(ReturnedMessage returnedMessage) {
        System.out.println("消息路由失败:" + returnedMessage);
    }
}

发送时带 CorrelationData

import org.springframework.amqp.rabbit.connection.CorrelationData;

CorrelationData correlationData = new CorrelationData("msg-1001");

rabbitTemplate.convertAndSend(
        OrderRabbitConfig.ORDER_EXCHANGE,
        OrderRabbitConfig.ORDER_CREATED_ROUTING_KEY,
        event,
        correlationData
);

发布确认只能说明消息到达了 Broker。

它不能说明消费者已经处理完成。

消费确认

手动 ACK 常见方法:

方法含义
basicAck(tag, false)确认当前消息
basicNack(tag, false, true)拒绝当前消息,并重新入队
basicNack(tag, false, false)拒绝当前消息,不重新入队
basicReject(tag, true)拒绝当前消息,并重新入队
basicReject(tag, false)拒绝当前消息,不重新入队

常见写法:

try {
    handleBusiness(message);
    channel.basicAck(deliveryTag, false);
} catch (RetryableException e) {
    channel.basicNack(deliveryTag, false, true);
} catch (Exception e) {
    channel.basicNack(deliveryTag, false, false);
}

注意:

basicNack(..., true) 会重新入队
如果业务一直失败,可能形成重复投递循环

所以更常见的是结合重试、死信队列和幂等处理。

basicNack 和 basicReject 的区别

basicNackbasicReject 都可以拒绝消息。

核心区别是:

basicReject 只能拒绝一条消息
basicNack 可以拒绝一条消息,也可以批量拒绝多条消息

basicReject 的参数:

channel.basicReject(deliveryTag, true);
channel.basicReject(deliveryTag, false);

含义:

参数说明
deliveryTag消息投递标识
requeue是否重新入队

basicNack 的参数:

channel.basicNack(deliveryTag, false, true);
channel.basicNack(deliveryTag, false, false);

含义:

参数说明
deliveryTag消息投递标识
multiple是否批量处理
requeue是否重新入队

第二个参数 multiple 是关键。

channel.basicNack(deliveryTag, false, false);

表示:

只拒绝当前这一条消息,并且不重新入队。
channel.basicNack(deliveryTag, true, false);

表示:

拒绝当前 deliveryTag 及之前所有未确认消息,并且不重新入队。

对比:

方法能否批量是否能重新入队常见用途
basicReject拒绝单条消息
basicNack拒绝单条或批量消息

业务代码里更常见的是:

channel.basicAck(deliveryTag, false);
channel.basicNack(deliveryTag, false, false);

含义:

处理成功:确认当前消息
处理失败:拒绝当前消息,不重新入队,让消息进入死信链路

prefetch

prefetch 控制消费者一次最多拿多少条未确认消息。

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 10

如果消费很慢,prefetch 太大可能导致某个消费者堆积很多未确认消息。

如果消费很快,prefetch 太小可能影响吞吐。

常见思路:

单条消息处理耗时长:prefetch 设置小一点
单条消息处理很快:prefetch 可以适当增大

死信队列

消息可能变成死信。

常见原因:

  • 消息被拒绝,并且不重新入队
  • 消息过期
  • 队列长度超过限制
  • quorum queue 投递次数超过限制

配置死信交换机:

package com.example.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DeadLetterRabbitConfig {

    public static final String ORDER_EXCHANGE = "order.dlx.demo.exchange";
    public static final String ORDER_QUEUE = "order.dlx.demo.queue";
    public static final String ORDER_ROUTING_KEY = "order.created";

    public static final String ORDER_DLX = "order.dead.exchange";
    public static final String ORDER_DLQ = "order.dead.queue";
    public static final String ORDER_DEAD_ROUTING_KEY = "order.dead";

    @Bean
    public DirectExchange orderDlxDemoExchange() {
        return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
    }

    @Bean
    public DirectExchange orderDeadExchange() {
        return ExchangeBuilder.directExchange(ORDER_DLX).durable(true).build();
    }

    @Bean
    public Queue orderDlxDemoQueue() {
        return QueueBuilder
                .durable(ORDER_QUEUE)
                .quorum()
                .deadLetterExchange(ORDER_DLX)
                .deadLetterRoutingKey(ORDER_DEAD_ROUTING_KEY)
                .ttl(60_000)
                .build();
    }

    @Bean
    public Queue orderDeadQueue() {
        return QueueBuilder
                .durable(ORDER_DLQ)
                .quorum()
                .build();
    }

    @Bean
    public Binding orderDlxDemoBinding() {
        return BindingBuilder
                .bind(orderDlxDemoQueue())
                .to(orderDlxDemoExchange())
                .with(ORDER_ROUTING_KEY);
    }

    @Bean
    public Binding orderDeadBinding() {
        return BindingBuilder
                .bind(orderDeadQueue())
                .to(orderDeadExchange())
                .with(ORDER_DEAD_ROUTING_KEY);
    }
}

这 6 个常量可以分成两组。

正常业务链路:

public static final String ORDER_EXCHANGE = "order.dlx.demo.exchange";
public static final String ORDER_QUEUE = "order.dlx.demo.queue";
public static final String ORDER_ROUTING_KEY = "order.created";

含义:

正常交换机:order.dlx.demo.exchange
正常队列:order.dlx.demo.queue
正常路由键:order.created

正常消息流转:

生产者
  |
  | exchange = order.dlx.demo.exchange
  | routing key = order.created
  v
order.dlx.demo.exchange
  |
  v
order.dlx.demo.queue
  |
  v
正常消费者

死信链路:

public static final String ORDER_DLX = "order.dead.exchange";
public static final String ORDER_DLQ = "order.dead.queue";
public static final String ORDER_DEAD_ROUTING_KEY = "order.dead";

含义:

死信交换机:order.dead.exchange
死信队列:order.dead.queue
死信路由键:order.dead

死信消息流转:

正常队列里的消息变成死信
  |
  v
order.dead.exchange
  |
  | routing key = order.dead
  v
order.dead.queue

关键配置在正常队列上:

.deadLetterExchange(ORDER_DLX)
.deadLetterRoutingKey(ORDER_DEAD_ROUTING_KEY)

它的意思是:

order.dlx.demo.queue 里的消息如果变成死信,
就转发到 order.dead.exchange,
并使用 routing key = order.dead。

再通过死信绑定:

order.dead.exchange + order.dead -> order.dead.queue

死信最终进入 order.dead.queue

死信配置逐段说明

正常交换机:

@Bean
public DirectExchange orderDlxDemoExchange() {
    return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
}

创建 Direct 类型的正常业务交换机。

名字:order.dlx.demo.exchange
持久化:true

死信交换机:

@Bean
public DirectExchange orderDeadExchange() {
    return ExchangeBuilder.directExchange(ORDER_DLX).durable(true).build();
}

创建 Direct 类型的死信交换机。

名字:order.dead.exchange
持久化:true

正常队列:

@Bean
public Queue orderDlxDemoQueue() {
    return QueueBuilder
            .durable(ORDER_QUEUE)
            .quorum()
            .deadLetterExchange(ORDER_DLX)
            .deadLetterRoutingKey(ORDER_DEAD_ROUTING_KEY)
            .ttl(60_000)
            .build();
}

逐项说明:

配置说明
durable(ORDER_QUEUE)创建持久化正常队列
quorum()使用 quorum queue
deadLetterExchange(ORDER_DLX)设置死信交换机
deadLetterRoutingKey(ORDER_DEAD_ROUTING_KEY)设置死信 routing key
ttl(60_000)消息 60 秒后过期

这段配置的整体含义:

order.dlx.demo.queue 是正常队列。
如果消息被拒绝且不重新入队,或者 60 秒过期,
就转发到 order.dead.exchange,
routing key 使用 order.dead。

死信队列:

@Bean
public Queue orderDeadQueue() {
    return QueueBuilder
            .durable(ORDER_DLQ)
            .quorum()
            .build();
}

创建真正保存死信消息的队列。

名字:order.dead.queue
类型:quorum queue

正常绑定:

@Bean
public Binding orderDlxDemoBinding() {
    return BindingBuilder
            .bind(orderDlxDemoQueue())
            .to(orderDlxDemoExchange())
            .with(ORDER_ROUTING_KEY);
}

绑定正常消息链路:

order.dlx.demo.exchange
  |
  | routing key = order.created
  v
order.dlx.demo.queue

死信绑定:

@Bean
public Binding orderDeadBinding() {
    return BindingBuilder
            .bind(orderDeadQueue())
            .to(orderDeadExchange())
            .with(ORDER_DEAD_ROUTING_KEY);
}

绑定死信消息链路:

order.dead.exchange
  |
  | routing key = order.dead
  v
order.dead.queue

完整正常流程:

生产者
  |
  v
正常交换机
  |
  v
正常队列
  |
  v
消费者处理成功
  |
  v
basicAck
  |
  v
消息删除

完整死信流程:

生产者
  |
  v
正常交换机
  |
  v
正常队列
  |
  v
消费者处理失败
  |
  v
basicNack(deliveryTag, false, false)
  |
  v
消息不重新入队
  |
  v
转发到死信交换机
  |
  v
进入死信队列

消费死信:

@RabbitListener(queues = DeadLetterRabbitConfig.ORDER_DLQ)
public void handleDeadLetter(OrderCreatedEvent event) {
    System.out.println("收到死信消息:" + event);
}

死信队列常用于:

  • 保存异常消息
  • 后台人工处理
  • 延迟处理
  • 重试次数耗尽后的兜底

延迟消息

RabbitMQ 常见延迟方案有两类:

TTL + 死信交换机
Delayed Message Exchange 插件

TTL + DLX 的思路:

消息先进入延迟队列
延迟队列不设置消费者
消息过期后变成死信
死信进入真正业务队列
消费者处理业务队列

适合订单超时取消这类场景。

如果需要更灵活的 per-message 延迟,常见做法是启用 RabbitMQ delayed message exchange 插件。

具体方案需要结合 RabbitMQ 版本、插件启用情况、运维规范来定。

幂等处理

RabbitMQ 不能保证业务只处理一次。

实际系统里可能出现重复投递:

  • 消费者处理完成但 ACK 失败
  • 网络抖动导致重新投递
  • 生产者重试发送
  • 服务重启后重新消费未确认消息

消费者需要做幂等。

常见做法:

消息带唯一 messageId
消费前查处理记录
没有处理过才执行业务
处理成功后保存记录
重复消息直接 ACK

消息对象:

public record ReliableMessage<T>(
        String messageId,
        T payload
) {
}

消费者伪代码:

@RabbitListener(queues = OrderRabbitConfig.ORDER_CREATED_QUEUE)
public void handle(
        ReliableMessage<OrderCreatedEvent> message,
        Channel channel,
        @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag
) throws IOException {
    try {
        if (messageLogRepository.exists(message.messageId())) {
            channel.basicAck(deliveryTag, false);
            return;
        }

        handleBusiness(message.payload());
        messageLogRepository.save(message.messageId());

        channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
        channel.basicNack(deliveryTag, false, false);
    }
}

幂等位置可以放在:

  • 数据库唯一键
  • Redis SETNX
  • 消息消费日志表
  • 业务状态机

可靠消息常见链路

更完整的可靠链路通常包括:

生产者本地业务事务
  |
  v
保存消息记录
  |
  v
发送 RabbitMQ
  |
  v
发布确认更新消息状态
  |
  v
消费者手动 ACK
  |
  v
消费者幂等处理
  |
  v
异常消息进入死信队列

如果业务对一致性要求很高,可以使用本地消息表或 outbox 模式。

简单示意:

同一个数据库事务:
  1. 创建订单
  2. 写入待发送消息表

后台任务:
  1. 扫描待发送消息
  2. 发送 RabbitMQ
  3. 根据发布确认更新状态

管理后台常看指标

RabbitMQ 管理后台常看这些内容:

指标含义
Ready队列里等待消费的消息数
Unacked已投递但未确认的消息数
TotalReady + Unacked
Publish rate发布速率
Deliver rate投递速率
Ack rate确认速率
Consumers消费者数量
Memory内存占用
Disk磁盘占用

排查思路:

Ready 持续增加:消费者处理能力不足或消费者异常
Unacked 很高:消费者处理慢、ACK 失败或 prefetch 过大
Publish 高于 Ack:生产速度超过消费速度
Consumers 为 0:消费者应用未启动或监听失败

RabbitMQ 数据存储在哪里

RabbitMQ 的数据存储在 Broker 节点上。

主要包括:

  • vhost、用户、权限等元数据
  • exchange、queue、binding 等拓扑信息
  • 队列里的消息数据

Docker 容器里常见数据目录是:

/var/lib/rabbitmq

如果没有挂载 volume,容器删除后数据也会删除。

本地开发可以挂载数据卷:

docker run -d \
  --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  -e RABBITMQ_DEFAULT_USER=admin \
  -e RABBITMQ_DEFAULT_PASS=admin123 \
  -v rabbitmq-data:/var/lib/rabbitmq \
  rabbitmq:management

这样 RabbitMQ 数据会保存到 Docker volume:

rabbitmq-data

消息是否能在 RabbitMQ 重启后保留,还要看几个条件:

exchange 是否 durable
queue 是否 durable
message 是否 persistent

常见配置:

ExchangeBuilder.directExchange("order.exchange").durable(true).build();
QueueBuilder.durable("order.queue").build();

Spring AMQP 常见发送方式通常会配合持久化队列发送持久化消息。

生产项目仍然适合通过重启 RabbitMQ、查看管理后台和消费验证来确认消息持久化行为。

简单总结:

RabbitMQ 数据由 Broker 管理。
持久化元数据和持久化消息会落到节点数据目录。
非持久化消息主要在内存中,服务重启后可能丢失。

常见使用建议

队列和交换机持久化

关键业务队列和交换机通常需要持久化。

ExchangeBuilder.directExchange("order.exchange").durable(true).build();
QueueBuilder.durable("order.queue").build();

消息本身也需要持久化投递。

Spring AMQP 默认消息持久化策略通常已经适合多数持久化队列场景,但生产项目仍应结合消息转换器和发送属性做验证。

区分发布确认和消费确认

发布确认:消息到没到 RabbitMQ
消费确认:消费者处理完没有

两者解决的问题不同,不能互相替代。

避免无限重新入队

消费失败后一直 basicNack(..., true),可能导致同一条消息反复投递。

更常见做法:

可重试异常:有限次数重试
不可恢复异常:进入死信队列

控制 prefetch 和并发

消费者吞吐不够时,可以增加消费者实例或并发数。

但数据库、Redis、第三方接口也有容量上限。

队列堆积时,需要同时看:

  • 消费者数量
  • 单条消息处理耗时
  • prefetch
  • 下游数据库连接池
  • 下游接口限流

消费者需要幂等

消息系统里“至少一次”投递更常见。

消费者应该能承受重复消息。

比如订单支付成功消息,可以根据订单状态判断:

未处理 -> 执行业务
已处理 -> 直接 ACK

常用 API 汇总

API / 注解作用
RabbitTemplate.convertAndSend(...)发送消息
@RabbitListener监听队列
QueueBuilder.durable(...)创建持久化队列
ExchangeBuilder.directExchange(...)创建 Direct 交换机
ExchangeBuilder.fanoutExchange(...)创建 Fanout 交换机
ExchangeBuilder.topicExchange(...)创建 Topic 交换机
BindingBuilder.bind(...).to(...)绑定队列和交换机
Jackson2JsonMessageConverterJSON 消息转换
channel.basicAck(...)确认消息
channel.basicNack(...)拒绝消息
channel.basicQos(...)设置预取数量
CorrelationData发布确认关联数据

总结

RabbitMQ 的核心是消息路由和异步消费。

最重要的几个概念:

Producer
Exchange
Routing Key
Binding
Queue
Consumer
ACK
DLX

普通业务可以从 Direct、Fanout、Topic 三种交换机开始。

真正进入工程实践后,需要重点关注:

  • 发布确认
  • 消费确认
  • 消息持久化
  • 死信队列
  • 消费幂等
  • prefetch
  • 监控和告警

RabbitMQ 不是简单地“把消息发出去就结束”。

可靠消息处理更像一条链路:生产者确认、Broker 路由、队列保存、消费者确认、异常兜底、重复消费处理,每个环节都需要有明确设计。