mall-demo-mq
RabbitMQ 学习 Demo,通过 REST 接口手动触发不同 MQ 场景,观察控制台日志。项目源码在项目地址
项目结构
mall-demo-mq
├── pom.xml
├── README-MQ.md
└── src/main
├── java/com/mtcarpenter/mall/demo/mq
│ ├── MallDemoMqApplication.java # 启动类
│ ├── component
│ │ └── RabbitMqDemoListener.java # 消费者监听器
│ ├── config
│ │ └── RabbitMqDemoConfig.java # 交换机/队列/绑定/RabbitTemplate 配置
│ ├── constant
│ │ └── MqDemoConstants.java # 交换机、队列、路由键常量
│ ├── controller
│ │ └── DemoMqController.java # REST 入口,触发发送消息/创建订单
│ ├── domain
│ │ ├── DemoOrder.java # 模拟订单实体
│ │ ├── DemoOrderStatus.java # 订单状态枚举
│ │ └── MqDemoMessage.java # 通用消息体
│ └── service
│ ├── DemoOrderService.java # 模拟订单服务(内存 Map)
│ └── RabbitMqDemoService.java # 生产者服务,封装消息发送逻辑
└── resources
├── application.yml # 端口、RabbitMQ 连接、手动 ack 等配置
└── static
└── demo-mq-page.html # 浏览器交互页面
文件说明
| 文件 | 说明 |
|---|---|
pom.xml |
项目依赖声明,parent 为 spring-boot-starter-parent,依赖 spring-boot-starter-web 和 spring-boot-starter-amqp |
MallDemoMqApplication.java |
Spring Boot 启动类,@SpringBootApplication |
RabbitMqDemoListener.java |
消费者,包含 4 个 @RabbitListener 方法,分别监听普通队列、订单取消队列、Topic 队列 A/B/C |
RabbitMqDemoConfig.java |
声明所有交换机、队列、绑定关系;配置 RabbitTemplate 的 JSON 转换器、ConfirmCallback、ReturnCallback |
MqDemoConstants.java |
集中定义交换机名、队列名、路由键名,方便查阅完整链路 |
DemoMqController.java |
REST 接口入口,提供发送普通消息、无法路由消息、创建/支付/查询订单、发送 Topic 消息等接口 |
DemoOrder.java |
模拟订单 POJO,包含 orderId、status、timeoutSeconds、createTime、payTime、cancelTime |
DemoOrderStatus.java |
订单状态枚举:WAIT_PAY、PAID、CANCELED |
MqDemoMessage.java |
通用消息体,包含 messageId、messageType、businessId、content、createdAt |
DemoOrderService.java |
内存订单服务,用 ConcurrentHashMap 存储订单,提供创建、支付、超时取消、查询操作 |
RabbitMqDemoService.java |
生产者服务,封装 4 种发送方法:普通 direct、无法路由、订单延迟、Topic |
application.yml |
端口 8088,RabbitMQ 连接本机,开启 publisher-confirms/returns,手动 ack,prefetch=1 |
demo-mq-page.html |
浏览器交互页面,点击按钮即可触发各接口,右侧显示响应结果 |
链路讲解
一、普通消息链路(Direct Exchange)
最基础的生产者 → 交换机 → 队列 → 消费者流程。
生产者 RabbitMQ 消费者
│ │ │
│ convertAndSend( │ │
│ demo.direct.exchange, │ │
│ demo.direct, │ │
│ message) │ │
│ ────────────────────────────► │ │
│ │ routing key = demo.direct │
│ │ 匹配绑定关系 │
│ │ ──────────────────────────────────► │
│ │ │
│ ◄── ConfirmCallback(ack=true)│ basicAck() │
│ │ ◄───────────────────────────────── │
关键资源:
| 类型 | 名称 | 说明 |
|---|---|---|
| Exchange | demo.direct.exchange |
Direct 类型,精确匹配 routing key |
| Queue | demo.direct.queue |
普通队列,无死信配置 |
| Binding | routing key = demo.direct |
交换机与队列的绑定关系 |
流程步骤:
- Controller 调用
RabbitMqDemoService.sendDirectMessage() RabbitTemplate.convertAndSend()将消息发到demo.direct.exchange,routing key 为demo.direct- 交换机根据 routing key 找到绑定队列
demo.direct.queue,投递消息 ConfirmCallback回调,ack=true表示消息已到达交换机RabbitMqDemoListener.handleDirectMessage()消费消息,手动basicAck
无法路由场景: 使用 routing key demo.no.queue 发送,交换机能收到(ConfirmCallback ack=true),但找不到队列,触发 ReturnCallback。
易混洗点
Direct 交换机不局限于和队列一对一映射,核心匹配规则是消息的路由键必须与队列的绑定键完全相等。交换机承担消息中转的作用,只要存在匹配绑定关系的队列,就会将消息路由至对应队列。具体特性如下:
- 多个队列绑定相同绑定键时,这些队列都会接收到同一条消息;
- 一台 Direct 交换机可使用不同绑定键,分别绑定多个队列;
- 单个队列也可配置多个绑定键,以此接收不同路由键投递的消息。
二、TTL + 死信队列链路(订单超时取消)
利用消息 TTL 过期后自动转发到死信交换机,实现延迟消费。
生产者 RabbitMQ 消费者
│ │ │
│ convertAndSend( │ │
│ demo.order.delay.exchange│ │
│ demo.order.delay, │ │
│ message + TTL) │ │
│ ──────────────────────────► │ │
│ │ 消息进入 demo.order.delay.queue │
│ │ ── 等待 TTL 到期 ──► │
│ │ │
│ │ TTL 过期,消息变为死信 │
│ │ 死信转发到 demo.order.dead.exchange │
│ │ routing key = demo.order.cancel │
│ │ 投递到 demo.order.cancel.queue │
│ │ ────────────────────────────────────────► │
│ │ 查询订单状态 │
│ │ WAIT_PAY → CANCELED │
│ │ PAID → 跳过取消 │
│ │ basicAck() │
│ │ ◄──────────────────────────────────────── │
关键资源:
| 类型 | 名称 | 说明 |
|---|---|---|
| Exchange | demo.order.delay.exchange |
延迟交换机,Direct 类型 |
| Queue | demo.order.delay.queue |
延迟队列,配置了 x-dead-letter-exchange 和 x-dead-letter-routing-key,无消费者监听 |
| Exchange | demo.order.dead.exchange |
死信交换机,接收过期消息 |
| Queue | demo.order.cancel.queue |
订单取消队列,消费者监听此队列 |
| Binding | delay 队列 → delay 交换机,routing key = demo.order.delay |
生产者投递入口 |
| Binding | cancel 队列 → dead 交换机,routing key = demo.order.cancel |
死信投递出口 |
流程步骤:
DemoOrderService.createOrder()创建订单(状态WAIT_PAY),调用RabbitMqDemoService.sendOrderTimeoutMessage()- 消息设置
expiration(毫秒级 TTL),发送到demo.order.delay.exchange - 消息进入
demo.order.delay.queue,没有消费者监听,消息在此等待 - TTL 到期,RabbitMQ 将消息作为死信,按队列配置的
x-dead-letter-exchange转发到demo.order.dead.exchange - 死信交换机按
x-dead-letter-routing-key=demo.order.cancel投递到demo.order.cancel.queue RabbitMqDemoListener.handleOrderTimeoutMessage()消费消息,调用DemoOrderService.cancelOrderIfStillWaitingPay()- 幂等判断 :只有
WAIT_PAY状态才取消;PAID跳过;CANCELED跳过
核心思想: 延迟队列只是"等待室",订单取消队列才是"处理室"。延迟消息只是提醒"该检查了",不能无脑取消。
三、Topic 交换机链路
Topic 交换机支持 routing key 的模式匹配:* 匹配一个单词,# 匹配零个或多个单词。
生产者 RabbitMQ 消费者
│ │ │
│ convertAndSend( │ │
│ demo.topic.exchange, │ │
│ topic.user.create, │ │
│ message) │ │
│ ────────────────────────────► │ │
│ │ topic.user.create 匹配结果: │
│ │ ┌─ topic.# ──► queue.a ──────────► │ 队列 A 消费者
│ │ └─ topic.user.* ──► queue.b ─────► │ 队列 B 消费者
│ │ │
│ │ topic.order.create 匹配结果: │
│ │ ┌─ topic.# ──► queue.a ──────────► │ 队列 A 消费者
│ │ └─ topic.order.create ──► queue.c ► │ 队列 C 消费者
│ │ │
│ │ topic.any.thing 匹配结果: │
│ │ └─ topic.# ──► queue.a ──────────► │ 队列 A 消费者
关键资源:
| 类型 | 名称 | 绑定模式 | 说明 |
|---|---|---|---|
| Exchange | demo.topic.exchange |
--- | Topic 类型交换机 |
| Queue | demo.topic.queue.a |
topic.# |
匹配所有以 topic. 开头的路由键 |
| Queue | demo.topic.queue.b |
topic.user.* |
只匹配 topic.user. 后跟一个单词 |
| Queue | demo.topic.queue.c |
topic.order.create |
精确匹配,效果等同于 Direct |
路由匹配示例:
| routing key | 队列 A (topic.#) |
队列 B (topic.user.*) |
队列 C (topic.order.create) |
|---|---|---|---|
topic.user.create |
✅ | ✅ | ❌ |
topic.user.delete |
✅ | ✅ | ❌ |
topic.order.create |
✅ | ❌ | ✅ |
topic.any.thing |
✅ | ❌ | ❌ |
topic.user.a.b |
✅ | ❌(* 只匹配一个单词) |
❌ |
流程步骤:
- Controller 调用
RabbitMqDemoService.sendTopicMessage(routingKey, content) - 消息发送到
demo.topic.exchange,携带用户指定的 routing key - 交换机根据绑定模式逐一匹配,将消息投递到所有匹配的队列
- 各队列对应的
@RabbitListener方法分别消费,手动basicAck
与 Direct 的区别: Direct 要求 routing key 完全相等;Topic 允许通配符,一条消息可同时投递到多个队列。