Skip to content

RabbitMQ

这篇笔记聚焦 RabbitMQ 这个具体产品,但不会只停留在“名词解释”层面,而是尽量沿着更接近工程落地的主线来讲:

  1. 消息是怎么从生产者进入 RabbitMQ 的
  2. RabbitMQ 为什么要拆成 ConnectionChannelExchangeQueueBindingRouting Key
  3. 为什么有的场景适合 Direct,有的更适合 Topic
  4. 可靠性、死信队列、TTL、延迟消息到底在解决什么问题
  5. 在 Java / Spring 项目里,RabbitMQ 一般是怎么接入业务系统的

如果你还没建立消息队列这条知识线,可以看:

这篇更强调 RabbitMQ 自己的模型、边界,以及它在真实业务里的常见用法。

1. RabbitMQ 是什么

RabbitMQ 是一个基于 AMQP 协议体系发展起来的消息队列产品。

这里的 AMQP 可以看成 一套面向消息中间件的协议和模型规范

所以 RabbitMQ 的特点,不只是“能把消息放进去再取出来”,而是把消息怎么进入、怎么路由、怎么投递、怎么确认,拆成了一套比较清晰的模型。

它最常被提到的优势通常有:

  1. 消息模型清晰
  2. 路由规则灵活
  3. 生态成熟
  4. 比较适合业务消息、事件通知、异步解耦这类场景

但它也不是所有场景都最优。

如果你的核心诉求是:

  1. 极高吞吐
  2. 海量日志流
  3. 流式计算和大规模消息堆积能力

很多团队会更自然地想到 Kafka。

所以 RabbitMQ 更适合的定位是:业务系统里的异步通知、事件分发、延迟处理、失败治理,而不是把所有消息场景都统一吞下来。

2. 抓住一条最核心的主线

第一次学 RabbitMQ,很容易被很多术语同时砸到。

更稳的方式是抓住一条最主干的链路:

  1. 应用先连接到 RabbitMQ Broker
  2. 生产者把消息发送到 Exchange
  3. Exchange 根据 BindingRouting Key 决定该发往哪些 Queue
  4. 消费者从 Queue 取到消息并处理
  5. 处理成功后再通过确认机制告诉 Broker 这条消息已经完成

可以把这条链路压成下面这张图:

mermaid
flowchart LR
    A[Producer] --> B[Connection]
    B --> C[Channel]
    C --> D[Exchange]
    D --> E[Binding]
    E --> F[Queue]
    F --> G[Consumer]
    G --> H[ACK / NACK]

这张图最想表达的是:

  1. 生产者通常不是直接把消息发给队列
  2. 队列也不是自己决定接不接消息
  3. RabbitMQ 之所以模型清晰,就是因为它把“连接、传输、路由、存储、消费”拆开了

3. RabbitMQ 里最核心的几个概念

3.1 Connection

Connection 可以理解成 应用和 RabbitMQ Broker 之间建立的一条 TCP 连接

它解决的是 应用怎么真正连上 RabbitMQ

在工程里,你平时不一定直接手写 Connection,但它一定存在。
在 Spring AMQP 里,更常接触到的是 ConnectionFactory

3.2 Channel

Channel 可以理解成 建立在一条 Connection 之上的逻辑通信通道

为什么 RabbitMQ 还要多一层 Channel

因为:

  1. TCP 连接建立成本高
  2. 一个应用里可能同时有很多发送和消费动作
  3. 在同一个连接上复用多个通道,比频繁新建 TCP 连接更轻量

可以把它理解成:

  1. Connection 像一条主连接
  2. Channel 像这条连接上复用出来的多条工作通路

可以直接记成:Connection 解决“能不能连上”,Channel 解决“具体通过哪条 AMQP 通道发消息、收消息、声明资源”。`

3.3 Exchange

Exchange 可以理解成 消息进入 RabbitMQ 后,第一层负责路由分发的交换机

它最重要的特点是:

  1. 生产者通常把消息发给 Exchange
  2. Exchange 再决定这条消息进入哪些队列
  3. Exchange 自己不是长期存放消息的地方

也就是说:Exchange 更像分发器,不像仓库。

3.4 Queue

Queue 就是队列本身。

它负责:

  1. 存储待消费消息
  2. 等待消费者处理
  3. 配合确认机制管理消息投递状态

如果把 RabbitMQ 里“真正堆消息”的地方找出来,通常就是队列。

3.5 Binding

Binding 可以理解成 Exchange 和 Queue 之间的绑定关系

没有绑定关系,消息就不知道该往哪里去。

这点很关键,因为它说明:消息不是因为消费者想要它,就自动进某个队列;而是因为交换机和队列之间事先定义了路由关系。

3.6 Routing Key

Routing Key 可以理解成 生产者发消息时携带的一段路由标识,用来帮助 Exchange 判断消息应该路由到哪些队列

它尤其常见于:

  1. Direct Exchange
  2. Topic Exchange

如果从业务角度理解,Routing Key 很多时候其实就是:一段带语义的事件名或消息分类标识。

例如:

  1. order.created
  2. order.paid
  3. user.registered

4. Exchange 有哪些常见类型

RabbitMQ 常见的交换机类型主要有 4 类。

4.1 Direct

Direct Exchange 的特点是:按精确匹配的 Routing Key 做路由。

例如消息携带:

text
order.created

如果某个队列绑定键也正好是:

text
order.created

它就会收到这条消息。

它最适合:

  1. 路由规则简单明确的场景
  2. 一个事件只需要投递到一个或少数几个固定队列
  3. 入门学习 RabbitMQ 最小链路

4.2 Topic

Topic Exchange 的特点是:按模式匹配 Routing Key。

常见通配符有:

  1. *:匹配一个单词
  2. #:匹配零个或多个单词

例如:

  1. order.created
  2. order.paid
  3. order.cancelled

如果某个队列绑定了:

text
order.#

那它就能接收所有订单事件。

它最适合:

  1. 事件类型较多
  2. 想把 Routing Key 设计成业务语义
  3. 想让不同队列按模式订阅不同事件

在业务系统里,Topic Exchange 往往比 Direct 更常承担事件总线的角色。

4.3 Fanout

Fanout Exchange 的特点是:忽略 Routing Key,直接把消息广播给所有已绑定队列。

它适合:

  1. 广播通知
  2. 一条消息要无差别复制给多个下游

例如:

  1. 系统刷新配置通知
  2. 某个事件需要所有订阅方都收到

4.4 Headers

Headers Exchange 是按消息头做匹配。

它没有前面几种高频,但适合某些:

  1. 路由条件较复杂
  2. 不想主要依赖 Routing Key
  3. 更想按消息属性做匹配

如果只是建立 RabbitMQ 主干认知,通常优先掌握:

  1. Direct
  2. Topic
  3. Fanout

5. 用实战场景把模型串起来

只看术语很容易散,RabbitMQ 更适合放到业务场景里理解。

5.1 下单后异步通知

一个很常见的业务场景是:

  1. 用户创建订单
  2. 订单服务把 order.created 事件发到 RabbitMQ
  3. 库存服务、积分服务、通知服务分别消费

这条链路的价值是:

  1. 下单主流程不需要同步等待所有下游
  2. 下游故障不会立刻拖死主流程
  3. 新增一个消费者时,主业务代码不用大改

如果把这条线画出来,大致像这样:

mermaid
flowchart TD
    A[订单服务创建订单] --> B[发送 order.created 到 Exchange]
    B --> C[库存队列]
    B --> D[积分队列]
    B --> E[通知队列]
    C --> F[库存服务消费]
    D --> G[积分服务消费]
    E --> H[通知服务消费]

这类场景里,RabbitMQ 解决的不是“替代数据库”,而是:把同步调用链拆成事件驱动链路。

5.2 支付超时自动取消

另一个很典型的 RabbitMQ 场景是:

  1. 订单创建后,给一条消息设置延迟处理
  2. 如果一段时间内没支付,系统自动取消订单

这类需求经常会落到:

  1. TTL
  2. 死信交换器
  3. 死信队列

也就是:

  1. 消息先进入延迟队列
  2. 到了超时时间后,消息因为过期变成死信
  3. 死信再被路由到真正执行“取消订单”的队列

这条链路非常有实战味,因为它说明:死信机制不只用于错误消息治理,也可以被拿来实现延迟处理。

5.3 消费失败后的异常治理

再看一个高频场景:

  1. 消费者拿到消息
  2. 业务处理失败
  3. 如果一直重试,会拖住主消费链路
  4. 如果直接丢弃,又无法排查和补偿

这时就需要死信队列。

所以死信队列真正要解决的是:

  1. 把异常消息和正常消息隔离
  2. 避免少量异常消息长期阻塞主队列
  3. 给重试、人工介入、补偿处理留入口

6. RabbitMQ 的可靠性要从哪里理解

工程里说 RabbitMQ 可靠性,通常不是一句“消息不会丢”就结束,而是至少要沿着 3 个阶段看:

  1. 生产者发出去时会不会丢
  2. Broker 收到后能不能保住
  3. 消费者处理时会不会重复或失败

6.1 生产者确认

生产者最常见的问题是:我发出去了,但 Broker 到底收没收到?

这时常见做法是开启 Publisher Confirm

它解决的是 生产者怎么确认消息已经真正进入 RabbitMQ

如果再细一点看,还经常会和以下问题一起出现:

  1. 交换机存在,但没有任何队列接住消息
  2. 路由失败后,生产者是否能感知

所以工程上常常会同时关注:

  1. Publisher Confirm
  2. 路由失败回调
  3. 日志与补偿机制

6.2 消费者确认

消费者处理成功后,通常要回 ACK

这样 RabbitMQ 才知道:这条消息可以从队列里真正移除了。

如果消费者处理异常退出、宕机,或者明确拒绝消息:

  1. RabbitMQ 可能重新投递
  2. 或者把消息送入死信链路

所以消费者确认这件事,本质上在解决:一条消息什么时候才算真的被业务处理完成。

6.3 持久化

如果不做持久化,Broker 重启后消息就可能丢失。

RabbitMQ 里通常要一起看:

  1. 交换机是否持久化
  2. 队列是否持久化
  3. 消息是否持久化

要注意的是:只做其中一部分持久化,不代表整条链路就可靠了。

6.4 幂等性

消息系统里,“至少一次投递”通常比“绝对只投一次”更常见。

这意味着业务上必须考虑:同一条消息重复消费怎么办。

所以真正落地时,经常需要:

  1. 业务幂等键
  2. 去重表 / 去重缓存
  3. 状态机保护

RabbitMQ 负责的是消息传递,不会自动替你解决所有业务幂等问题。

7. 死信队列、TTL、延迟消息怎么理解

7.1 死信队列是什么

很多人第一次看到“死信队列”,会以为它只是:一条专门存放失败消息的特殊队列。

这个理解不算全错,但还不够准确。RabbitMQ 里常一起出现的是:

  1. DLXDead Letter Exchange,死信交换器
  2. DLQDead Letter Queue,死信队列

这条链路直接看成:

  1. 原队列里的消息因为某些原因走不下去了
  2. RabbitMQ 把它投递到死信交换器
  3. 再由死信交换器路由到专门处理这类消息的队列

所以它的重点不是“把消息扔掉”,而是把异常消息从主链路摘出来,交给另一套治理流程。

7.2 什么情况下会进入死信链路

RabbitMQ 中常见的死信来源通常有:

  1. 消息过期
  2. 队列满了,被挤出
  3. 消费者明确拒绝,并且不重新入队

这三类场景分别代表:

  1. 时间到了,还没处理
  2. 队列承压到边界
  3. 消费者明确判断这条消息不该继续在主链路里重试

7.3 TTL 是什么

TTLTime To Live,可以理解成 消息或队列的生存时间

超过这个时间后,消息可能过期。

如果队列配置了死信交换器,这条过期消息就可能继续进入死信链路。

所以很多人说:RabbitMQ 用 TTL + DLX 做延迟消息

真正的意思通常是:

  1. 消息先在一个带 TTL 的队列里待一段时间
  2. 到期后变成死信
  3. 再被路由到真正执行业务的队列

7.4 延迟消息到底是怎么落地的

RabbitMQ 里常见延迟消息方案主要有两类:

  1. TTL + 死信队列
  2. 插件方案

其中第一种最经典,也最适合建立理解。

例如“订单 30 分钟未支付自动取消”的主线通常是:

  1. 创建订单时,发送一条延迟处理消息
  2. 消息先进入带 TTL 的延迟队列
  3. 到期后通过死信交换器转发
  4. 最后进入真正的超时处理队列
  5. 消费者执行取消订单逻辑

可以把这条链路画成下面这样:

mermaid
flowchart TD
    A[订单创建] --> B[发送超时检查消息]
    B --> C[延迟队列 TTL 等待]
    C --> D[消息过期]
    D --> E[死信交换器 DLX]
    E --> F[超时处理队列]
    F --> G[取消未支付订单]

这张图真正想表达的是:延迟消息并不是队列自己会“定时执行”,而是消息先等,到点后再转到真正消费队列。

7.5 死信队列和重试是什么关系

这两个词经常一起出现,但不是同一个东西。

可以直接这样区分:

  1. 重试:重点是“再试一次”
  2. 死信队列:重点是“别继续在主链路里打转了,先转移出去”

对应到工程判断上:

  1. 短暂失败、可恢复错误,通常先考虑有限次重试
  2. 明显异常、长期失败、格式错误、业务状态不合法的消息,更适合进入死信链路

要注意的是:死信队列不是为了替代重试,而是为了给“不该继续在主链路里重试”的消息找出口。

8. 在 Java / Spring 项目里,RabbitMQ 一般怎么接

如果你是从工程角度学 RabbitMQ,更适合按下面这条集成主线理解:

  1. 引入 AMQP 依赖
  2. 配置 Broker 连接信息
  3. 声明交换机、队列、绑定
  4. 配置消息转换器
  5. 生产者通过 RabbitTemplate 发消息
  6. 消费者通过 @RabbitListener 监听队列
  7. 结合确认、重试、死信和幂等把链路补完整

8.1 生产者侧通常长什么样

在 Spring AMQP 里,生产者常见核心工具就是:

  1. RabbitTemplate
  2. JSON 消息转换器

它的职责通常包括:

  1. 组装消息载荷
  2. 指定目标交换机
  3. 指定 Routing Key
  4. 发送消息并记录结果

更贴近工程现场地看:

生产者 是角色,不一定是类名。
只要某段代码在向 RabbitMQ 发消息,它就在扮演生产者。

8.1.1 一个最小生产者示例

下面这段代码演示一个更贴近业务的生产者写法。

场景假设是:

  1. 订单创建成功
  2. 服务层准备发布一条 order.created 事件
  3. 通知下游库存、通知、审计等系统继续处理
java
/**
 * 订单事件生产者,负责把业务状态变化发布到 RabbitMQ。
 */
@Service
public class OrderEventProducer {

    private final RabbitTemplate rabbitTemplate;

    /**
     * 注入 RabbitMQ 发送模板。
     *
     * @param rabbitTemplate 用于发送 AMQP 消息的模板对象
     */
    public OrderEventProducer(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * publishOrderCreated
     * 功能:发布订单创建事件到 RabbitMQ。
     * 核心职责:
     * 1. 组装要发送的消息对象
     * 2. 指定目标交换机
     * 3. 指定 routing key
     * 4. 调用 RabbitTemplate 把消息发送出去
     * 参数:
     * - orderId: 订单 ID
     * - userId: 用户 ID
     * - amount: 订单金额
     * 返回值:
     * - 无显式返回值,副作用是向 RabbitMQ 发送一条 order.created 消息
     */
    public void publishOrderCreated(String orderId, Long userId, BigDecimal amount) {
        OrderCreatedEvent event = OrderCreatedEvent.builder()
            .orderId(orderId)
            .userId(userId)
            .amount(amount)
            .createdAt(LocalDateTime.now())
            .build();

        rabbitTemplate.convertAndSend(
            "order.event.exchange",
            "order.created",
            event
        );
    }
}

这段代码里最关键的 3 个点是:

  1. "order.event.exchange":目标交换机,决定消息先进入哪一层路由入口
  2. "order.created"Routing Key,决定交换机后续怎么分发
  3. event:真正的消息载荷,通常是一个业务事件对象

如果把它翻译成一句更口语化的话,就是:订单服务把“订单创建完成”这件事,作为一条业务事件发给 RabbitMQ。

8.1.2 消息对象通常长什么样

生产者代码里除了 RabbitTemplate,另一个很常见的部分就是消息对象本身。

例如上面那条消息,可以配一个类似这样的事件类:

java
/**
 * 订单创建事件的消息载体。
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderCreatedEvent {

    private String orderId;
    private Long userId;
    private BigDecimal amount;
    private LocalDateTime createdAt;
}

它的意义不是“为了面向对象而面向对象”,而是:

  1. 把消息载荷结构固定下来
  2. 让生产者和消费者围绕同一份事件模型协作
  3. 方便配合 JSON 消息转换器自动序列化和反序列化

8.1.3 这段生产者代码在工程里真正说明了什么

这类生产者代码真正想表达的,不只是“怎么调一次 API”,而是:

  1. 业务系统里,发送消息通常发生在服务层
  2. 发送的不是随便一段字符串,而是一条带业务语义的事件
  3. 生产者最少要想清楚 3 件事:
    • 发到哪个交换机
    • 用什么 Routing Key
    • 消息体里到底放什么

把这一层压缩成一句话:生产者代码的本质,是把业务状态变化翻译成一条可路由、可消费、可追踪的消息事件。

8.1.4 Spring 实战里,绑定关系一般怎么写

如果只看前面的生产者代码,很多人会继续追问:

  1. 生产者明明发的是 order.event.exchange
  2. 路由键明明是 order.created
  3. 为什么最后消费者监听的是 order.notify.queue

中间真正起作用的,就是 Binding

也就是说,完整链路不是:Producer -> Consumer

而是:Producer -> Exchange -> Binding -> Queue -> Consumer

在 Spring AMQP 里,这层关系通常会在配置类里声明出来,例如:

java
/**
 * 声明订单事件交换机、队列和绑定关系的配置类。
 */
@Configuration
public class OrderRabbitMqConfig {

    /**
     * orderEventExchange
     * 功能:声明订单事件交换机。
     * 核心职责:
     * 1. 作为订单类事件进入 RabbitMQ 的统一入口
     * 2. 让后续队列可以基于 Routing Key 建立绑定关系
     * 返回值:
     * - TopicExchange: 名称为 order.event.exchange 的主题交换机
     */
    @Bean
    public TopicExchange orderEventExchange() {
        return new TopicExchange("order.event.exchange", true, false);
    }

    /**
     * orderNotifyQueue
     * 功能:声明订单通知队列。
     * 核心职责:
     * 1. 承接订单创建后需要发送通知的消息
     * 2. 为消费者提供稳定的监听目标
     * 返回值:
     * - Queue: 名称为 order.notify.queue 的持久化队列
     */
    @Bean
    public Queue orderNotifyQueue() {
        return QueueBuilder
            .durable("order.notify.queue")
            .build();
    }

    /**
     * orderNotifyBinding
     * 功能:声明订单事件交换机与通知队列之间的绑定关系。
     * 核心职责:
     * 1. 把 order.notify.queue 绑定到 order.event.exchange
     * 2. 指定只有 Routing Key 为 order.created 的消息才会路由到该队列
     * 参数:
     * - orderNotifyQueue: 订单通知队列 Bean
     * - orderEventExchange: 订单事件交换机 Bean
     * 返回值:
     * - Binding: 当前队列与交换机之间基于 order.created 建立的绑定关系
     */
    @Bean
    public Binding orderNotifyBinding(Queue orderNotifyQueue, TopicExchange orderEventExchange) {
        return BindingBuilder
            .bind(orderNotifyQueue)
            .to(orderEventExchange)
            .with("order.created");
    }
}

这段代码里最关键的 3 个点是:

  1. orderEventExchange() 声明消息先进入的交换机
  2. orderNotifyQueue() 声明真正承接消息的队列
  3. orderNotifyBinding(...) 声明:
    • 当消息进入 order.event.exchange
    • Routing Keyorder.created
    • 就把它路由到 order.notify.queue

如果把这条路由关系翻译成一句话,就是:order.event.exchange --(order.created)--> order.notify.queue

8.1.5 为什么 order.created 最后能被 order.notify.queue 消费

现在把生产者、绑定关系和消费者放到一起看,就更容易明白了。

生产者代码里:

  1. 指定交换机:order.event.exchange
  2. 指定路由键:order.created

绑定代码里:

  1. order.notify.queue 绑定到 order.event.exchange
  2. 绑定键也是 order.created

消费者代码里:

  1. @RabbitListener 监听 order.notify.queue

于是完整链路就是:

text
订单服务发送消息
-> order.event.exchange
-> Binding 匹配 order.created
-> order.notify.queue
-> OrderNotifyConsumer 消费

这也是为什么要特别记住一件事:生产者不是直接把消息发给消费者,而是先发给 Exchange,再由 Exchange 根据 Binding 和 Routing Key 把消息送到消费者监听的 Queue。

8.2 消费者侧通常长什么样

消费者最常见的写法就是:

java
/**
 * 订单通知消费者,负责处理 `order.created` 事件。
 */
@Service
public class OrderNotifyConsumer {

    private final NotificationService notificationService;

    /**
     * 注入通知业务服务。
     *
     * @param notificationService 真正执行业务通知的服务
     */
    public OrderNotifyConsumer(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    /**
     * handleOrderCreated
     * 功能:监听订单创建队列,并在收到消息后执行通知逻辑。
     * 核心职责:
     * 1. 从指定队列中接收 OrderCreatedEvent
     * 2. 调用业务服务执行真正的通知处理
     * 3. 在不可恢复错误时明确拒绝消息,让它进入死信链路
     * 参数:
     * - event: 从 RabbitMQ 反序列化得到的订单创建事件
     * 返回值:
     * - 无显式返回值,副作用是执行通知逻辑并影响消息确认结果
     */
    @RabbitListener(
        queues = "order.notify.queue",
        concurrency = "2-4"
    )
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            notificationService.sendOrderCreatedNotice(
                event.getOrderId(),
                event.getUserId(),
                event.getAmount()
            );
        } catch (IllegalArgumentException ex) {
            // 这类错误通常代表消息内容本身有问题,不适合在主链路里无限重试
            throw new AmqpRejectAndDontRequeueException("订单通知消息格式非法", ex);
        }
    }
}

它通常要关心:

  1. 监听哪个队列
  2. 收到消息后做什么业务处理
  3. 处理失败时是重试、拒绝,还是送入死信

把上面注解里的两个参数拆开看,它们分别在表达:

  1. queues = "order.notify.queue"

    • 表示当前这个消费者方法要监听的目标队列
    • 也就是说,只有进入 order.notify.queue 的消息,才会被这个方法接住
    • 它回答的是: 这个消费者到底在消费哪条队列。
  2. concurrency = "2-4"

    • 表示监听容器的并发消费者数量范围
    • 这里可以理解成:
      • 最少维持 2 个并发消费者
      • 忙的时候最多扩到 4 个并发消费者
    • 它回答的是: 这条队列允许多少个消费者同时并发处理消息。

把这两个参数压缩成一句话:

  1. queues 决定“消费谁”
  2. concurrency 决定“几个人一起消费”

同样地:

消费者 也是角色,不一定有一个固定叫 Consumer 的类。

8.3 消息转换器为什么重要

实际项目里,消息通常不是一段原始字符串,而是对象。

这时就经常要配置消息转换器,例如 JSON 转换器。

它解决的是:

  1. 生产者把 Java 对象序列化为消息体
  2. 消费者再把消息体反序列化回对象

没有这层能力,业务代码里会充满手工 JSON 转换逻辑。

8.4 一个更贴近实战的接入顺序

如果你自己要在项目里接 RabbitMQ,建议按下面顺序思考:

  1. 先定义业务事件是什么
  2. 再定义交换机类型和 Routing Key 规则
  3. 再定义需要哪些队列
  4. 再补确认、重试、死信、幂等
  5. 最后再看监控和告警

放到工程设计里看,RabbitMQ 不是“先建几个队列再看业务怎么塞进去”,而更适合 先按业务事件和失败治理设计消息主线,再把交换机、队列和绑定配置出来

9. 一条更贴近实战的订单主线

前面已经把 RabbitMQ 的核心模型、可靠性、死信、TTL、延迟消息,以及在 Spring 项目中的接入方式拆开讲过了。

到这里再回头看一个综合场景,会更容易理解:RabbitMQ 在复杂业务里,真正承接的往往不是“一个动作”,而是一整条异步扩散和异常治理链路。

订单类业务就是最典型的例子。

9.1 为什么订单类场景更适合放在后面看

实际业务里的订单流程,通常不会只是:创建订单 -> 发一条消息 -> 消费结束。

更常见的真实情况是:

  1. 创建订单
  2. 等待支付
  3. 支付成功 / 支付失败 / 支付超时
  4. 支付成功后继续扣减库存、发通知、加积分、走履约链路
  5. 任一环节失败后,还要考虑重试、补偿和人工介入

所以在工程里,更稳的思路通常不是“让一条消息把所有事情串完”,而是:主链路把订单核心状态推进正确,再通过 RabbitMQ 把后续影响扩散给各个下游系统。

9.2 先有状态机,再有消息链路

订单类业务最重要的基础,通常不是 MQ,而是状态机。

例如一个简化版订单状态流转,常见会有:

  1. WAIT_PAY:已创建,等待支付
  2. PAID:支付成功
  3. PAY_FAILED:支付失败
  4. CLOSED:超时关闭或人工关闭
  5. FINISHED:履约完成

要注意的是:RabbitMQ 负责传播事件,但订单状态本身必须先在核心系统里可控、可校验、可防并发冲突。

也就是说:

  1. 订单是否能从 WAIT_PAY 进入 PAID
  2. 订单是否还能从 WAIT_PAY 进入 CLOSED
  3. 支付成功回调和超时关单是否发生竞争

这些问题首先是状态机问题,然后才是消息问题。

9.3 一条更贴近实战的订单主线

把流程压缩一下,比较常见的一条线是:

  1. 用户创建订单
  2. 订单服务落库,状态置为 WAIT_PAY
  3. 发送 order.created 事件
  4. 同时安排一条延迟关闭订单的消息
  5. 用户发起支付
  6. 支付系统异步回调支付结果
  7. 如果支付成功,订单状态推进为 PAID
  8. 再发送 order.paid 事件
  9. 库存、积分、通知、履约等下游分别消费
  10. 如果一直没支付,延迟消息到期后触发关单
  11. 订单状态从 WAIT_PAY 进入 CLOSED
  12. 再发送 order.closedorder.timeout_closed 事件

可以把这条主线看成下面这张图:

mermaid
flowchart TD
    A[创建订单] --> B[订单落库 状态 WAIT_PAY]
    B --> C[发送 order.created]
    B --> D[发送延迟关单消息]

    C --> E[通知服务]
    C --> F[审计服务]
    C --> G[营销/风控服务]

    H[支付平台回调成功] --> I[订单状态更新为 PAID]
    I --> J[发送 order.paid]
    J --> K[库存服务扣减或确认库存]
    J --> L[积分服务]
    J --> M[履约/发货服务]
    J --> N[用户通知服务]

    D --> O[TTL 到期 / 延迟消息触发]
    O --> P{订单仍是 WAIT_PAY 吗}
    P -- 是 --> Q[订单状态更新为 CLOSED]
    Q --> R[发送 order.closed]
    P -- 否 --> S[忽略关单]

这张图最重要的意思是:

  1. 创建订单和支付成功是两次不同的业务推进
  2. 支付成功后的下游链路通常是异步扩散出去的
  3. 超时关单是一条独立的延迟治理链路

9.4 创建订单时,RabbitMQ 一般负责什么

用户提交订单后,订单服务通常会先做这些动作:

  1. 校验商品、价格、优惠、库存等基本信息
  2. 创建订单记录
  3. 把订单状态置为 WAIT_PAY
  4. 记录支付超时时间
  5. 发送 order.created 事件

这里最关键的边界是:创建订单本身通常属于主链路事务,MQ 更常负责把“订单已创建”这件事通知给下游。

也就是说,RabbitMQ 更适合做这些事:

  1. 通知库存系统做预处理
  2. 通知营销系统记录活动参与
  3. 通知审计系统记录事件流
  4. 通知消息系统给用户发提醒

但“订单到底有没有创建成功”这件事,通常还是以订单库落地结果为准。

9.5 等待支付和超时关闭一般怎么配合

创建订单后,系统通常不会阻塞等待支付,而是:

  1. 前端拿到订单号和支付单信息
  2. 用户进入支付流程
  3. 系统后台同时安排“超时关单”这条延迟链路

RabbitMQ 在这里的常见做法就是:

  1. 把一条消息先放进带 TTL 的延迟队列
  2. 到期后通过死信交换器转到真正的关单队列
  3. 关单消费者收到消息后,再检查订单状态

要注意的是:超时消息到了,不代表一定要关单,而是要先判断订单当前是不是仍然处于 WAIT_PAY。

因为支付成功回调和超时关单,可能刚好在时间上发生竞争。

9.6 支付成功后为什么常常再发一轮事件

支付成功并不等于所有后续流程也同步做完。

更常见的做法是:

  1. 支付系统回调成功
  2. 支付服务验签并做幂等校验
  3. 订单状态推进到 PAID
  4. 再发送 order.paid 事件

然后由下游分别消费:

  1. 库存服务:扣减库存或确认预占库存
  2. 积分服务:发放积分
  3. 履约服务:生成发货/履约任务
  4. 通知服务:给用户发支付成功通知
  5. 审计服务:记录支付事件

这里 RabbitMQ 的价值就在于:支付成功后的扩散链路不需要全部同步挂在支付回调接口上。

这样做的好处通常包括:

  1. 支付回调主链路更短
  2. 下游新增能力时更容易扩展
  3. 某个下游失败,不会直接拖死整个支付主流程

9.7 库存一般是在什么时候扣

这个问题在实战里非常常见,而且没有唯一答案。

常见做法通常有两种:

  1. 创建订单时预占库存,支付成功后确认占用
  2. 支付成功后再真正扣减库存

第一种更适合:

  1. 热门商品
  2. 库存紧张
  3. 更关注防超卖

第二种更适合:

  1. 超卖风险较低
  2. 库存模型没那么强约束
  3. 更强调流程简单

如果用 RabbitMQ 把这件事放进消息链路里理解,核心不是“扣库存到底同步还是异步”,而是:

库存动作本身要和订单状态推进、支付结果、超时关闭这几条线一起设计。

9.8 这种链路里最难的通常不是发消息

真正难的地方,通常集中在这几件事:

  1. 幂等

    • 支付回调可能重复
    • MQ 消息可能重复消费
    • 下游服务也可能重复收到事件
  2. 并发竞争

    • 支付成功和超时关单可能同时发生
    • 重复点击支付和异步回调也可能交织
  3. 补偿

    • 支付成功了,但库存扣减失败怎么办
    • 通知失败了,是否重试或进入死信
    • 履约失败后是否退款或人工介入
  4. 状态可追踪

    • 当前订单到底走到哪一步了
    • 某次失败是主链路失败还是下游异步失败

所以这里真正该盯住的是:RabbitMQ 解决的是异步解耦和事件扩散问题,但订单这类复杂业务最终能不能稳,关键还在状态机、幂等、补偿和异常治理。

9.9 如果只记一条实战经验

可以记住这句:订单主流程负责把核心状态推进正确,RabbitMQ 负责把支付、库存、通知、履约等后续影响异步扩散出去,再通过延迟消息、死信队列和补偿机制把异常链路补完整。

10. RabbitMQ 更适合什么,不适合什么

更适合

  1. 业务事件通知
  2. 异步解耦
  3. 路由规则较灵活的业务系统
  4. 延迟处理和异常治理
  5. 中小到中大型业务消息系统

不那么适合

  1. 极高吞吐日志流场景
  2. 超大规模流式分析场景
  3. 更强调分布式日志存储能力的场景

如果系统核心目标是:海量日志流 + 高吞吐 + 长时间保留 + 流处理

RabbitMQ 通常不是第一选择。

11. 工程里最容易踩的坑

11.1 以为生产者是直接把消息发到队列

RabbitMQ 最关键的一层恰恰是交换机模型。

11.2 只会配队列,不会设计 Routing Key

一旦事件多起来,路由会很快失控。

11.3 只关心发消息,不关心确认和失败治理

这样系统在异常情况下会很脆。

11.4 把死信队列理解成“失败消息垃圾桶”

实际上它更像异常治理链路的入口。

11.5 忽略业务幂等

消息系统最常见的问题之一不是“收不到”,而是“收到两次怎么办”。

12. 小结

RabbitMQ 最值得记住的,不是某个孤立术语,而是下面这条主线:

生产者通过 Connection / Channel 把消息送到 Exchange,Exchange 再根据 Binding 和 Routing Key 把消息路由到 Queue,消费者处理后再通过确认、重试、死信和幂等机制把整条业务链路补完整。

如果把它再压成一句话,可以记成:

RabbitMQ 不是简单的“放消息的队列”,而是一套围绕连接、通道、交换机、路由、确认和异常治理构建起来的业务消息模型。

基于 VitePress 构建的个人技术笔记。