场景:用户注册成功后,异步发送欢迎邮件
技术栈:Spring Boot 3.x + RabbitMQ
关键词:Producer / Consumer / Topic / Confirm / Retry / DLQ(死信队列)
1. 你最终要跑通的链路
POST /api/register
→ UserService 写入用户(这里用内存模拟或数据库都行)
→ 发布事件 USER_REGISTERED
→ 立刻返回 success
RabbitMQ
→ MailConsumer 监听队列
→ 模拟发送邮件(可故意失败触发重试/死信)
2. 项目结构(建议这样放)
mq-register-demo
├── src/main/java/com/example/demo
│ ├── DemoApplication.java
│ ├── config
│ │ ├── RabbitMQConfig.java
│ │ └── RabbitTemplateConfig.java
│ ├── controller
│ │ └── RegisterController.java
│ ├── domain
│ │ ├── event
│ │ │ └── UserRegisteredEvent.java
│ │ └── model
│ │ └── User.java
│ ├── service
│ │ ├── UserService.java
│ │ └── MailService.java
│ ├── mq
│ │ ├── producer
│ │ │ └── UserEventProducer.java
│ │ └── consumer
│ │ └── MailConsumer.java
│ └── util
│ └── Jsons.java
└── src/main/resources
└── application.yml
3. 环境准备:RabbitMQ(本地 Docker)
docker run -d --name rabbit \
-p 5672:5672 -p 15672:15672 \
rabbitmq:3-management
- 管理台:
http://localhost:15672 - 默认账号密码:
guest / guest
4. Maven 依赖(pom.xml)
XML
<dependencies>
<!-- Web 层:提供 REST API(Controller、JSON 入参/出参、内置 Tomcat 等运行时能力) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- RabbitMQ / AMQP:Spring 对 MQ 的封装(生产/消费都靠它)
- Producer:RabbitTemplate 发送消息
- Consumer:@RabbitListener 监听队列
- 连接/通道:ConnectionFactory、消息确认(confirm/return)、重试等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- 参数校验:用于校验 Controller 入参 DTO
常用注解:@NotBlank、@NotNull、@Email、@Size 等
目的:把"非法请求"挡在业务层之外 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- JSON 序列化/反序列化(Java 对象 ↔ JSON)
- Web:返回/接收 JSON 时使用
- MQ:若配置 Jackson2JsonMessageConverter,消息体会用它做序列化
备注:starter-web 通常已间接引入 Jackson,这里显式写出便于读者理解 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
- 如果只做 Web 接口 :
starter-web + validation就够 - 引入 MQ :必须加
starter-amqp - 做 JSON 消息体(跨语言友好) :通常会配
Jackson2JsonMessageConverter,Jackson 相关依赖就更关键
5. application.yml(写全一点)
XML
server:
port: 8080
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
# 生产者确认:消息是否到达交换机 / 是否路由到队列
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
acknowledge-mode: manual # 手动ACK,便于演示可靠性
retry:
enabled: true
initial-interval: 1000ms
max-attempts: 3
multiplier: 2.0
max-interval: 5000ms
6. MQ 设计:交换机/队列/死信队列
我们定义:
- 交换机:
user.exchange(topic) - 业务队列:
mail.queue - routingKey:
user.registered - 死信交换机:
dlx.exchange - 死信队列:
mail.dlq
6.1 RabbitMQConfig.java
java
package com.example.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
public class RabbitMQConfig {
public static final String USER_EXCHANGE = "user.exchange";
public static final String USER_REGISTERED_KEY = "user.registered";
public static final String MAIL_QUEUE = "mail.queue";
public static final String DLX_EXCHANGE = "dlx.exchange";
public static final String MAIL_DLQ = "mail.dlq";
public static final String DLX_ROUTING_KEY = "dlx.mail";
@Bean
public TopicExchange userExchange() {
return new TopicExchange(USER_EXCHANGE, true, false);
}
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(DLX_EXCHANGE, true, false);
}
@Bean
public Queue mailQueue() {
// 绑定死信:消费失败/拒绝/过期等 → 进入 DLX
return QueueBuilder.durable(MAIL_QUEUE)
.withArguments(Map.of(
"x-dead-letter-exchange", DLX_EXCHANGE,
"x-dead-letter-routing-key", DLX_ROUTING_KEY
))
.build();
}
@Bean
public Queue mailDlq() {
return QueueBuilder.durable(MAIL_DLQ).build();
}
@Bean
public Binding mailBinding(Queue mailQueue, TopicExchange userExchange) {
return BindingBuilder.bind(mailQueue).to(userExchange).with(USER_REGISTERED_KEY);
}
@Bean
public Binding dlqBinding(Queue mailDlq, DirectExchange dlxExchange) {
return BindingBuilder.bind(mailDlq).to(dlxExchange).with(DLX_ROUTING_KEY);
}
}
7. 生产者可靠性:Confirm + Return
7.1 RabbitTemplateConfig.java
java
package com.example.demo.config;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitTemplateConfig {
public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
// confirm:消息是否到达交换机
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
String id = correlationData == null ? "null" : correlationData.getId();
if (ack) {
System.out.println("[CONFIRM] OK id=" + id);
} else {
System.out.println("[CONFIRM] FAIL id=" + id + " cause=" + cause);
}
});
// return:消息到达交换机,但无法路由到队列
rabbitTemplate.setReturnsCallback(returned -> {
System.out.println("[RETURN] message=" + returned.getMessage()
+ " replyText=" + returned.getReplyText()
+ " exchange=" + returned.getExchange()
+ " routingKey=" + returned.getRoutingKey());
});
}
}
8. 事件模型:别只传 success
8.1 UserRegisteredEvent.java
java
package com.example.demo.domain.event;
import java.time.Instant;
public class UserRegisteredEvent {
public Long userId;
public String email;
public Instant occurredAt;
public UserRegisteredEvent() {}
public UserRegisteredEvent(Long userId, String email) {
this.userId = userId;
this.email = email;
this.occurredAt = Instant.now();
}
}
9. Producer:注册成功后发事件
9.1 UserEventProducer.java
java
package com.example.demo.mq.producer;
import com.example.demo.config.RabbitMQConfig;
import com.example.demo.domain.event.UserRegisteredEvent;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
public class UserEventProducer {
private final RabbitTemplate rabbitTemplate;
public UserEventProducer(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void publishUserRegistered(UserRegisteredEvent event) {
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(
RabbitMQConfig.USER_EXCHANGE,
RabbitMQConfig.USER_REGISTERED_KEY,
event,
cd
);
}
}
这里我们直接发 对象,Spring AMQP 默认会用序列化(你也可以切换为 JSON MessageConverter,生产更规范,后面我给你一段可选增强)。
10. Consumer:监听队列 + 手动 ACK + 失败进入死信
10.1 MailService.java(模拟发邮件)
java
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class MailService {
public void sendWelcomeMail(String email) {
// 模拟:你可以在这里接入真实邮件服务
System.out.println("✅ 发送欢迎邮件给: " + email);
// 演示失败:取消注释可触发重试/死信
// if (email.contains("fail")) throw new RuntimeException("模拟邮件服务失败");
}
}
10.2 MailConsumer.java
java
package com.example.demo.mq.consumer;
import com.example.demo.config.RabbitMQConfig;
import com.example.demo.domain.event.UserRegisteredEvent;
import com.example.demo.service.MailService;
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;
@Component
public class MailConsumer {
private final MailService mailService;
public MailConsumer(MailService mailService) {
this.mailService = mailService;
}
@RabbitListener(queues = RabbitMQConfig.MAIL_QUEUE)
public void onMessage(UserRegisteredEvent event,
Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
try {
System.out.println("📩 [Consumer] 收到事件 userId=" + event.userId + " email=" + event.email);
mailService.sendWelcomeMail(event.email);
channel.basicAck(tag, false); // 成功:ACK
} catch (Exception e) {
System.out.println("❌ [Consumer] 处理失败:" + e.getMessage());
// requeue=false:不重新入队,交给死信队列(DLQ)
channel.basicNack(tag, false, false);
}
}
}
11. Controller:提供注册接口
11.1 User.java(简单模型)
java
package com.example.demo.domain.model;
public class User {
public Long id;
public String email;
public User(Long id, String email) {
this.id = id;
this.email = email;
}
}
11.2 UserService.java(模拟"写库")
java
package com.example.demo.service;
import com.example.demo.domain.model.User;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class UserService {
private final AtomicLong idGen = new AtomicLong(1000);
public User register(String email) {
// 这里可替换成:写 MySQL(事务)
Long id = idGen.incrementAndGet();
System.out.println("🧾 用户注册成功,写入用户表 userId=" + id + " email=" + email);
return new User(id, email);
}
}
11.3 RegisterController.java
java
package com.example.demo.controller;
import com.example.demo.domain.event.UserRegisteredEvent;
import com.example.demo.domain.model.User;
import com.example.demo.mq.producer.UserEventProducer;
import com.example.demo.service.UserService;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@Validated
public class RegisterController {
private final UserService userService;
private final UserEventProducer producer;
public RegisterController(UserService userService, UserEventProducer producer) {
this.userService = userService;
this.producer = producer;
}
public static class RegisterReq {
@NotBlank
@Email
public String email;
}
@PostMapping("/register")
public String register(@RequestBody RegisterReq req) {
User user = userService.register(req.email);
// 发布事件(异步)
producer.publishUserRegistered(new UserRegisteredEvent(user.id, user.email));
// 立刻返回
return "success";
}
}
12. 启动 & 验证
12.1 启动服务
启动 Spring Boot。
12.2 调用接口
java
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"email":"a@b.com"}'
你会看到:
- Controller 立刻返回 success
- Consumer 异步打印"发送欢迎邮件"
12.3 验证死信(可选)
把邮箱改成 fail@b.com,并在 MailService 打开模拟失败那行。
然后去 RabbitMQ 管理台看 mail.dlq 队列里是否有消息。
13. 这就是"最小闭环"的工程价值
你跑通的不是"发一条 MQ"这么简单,而是把系统做成了:
- 主链路(注册)尽快返回
- 旁路(邮件)异步解耦
- 失败不丢(DLQ 接住)
- 可观测(Confirm / Return / Consumer 日志)
14. 下一步怎么升级(你后续文章的方向)
如果你要从"最小闭环"升级成"生产可用",通常会加:
- Outbox(本地消息表):解决"写库成功但发 MQ 失败"的一致性问题
- 幂等 :消费者按
eventId去重 - 重试策略:业务可重试、不可重试分别处理
- 延迟队列/补偿:和第20课"任务系统"衔接
- 统一事件规范:eventName、eventId、traceId、occurredAt
可选增强:改为 JSON 消息(更规范)
如果你希望消息体可读、跨语言友好,建议加一个 JSON MessageConverter:
java
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
然后 RabbitTemplate 和 Listener 都会走 JSON。