Appearance
RabbitMQ
这篇笔记聚焦 RabbitMQ 这个具体产品,但不会只停留在“名词解释”层面,而是尽量沿着更接近工程落地的主线来讲:
- 消息是怎么从生产者进入 RabbitMQ 的
- RabbitMQ 为什么要拆成
Connection、Channel、Exchange、Queue、Binding、Routing Key - 为什么有的场景适合
Direct,有的更适合Topic - 可靠性、死信队列、
TTL、延迟消息到底在解决什么问题 - 在 Java / Spring 项目里,RabbitMQ 一般是怎么接入业务系统的
如果你还没建立消息队列这条知识线,可以看:
这篇更强调 RabbitMQ 自己的模型、边界,以及它在真实业务里的常见用法。
1. RabbitMQ 是什么
RabbitMQ 是一个基于 AMQP 协议体系发展起来的消息队列产品。
这里的 AMQP 可以看成 一套面向消息中间件的协议和模型规范。
所以 RabbitMQ 的特点,不只是“能把消息放进去再取出来”,而是把消息怎么进入、怎么路由、怎么投递、怎么确认,拆成了一套比较清晰的模型。
它最常被提到的优势通常有:
- 消息模型清晰
- 路由规则灵活
- 生态成熟
- 比较适合业务消息、事件通知、异步解耦这类场景
但它也不是所有场景都最优。
如果你的核心诉求是:
- 极高吞吐
- 海量日志流
- 流式计算和大规模消息堆积能力
很多团队会更自然地想到 Kafka。
所以 RabbitMQ 更适合的定位是:业务系统里的异步通知、事件分发、延迟处理、失败治理,而不是把所有消息场景都统一吞下来。
2. 抓住一条最核心的主线
第一次学 RabbitMQ,很容易被很多术语同时砸到。
更稳的方式是抓住一条最主干的链路:
- 应用先连接到 RabbitMQ Broker
- 生产者把消息发送到
Exchange Exchange根据Binding和Routing Key决定该发往哪些Queue- 消费者从
Queue取到消息并处理 - 处理成功后再通过确认机制告诉 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]这张图最想表达的是:
- 生产者通常不是直接把消息发给队列
- 队列也不是自己决定接不接消息
- RabbitMQ 之所以模型清晰,就是因为它把“连接、传输、路由、存储、消费”拆开了
3. RabbitMQ 里最核心的几个概念
3.1 Connection
Connection 可以理解成 应用和 RabbitMQ Broker 之间建立的一条 TCP 连接。
它解决的是 应用怎么真正连上 RabbitMQ。
在工程里,你平时不一定直接手写 Connection,但它一定存在。
在 Spring AMQP 里,更常接触到的是 ConnectionFactory。
3.2 Channel
Channel 可以理解成 建立在一条 Connection 之上的逻辑通信通道。
为什么 RabbitMQ 还要多一层 Channel?
因为:
- TCP 连接建立成本高
- 一个应用里可能同时有很多发送和消费动作
- 在同一个连接上复用多个通道,比频繁新建 TCP 连接更轻量
可以把它理解成:
Connection像一条主连接Channel像这条连接上复用出来的多条工作通路
可以直接记成:Connection 解决“能不能连上”,Channel 解决“具体通过哪条 AMQP 通道发消息、收消息、声明资源”。`
3.3 Exchange
Exchange 可以理解成 消息进入 RabbitMQ 后,第一层负责路由分发的交换机。
它最重要的特点是:
- 生产者通常把消息发给
Exchange Exchange再决定这条消息进入哪些队列Exchange自己不是长期存放消息的地方
也就是说:Exchange 更像分发器,不像仓库。
3.4 Queue
Queue 就是队列本身。
它负责:
- 存储待消费消息
- 等待消费者处理
- 配合确认机制管理消息投递状态
如果把 RabbitMQ 里“真正堆消息”的地方找出来,通常就是队列。
3.5 Binding
Binding 可以理解成 Exchange 和 Queue 之间的绑定关系。
没有绑定关系,消息就不知道该往哪里去。
这点很关键,因为它说明:消息不是因为消费者想要它,就自动进某个队列;而是因为交换机和队列之间事先定义了路由关系。
3.6 Routing Key
Routing Key 可以理解成 生产者发消息时携带的一段路由标识,用来帮助 Exchange 判断消息应该路由到哪些队列。
它尤其常见于:
Direct ExchangeTopic Exchange
如果从业务角度理解,Routing Key 很多时候其实就是:一段带语义的事件名或消息分类标识。
例如:
order.createdorder.paiduser.registered
4. Exchange 有哪些常见类型
RabbitMQ 常见的交换机类型主要有 4 类。
4.1 Direct
Direct Exchange 的特点是:按精确匹配的 Routing Key 做路由。
例如消息携带:
text
order.created如果某个队列绑定键也正好是:
text
order.created它就会收到这条消息。
它最适合:
- 路由规则简单明确的场景
- 一个事件只需要投递到一个或少数几个固定队列
- 入门学习 RabbitMQ 最小链路
4.2 Topic
Topic Exchange 的特点是:按模式匹配 Routing Key。
常见通配符有:
*:匹配一个单词#:匹配零个或多个单词
例如:
order.createdorder.paidorder.cancelled
如果某个队列绑定了:
text
order.#那它就能接收所有订单事件。
它最适合:
- 事件类型较多
- 想把 Routing Key 设计成业务语义
- 想让不同队列按模式订阅不同事件
在业务系统里,Topic Exchange 往往比 Direct 更常承担事件总线的角色。
4.3 Fanout
Fanout Exchange 的特点是:忽略 Routing Key,直接把消息广播给所有已绑定队列。
它适合:
- 广播通知
- 一条消息要无差别复制给多个下游
例如:
- 系统刷新配置通知
- 某个事件需要所有订阅方都收到
4.4 Headers
Headers Exchange 是按消息头做匹配。
它没有前面几种高频,但适合某些:
- 路由条件较复杂
- 不想主要依赖 Routing Key
- 更想按消息属性做匹配
如果只是建立 RabbitMQ 主干认知,通常优先掌握:
DirectTopicFanout
5. 用实战场景把模型串起来
只看术语很容易散,RabbitMQ 更适合放到业务场景里理解。
5.1 下单后异步通知
一个很常见的业务场景是:
- 用户创建订单
- 订单服务把
order.created事件发到 RabbitMQ - 库存服务、积分服务、通知服务分别消费
这条链路的价值是:
- 下单主流程不需要同步等待所有下游
- 下游故障不会立刻拖死主流程
- 新增一个消费者时,主业务代码不用大改
如果把这条线画出来,大致像这样:
mermaid
flowchart TD
A[订单服务创建订单] --> B[发送 order.created 到 Exchange]
B --> C[库存队列]
B --> D[积分队列]
B --> E[通知队列]
C --> F[库存服务消费]
D --> G[积分服务消费]
E --> H[通知服务消费]这类场景里,RabbitMQ 解决的不是“替代数据库”,而是:把同步调用链拆成事件驱动链路。
5.2 支付超时自动取消
另一个很典型的 RabbitMQ 场景是:
- 订单创建后,给一条消息设置延迟处理
- 如果一段时间内没支付,系统自动取消订单
这类需求经常会落到:
TTL- 死信交换器
- 死信队列
也就是:
- 消息先进入延迟队列
- 到了超时时间后,消息因为过期变成死信
- 死信再被路由到真正执行“取消订单”的队列
这条链路非常有实战味,因为它说明:死信机制不只用于错误消息治理,也可以被拿来实现延迟处理。
5.3 消费失败后的异常治理
再看一个高频场景:
- 消费者拿到消息
- 业务处理失败
- 如果一直重试,会拖住主消费链路
- 如果直接丢弃,又无法排查和补偿
这时就需要死信队列。
所以死信队列真正要解决的是:
- 把异常消息和正常消息隔离
- 避免少量异常消息长期阻塞主队列
- 给重试、人工介入、补偿处理留入口
6. RabbitMQ 的可靠性要从哪里理解
工程里说 RabbitMQ 可靠性,通常不是一句“消息不会丢”就结束,而是至少要沿着 3 个阶段看:
- 生产者发出去时会不会丢
- Broker 收到后能不能保住
- 消费者处理时会不会重复或失败
6.1 生产者确认
生产者最常见的问题是:我发出去了,但 Broker 到底收没收到?
这时常见做法是开启 Publisher Confirm。
它解决的是 生产者怎么确认消息已经真正进入 RabbitMQ。
如果再细一点看,还经常会和以下问题一起出现:
- 交换机存在,但没有任何队列接住消息
- 路由失败后,生产者是否能感知
所以工程上常常会同时关注:
Publisher Confirm- 路由失败回调
- 日志与补偿机制
6.2 消费者确认
消费者处理成功后,通常要回 ACK。
这样 RabbitMQ 才知道:这条消息可以从队列里真正移除了。
如果消费者处理异常退出、宕机,或者明确拒绝消息:
- RabbitMQ 可能重新投递
- 或者把消息送入死信链路
所以消费者确认这件事,本质上在解决:一条消息什么时候才算真的被业务处理完成。
6.3 持久化
如果不做持久化,Broker 重启后消息就可能丢失。
RabbitMQ 里通常要一起看:
- 交换机是否持久化
- 队列是否持久化
- 消息是否持久化
要注意的是:只做其中一部分持久化,不代表整条链路就可靠了。
6.4 幂等性
消息系统里,“至少一次投递”通常比“绝对只投一次”更常见。
这意味着业务上必须考虑:同一条消息重复消费怎么办。
所以真正落地时,经常需要:
- 业务幂等键
- 去重表 / 去重缓存
- 状态机保护
RabbitMQ 负责的是消息传递,不会自动替你解决所有业务幂等问题。
7. 死信队列、TTL、延迟消息怎么理解
7.1 死信队列是什么
很多人第一次看到“死信队列”,会以为它只是:一条专门存放失败消息的特殊队列。
这个理解不算全错,但还不够准确。RabbitMQ 里常一起出现的是:
DLX:Dead Letter Exchange,死信交换器DLQ:Dead Letter Queue,死信队列
这条链路直接看成:
- 原队列里的消息因为某些原因走不下去了
- RabbitMQ 把它投递到死信交换器
- 再由死信交换器路由到专门处理这类消息的队列
所以它的重点不是“把消息扔掉”,而是把异常消息从主链路摘出来,交给另一套治理流程。
7.2 什么情况下会进入死信链路
RabbitMQ 中常见的死信来源通常有:
- 消息过期
- 队列满了,被挤出
- 消费者明确拒绝,并且不重新入队
这三类场景分别代表:
- 时间到了,还没处理
- 队列承压到边界
- 消费者明确判断这条消息不该继续在主链路里重试
7.3 TTL 是什么
TTL 是 Time To Live,可以理解成 消息或队列的生存时间。
超过这个时间后,消息可能过期。
如果队列配置了死信交换器,这条过期消息就可能继续进入死信链路。
所以很多人说:RabbitMQ 用 TTL + DLX 做延迟消息
真正的意思通常是:
- 消息先在一个带 TTL 的队列里待一段时间
- 到期后变成死信
- 再被路由到真正执行业务的队列
7.4 延迟消息到底是怎么落地的
RabbitMQ 里常见延迟消息方案主要有两类:
TTL + 死信队列- 插件方案
其中第一种最经典,也最适合建立理解。
例如“订单 30 分钟未支付自动取消”的主线通常是:
- 创建订单时,发送一条延迟处理消息
- 消息先进入带
TTL的延迟队列 - 到期后通过死信交换器转发
- 最后进入真正的超时处理队列
- 消费者执行取消订单逻辑
可以把这条链路画成下面这样:
mermaid
flowchart TD
A[订单创建] --> B[发送超时检查消息]
B --> C[延迟队列 TTL 等待]
C --> D[消息过期]
D --> E[死信交换器 DLX]
E --> F[超时处理队列]
F --> G[取消未支付订单]这张图真正想表达的是:延迟消息并不是队列自己会“定时执行”,而是消息先等,到点后再转到真正消费队列。
7.5 死信队列和重试是什么关系
这两个词经常一起出现,但不是同一个东西。
可以直接这样区分:
- 重试:重点是“再试一次”
- 死信队列:重点是“别继续在主链路里打转了,先转移出去”
对应到工程判断上:
- 短暂失败、可恢复错误,通常先考虑有限次重试
- 明显异常、长期失败、格式错误、业务状态不合法的消息,更适合进入死信链路
要注意的是:死信队列不是为了替代重试,而是为了给“不该继续在主链路里重试”的消息找出口。
8. 在 Java / Spring 项目里,RabbitMQ 一般怎么接
如果你是从工程角度学 RabbitMQ,更适合按下面这条集成主线理解:
- 引入 AMQP 依赖
- 配置 Broker 连接信息
- 声明交换机、队列、绑定
- 配置消息转换器
- 生产者通过
RabbitTemplate发消息 - 消费者通过
@RabbitListener监听队列 - 结合确认、重试、死信和幂等把链路补完整
8.1 生产者侧通常长什么样
在 Spring AMQP 里,生产者常见核心工具就是:
RabbitTemplate- JSON 消息转换器
它的职责通常包括:
- 组装消息载荷
- 指定目标交换机
- 指定
Routing Key - 发送消息并记录结果
更贴近工程现场地看:
生产者 是角色,不一定是类名。
只要某段代码在向 RabbitMQ 发消息,它就在扮演生产者。
8.1.1 一个最小生产者示例
下面这段代码演示一个更贴近业务的生产者写法。
场景假设是:
- 订单创建成功
- 服务层准备发布一条
order.created事件 - 通知下游库存、通知、审计等系统继续处理
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 个点是:
"order.event.exchange":目标交换机,决定消息先进入哪一层路由入口"order.created":Routing Key,决定交换机后续怎么分发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;
}它的意义不是“为了面向对象而面向对象”,而是:
- 把消息载荷结构固定下来
- 让生产者和消费者围绕同一份事件模型协作
- 方便配合 JSON 消息转换器自动序列化和反序列化
8.1.3 这段生产者代码在工程里真正说明了什么
这类生产者代码真正想表达的,不只是“怎么调一次 API”,而是:
- 业务系统里,发送消息通常发生在服务层
- 发送的不是随便一段字符串,而是一条带业务语义的事件
- 生产者最少要想清楚 3 件事:
- 发到哪个交换机
- 用什么
Routing Key - 消息体里到底放什么
把这一层压缩成一句话:生产者代码的本质,是把业务状态变化翻译成一条可路由、可消费、可追踪的消息事件。
8.1.4 Spring 实战里,绑定关系一般怎么写
如果只看前面的生产者代码,很多人会继续追问:
- 生产者明明发的是
order.event.exchange - 路由键明明是
order.created - 为什么最后消费者监听的是
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 个点是:
orderEventExchange()声明消息先进入的交换机orderNotifyQueue()声明真正承接消息的队列orderNotifyBinding(...)声明:- 当消息进入
order.event.exchange - 且
Routing Key为order.created - 就把它路由到
order.notify.queue
- 当消息进入
如果把这条路由关系翻译成一句话,就是:order.event.exchange --(order.created)--> order.notify.queue
8.1.5 为什么 order.created 最后能被 order.notify.queue 消费
现在把生产者、绑定关系和消费者放到一起看,就更容易明白了。
生产者代码里:
- 指定交换机:
order.event.exchange - 指定路由键:
order.created
绑定代码里:
- 把
order.notify.queue绑定到order.event.exchange - 绑定键也是
order.created
消费者代码里:
@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);
}
}
}它通常要关心:
- 监听哪个队列
- 收到消息后做什么业务处理
- 处理失败时是重试、拒绝,还是送入死信
把上面注解里的两个参数拆开看,它们分别在表达:
queues = "order.notify.queue"- 表示当前这个消费者方法要监听的目标队列
- 也就是说,只有进入
order.notify.queue的消息,才会被这个方法接住 - 它回答的是:
这个消费者到底在消费哪条队列。
concurrency = "2-4"- 表示监听容器的并发消费者数量范围
- 这里可以理解成:
- 最少维持
2个并发消费者 - 忙的时候最多扩到
4个并发消费者
- 最少维持
- 它回答的是:
这条队列允许多少个消费者同时并发处理消息。
把这两个参数压缩成一句话:
queues决定“消费谁”concurrency决定“几个人一起消费”
同样地:
消费者 也是角色,不一定有一个固定叫 Consumer 的类。
8.3 消息转换器为什么重要
实际项目里,消息通常不是一段原始字符串,而是对象。
这时就经常要配置消息转换器,例如 JSON 转换器。
它解决的是:
- 生产者把 Java 对象序列化为消息体
- 消费者再把消息体反序列化回对象
没有这层能力,业务代码里会充满手工 JSON 转换逻辑。
8.4 一个更贴近实战的接入顺序
如果你自己要在项目里接 RabbitMQ,建议按下面顺序思考:
- 先定义业务事件是什么
- 再定义交换机类型和 Routing Key 规则
- 再定义需要哪些队列
- 再补确认、重试、死信、幂等
- 最后再看监控和告警
放到工程设计里看,RabbitMQ 不是“先建几个队列再看业务怎么塞进去”,而更适合 先按业务事件和失败治理设计消息主线,再把交换机、队列和绑定配置出来。
9. 一条更贴近实战的订单主线
前面已经把 RabbitMQ 的核心模型、可靠性、死信、TTL、延迟消息,以及在 Spring 项目中的接入方式拆开讲过了。
到这里再回头看一个综合场景,会更容易理解:RabbitMQ 在复杂业务里,真正承接的往往不是“一个动作”,而是一整条异步扩散和异常治理链路。
订单类业务就是最典型的例子。
9.1 为什么订单类场景更适合放在后面看
实际业务里的订单流程,通常不会只是:创建订单 -> 发一条消息 -> 消费结束。
更常见的真实情况是:
- 创建订单
- 等待支付
- 支付成功 / 支付失败 / 支付超时
- 支付成功后继续扣减库存、发通知、加积分、走履约链路
- 任一环节失败后,还要考虑重试、补偿和人工介入
所以在工程里,更稳的思路通常不是“让一条消息把所有事情串完”,而是:主链路把订单核心状态推进正确,再通过 RabbitMQ 把后续影响扩散给各个下游系统。
9.2 先有状态机,再有消息链路
订单类业务最重要的基础,通常不是 MQ,而是状态机。
例如一个简化版订单状态流转,常见会有:
WAIT_PAY:已创建,等待支付PAID:支付成功PAY_FAILED:支付失败CLOSED:超时关闭或人工关闭FINISHED:履约完成
要注意的是:RabbitMQ 负责传播事件,但订单状态本身必须先在核心系统里可控、可校验、可防并发冲突。
也就是说:
- 订单是否能从
WAIT_PAY进入PAID - 订单是否还能从
WAIT_PAY进入CLOSED - 支付成功回调和超时关单是否发生竞争
这些问题首先是状态机问题,然后才是消息问题。
9.3 一条更贴近实战的订单主线
把流程压缩一下,比较常见的一条线是:
- 用户创建订单
- 订单服务落库,状态置为
WAIT_PAY - 发送
order.created事件 - 同时安排一条延迟关闭订单的消息
- 用户发起支付
- 支付系统异步回调支付结果
- 如果支付成功,订单状态推进为
PAID - 再发送
order.paid事件 - 库存、积分、通知、履约等下游分别消费
- 如果一直没支付,延迟消息到期后触发关单
- 订单状态从
WAIT_PAY进入CLOSED - 再发送
order.closed或order.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[忽略关单]这张图最重要的意思是:
- 创建订单和支付成功是两次不同的业务推进
- 支付成功后的下游链路通常是异步扩散出去的
- 超时关单是一条独立的延迟治理链路
9.4 创建订单时,RabbitMQ 一般负责什么
用户提交订单后,订单服务通常会先做这些动作:
- 校验商品、价格、优惠、库存等基本信息
- 创建订单记录
- 把订单状态置为
WAIT_PAY - 记录支付超时时间
- 发送
order.created事件
这里最关键的边界是:创建订单本身通常属于主链路事务,MQ 更常负责把“订单已创建”这件事通知给下游。
也就是说,RabbitMQ 更适合做这些事:
- 通知库存系统做预处理
- 通知营销系统记录活动参与
- 通知审计系统记录事件流
- 通知消息系统给用户发提醒
但“订单到底有没有创建成功”这件事,通常还是以订单库落地结果为准。
9.5 等待支付和超时关闭一般怎么配合
创建订单后,系统通常不会阻塞等待支付,而是:
- 前端拿到订单号和支付单信息
- 用户进入支付流程
- 系统后台同时安排“超时关单”这条延迟链路
RabbitMQ 在这里的常见做法就是:
- 把一条消息先放进带
TTL的延迟队列 - 到期后通过死信交换器转到真正的关单队列
- 关单消费者收到消息后,再检查订单状态
要注意的是:超时消息到了,不代表一定要关单,而是要先判断订单当前是不是仍然处于 WAIT_PAY。
因为支付成功回调和超时关单,可能刚好在时间上发生竞争。
9.6 支付成功后为什么常常再发一轮事件
支付成功并不等于所有后续流程也同步做完。
更常见的做法是:
- 支付系统回调成功
- 支付服务验签并做幂等校验
- 订单状态推进到
PAID - 再发送
order.paid事件
然后由下游分别消费:
- 库存服务:扣减库存或确认预占库存
- 积分服务:发放积分
- 履约服务:生成发货/履约任务
- 通知服务:给用户发支付成功通知
- 审计服务:记录支付事件
这里 RabbitMQ 的价值就在于:支付成功后的扩散链路不需要全部同步挂在支付回调接口上。
这样做的好处通常包括:
- 支付回调主链路更短
- 下游新增能力时更容易扩展
- 某个下游失败,不会直接拖死整个支付主流程
9.7 库存一般是在什么时候扣
这个问题在实战里非常常见,而且没有唯一答案。
常见做法通常有两种:
- 创建订单时预占库存,支付成功后确认占用
- 支付成功后再真正扣减库存
第一种更适合:
- 热门商品
- 库存紧张
- 更关注防超卖
第二种更适合:
- 超卖风险较低
- 库存模型没那么强约束
- 更强调流程简单
如果用 RabbitMQ 把这件事放进消息链路里理解,核心不是“扣库存到底同步还是异步”,而是:
库存动作本身要和订单状态推进、支付结果、超时关闭这几条线一起设计。
9.8 这种链路里最难的通常不是发消息
真正难的地方,通常集中在这几件事:
幂等
- 支付回调可能重复
- MQ 消息可能重复消费
- 下游服务也可能重复收到事件
并发竞争
- 支付成功和超时关单可能同时发生
- 重复点击支付和异步回调也可能交织
补偿
- 支付成功了,但库存扣减失败怎么办
- 通知失败了,是否重试或进入死信
- 履约失败后是否退款或人工介入
状态可追踪
- 当前订单到底走到哪一步了
- 某次失败是主链路失败还是下游异步失败
所以这里真正该盯住的是:RabbitMQ 解决的是异步解耦和事件扩散问题,但订单这类复杂业务最终能不能稳,关键还在状态机、幂等、补偿和异常治理。
9.9 如果只记一条实战经验
可以记住这句:订单主流程负责把核心状态推进正确,RabbitMQ 负责把支付、库存、通知、履约等后续影响异步扩散出去,再通过延迟消息、死信队列和补偿机制把异常链路补完整。
10. RabbitMQ 更适合什么,不适合什么
更适合
- 业务事件通知
- 异步解耦
- 路由规则较灵活的业务系统
- 延迟处理和异常治理
- 中小到中大型业务消息系统
不那么适合
- 极高吞吐日志流场景
- 超大规模流式分析场景
- 更强调分布式日志存储能力的场景
如果系统核心目标是:海量日志流 + 高吞吐 + 长时间保留 + 流处理
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 不是简单的“放消息的队列”,而是一套围绕连接、通道、交换机、路由、确认和异常治理构建起来的业务消息模型。