第十九课实战:消息队列实战——SpringBoot 用户注册异步链路最小闭环(工程版)

场景:用户注册成功后,异步发送欢迎邮件

技术栈: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. 下一步怎么升级(你后续文章的方向)

如果你要从"最小闭环"升级成"生产可用",通常会加:

  1. Outbox(本地消息表):解决"写库成功但发 MQ 失败"的一致性问题
  2. 幂等 :消费者按 eventId 去重
  3. 重试策略:业务可重试、不可重试分别处理
  4. 延迟队列/补偿:和第20课"任务系统"衔接
  5. 统一事件规范:eventName、eventId、traceId、occurredAt

可选增强:改为 JSON 消息(更规范)

如果你希望消息体可读、跨语言友好,建议加一个 JSON MessageConverter:

java 复制代码
@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}

然后 RabbitTemplate 和 Listener 都会走 JSON。

相关推荐
whinc17 小时前
Rust技术周刊 2026年第17周
后端·rust
whinc17 小时前
Rust技术周刊 2026年第18周
后端·rust
xqqxqxxq17 小时前
Java AI智能P图工具技术笔记
java·人工智能·笔记
whinc17 小时前
Rust技术周刊 2026年第16周
后端·rust
谷雨不太卷17 小时前
进程的状态码
java·前端·算法
jieyucx17 小时前
Go语言深度解剖:Map扩容机制全解析(增量扩容+等量扩容+渐进式迁移)
开发语言·后端·golang·map·扩容策略
顾温17 小时前
default——C#/C++
java·c++·c#
空中海17 小时前
02 ArkTS 语言与工程规范
java·前端·spring
楚国的小隐士18 小时前
在AI时代,如何从0接手一个项目?
java·ai·大模型·编程·ai编程·自闭症·自闭症谱系障碍·神经多样性
yaki_ya18 小时前
yaki-C语言:从概念基础到内存解析---数组(array)完全指南
java·c语言·算法