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

简介

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

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

简单理解:

text 复制代码
生产者
  |
  v
RabbitMQ
  |
  v
消费者

更完整一点:

text 复制代码
Producer
  |
  v
Exchange
  |
  v
Queue
  |
  v
Consumer

一句话概括:

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

RabbitMQ 解决什么问题

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

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

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

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

引入 RabbitMQ 后,可以拆成:

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

常见收益:

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

核心概念

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

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

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

Exchange 类型

RabbitMQ 常见交换机有几种。

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

Direct Exchange

Direct Exchange 按 routing key 精确匹配。

text 复制代码
Exchange: order.exchange

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

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

发送:

text 复制代码
routing key = order.created

消息进入:

text 复制代码
order.created.queue

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

Fanout Exchange

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

它忽略 routing key。

text 复制代码
notice.exchange
  |
  +--> sms.queue
  +--> email.queue
  +--> app.push.queue

适合广播通知。

比如用户注册成功后:

text 复制代码
短信服务收到消息
邮件服务收到消息
积分服务收到消息

Topic Exchange

Topic Exchange 支持通配符。

规则:

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

示例:

text 复制代码
order.created
order.paid
order.cancelled
product.stock.changed

绑定:

text 复制代码
order.*        匹配 order.created、order.paid
order.#        匹配 order.created、order.paid、order.refund.created
*.created      匹配 order.created、user.created
#              匹配所有 routing key

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

Docker 启动 RabbitMQ

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

bash 复制代码
docker run -d \
  --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  -e RABBITMQ_DEFAULT_USER=admin \
  -e RABBITMQ_DEFAULT_PASS=admin123 \
  rabbitmq:management

端口说明:

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

管理后台:

text 复制代码
http://localhost:15672

账号:

text 复制代码
admin / admin123

原生 Java Client 依赖

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

xml 复制代码
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>${rabbitmq.client.version}</version>
</dependency>

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

原生 Demo:发送消息

这个 Demo 使用默认交换机。

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

java 复制代码
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);
        }
    }
}

这里的参数:

java 复制代码
channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);

含义:

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

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

原生 Demo:消费消息

java 复制代码
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("消费者已启动");
    }
}

这里使用手动确认:

java 复制代码
boolean autoAck = false;

处理成功:

java 复制代码
channel.basicAck(deliveryTag, false);

处理失败并重新入队:

java 复制代码
channel.basicNack(deliveryTag, false, true);

Spring Boot 依赖

Spring Boot 项目通常使用 Spring AMQP。

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

它会提供:

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

application.yml

yaml 复制代码
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 条未确认消息

消息对象

示例使用订单创建事件。

java 复制代码
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。

java 复制代码
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 实战:订单创建消息

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

java 复制代码
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);
    }
}

生产者:

java 复制代码
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:

java 复制代码
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 "订单创建消息已发送";
    }
}

消费者:

java 复制代码
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);
        }
    }
}

这里失败时使用:

java 复制代码
channel.basicNack(deliveryTag, false, false);

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

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

Fanout 实战:广播通知

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

text 复制代码
user.registered.exchange
  |
  +--> sms.notice.queue
  +--> email.notice.queue

配置:

java 复制代码
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);
    }
}

发送:

java 复制代码
rabbitTemplate.convertAndSend(
        NoticeRabbitConfig.USER_REGISTERED_EXCHANGE,
        "",
        new UserRegisteredEvent(3001L, "zhangsan@example.com", "13800000000")
);

事件对象:

java 复制代码
public record UserRegisteredEvent(
        Long userId,
        String email,
        String phone
) {
}

短信消费者:

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

邮件消费者:

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

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

Topic 实战:业务事件分发

Topic 适合做事件总线。

text 复制代码
business.event.exchange
  |
  +--> order.event.queue    binding: order.#
  +--> all.event.queue      binding: #

配置:

java 复制代码
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("#");
    }
}

发送:

java 复制代码
rabbitTemplate.convertAndSend(
        BusinessEventRabbitConfig.BUSINESS_EVENT_EXCHANGE,
        "order.paid",
        new OrderPaidEvent(1001L, "O202606290001")
);

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

发布确认

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

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

开启配置:

yaml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true

配置 RabbitTemplate 回调:

java 复制代码
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

java 复制代码
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) 拒绝当前消息,不重新入队

常见写法:

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

注意:

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

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

basicNack 和 basicReject 的区别

basicNackbasicReject 都可以拒绝消息。

核心区别是:

text 复制代码
basicReject 只能拒绝一条消息
basicNack 可以拒绝一条消息,也可以批量拒绝多条消息

basicReject 的参数:

java 复制代码
channel.basicReject(deliveryTag, true);
channel.basicReject(deliveryTag, false);

含义:

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

basicNack 的参数:

java 复制代码
channel.basicNack(deliveryTag, false, true);
channel.basicNack(deliveryTag, false, false);

含义:

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

第二个参数 multiple 是关键。

java 复制代码
channel.basicNack(deliveryTag, false, false);

表示:

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

表示:

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

对比:

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

业务代码里更常见的是:

java 复制代码
channel.basicAck(deliveryTag, false);
channel.basicNack(deliveryTag, false, false);

含义:

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

prefetch

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

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 10

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

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

常见思路:

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

死信队列

消息可能变成死信。

常见原因:

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

配置死信交换机:

java 复制代码
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 个常量可以分成两组。

正常业务链路:

java 复制代码
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";

含义:

text 复制代码
正常交换机:order.dlx.demo.exchange
正常队列:order.dlx.demo.queue
正常路由键:order.created

正常消息流转:

text 复制代码
生产者
  |
  | exchange = order.dlx.demo.exchange
  | routing key = order.created
  v
order.dlx.demo.exchange
  |
  v
order.dlx.demo.queue
  |
  v
正常消费者

死信链路:

java 复制代码
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";

含义:

text 复制代码
死信交换机:order.dead.exchange
死信队列:order.dead.queue
死信路由键:order.dead

死信消息流转:

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

关键配置在正常队列上:

java 复制代码
.deadLetterExchange(ORDER_DLX)
.deadLetterRoutingKey(ORDER_DEAD_ROUTING_KEY)

它的意思是:

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

再通过死信绑定:

text 复制代码
order.dead.exchange + order.dead -> order.dead.queue

死信最终进入 order.dead.queue

死信配置逐段说明

正常交换机:

java 复制代码
@Bean
public DirectExchange orderDlxDemoExchange() {
    return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
}

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

text 复制代码
名字:order.dlx.demo.exchange
持久化:true

死信交换机:

java 复制代码
@Bean
public DirectExchange orderDeadExchange() {
    return ExchangeBuilder.directExchange(ORDER_DLX).durable(true).build();
}

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

text 复制代码
名字:order.dead.exchange
持久化:true

正常队列:

java 复制代码
@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 秒后过期

这段配置的整体含义:

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

死信队列:

java 复制代码
@Bean
public Queue orderDeadQueue() {
    return QueueBuilder
            .durable(ORDER_DLQ)
            .quorum()
            .build();
}

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

text 复制代码
名字:order.dead.queue
类型:quorum queue

正常绑定:

java 复制代码
@Bean
public Binding orderDlxDemoBinding() {
    return BindingBuilder
            .bind(orderDlxDemoQueue())
            .to(orderDlxDemoExchange())
            .with(ORDER_ROUTING_KEY);
}

绑定正常消息链路:

text 复制代码
order.dlx.demo.exchange
  |
  | routing key = order.created
  v
order.dlx.demo.queue

死信绑定:

java 复制代码
@Bean
public Binding orderDeadBinding() {
    return BindingBuilder
            .bind(orderDeadQueue())
            .to(orderDeadExchange())
            .with(ORDER_DEAD_ROUTING_KEY);
}

绑定死信消息链路:

text 复制代码
order.dead.exchange
  |
  | routing key = order.dead
  v
order.dead.queue

完整正常流程:

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

完整死信流程:

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

消费死信:

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

死信队列常用于:

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

延迟消息

RabbitMQ 常见延迟方案有两类:

text 复制代码
TTL + 死信交换机
Delayed Message Exchange 插件

TTL + DLX 的思路:

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

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

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

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

幂等处理

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

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

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

消费者需要做幂等。

常见做法:

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

消息对象:

java 复制代码
public record ReliableMessage<T>(
        String messageId,
        T payload
) {
}

消费者伪代码:

java 复制代码
@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
  • 消息消费日志表
  • 业务状态机

可靠消息常见链路

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

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

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

简单示意:

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

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

管理后台常看指标

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

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

排查思路:

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

RabbitMQ 数据存储在哪里

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

主要包括:

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

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

text 复制代码
/var/lib/rabbitmq

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

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

bash 复制代码
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:

text 复制代码
rabbitmq-data

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

text 复制代码
exchange 是否 durable
queue 是否 durable
message 是否 persistent

常见配置:

java 复制代码
ExchangeBuilder.directExchange("order.exchange").durable(true).build();
QueueBuilder.durable("order.queue").build();

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

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

简单总结:

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

常见使用建议

队列和交换机持久化

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

java 复制代码
ExchangeBuilder.directExchange("order.exchange").durable(true).build();
QueueBuilder.durable("order.queue").build();

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

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

区分发布确认和消费确认

text 复制代码
发布确认:消息到没到 RabbitMQ
消费确认:消费者处理完没有

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

避免无限重新入队

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

更常见做法:

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

控制 prefetch 和并发

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

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

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

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

消费者需要幂等

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

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

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

text 复制代码
未处理 -> 执行业务
已处理 -> 直接 ACK

常用 API 汇总

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

总结

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

最重要的几个概念:

text 复制代码
Producer
Exchange
Routing Key
Binding
Queue
Consumer
ACK
DLX

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

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

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

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

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

相关推荐
卓怡学长1 小时前
w272基于springboot便民医疗服务小程序
java·spring boot·spring·小程序·intellij-idea
一方~1 小时前
权限系统设计
java
盐焗鹌鹑蛋1 小时前
【C++】多态
java·jvm·c++
rebibabo1 小时前
Java高并发底层原理(七)volatile 到底解决了什么问题
java·学习笔记·volatile·可见性·有序性·指令重排
名字还没想好☜2 小时前
Go 并发实战:用 channel 实现 worker pool
java·数据库·后端·golang·go
人道领域2 小时前
【LeetCode刷题日记】贪心算法理论与实战:455.分发饼干最优解
java·开发语言·数据结构·算法·leetcode·贪心算法
rebibabo2 小时前
Java高并发底层原理(六)—— 多核 CPU 如何保持缓存一致
java·缓存·cas·缓存一致性·cache line·longadder·mesi协议
_abab2 小时前
Java面试宝典:从基础到架构2
java·面试·架构