3D打印云平台订单模块设计与优化(高并发+幂等性+异步解耦)
1. 引言:项目背景与挑战
在2025年5月至8月期间,我作为核心开发人员,参与了一个为3D打印企业搭建的云端管理平台项目。该平台采用经典的前后端分离架构,我的工作主要集中在后端。
项目核心挑战:
- 业务核心:订单模块是平台的“心脏”,连接用户、支付、设备,一旦出问题,直接导致资损和用户投诉。
- 高并发场景:促销活动或热门模型上线时,瞬时下单压力巨大。
- 网络不确定性:用户重复点击、支付回调重复触发等问题难以避免。
- 复杂流程:订单状态变更后,需触发通知、更新报表等大量后续操作,如何保证核心链路快速响应?
面对这些挑战,我主导了接口幂等性设计和基于MQ的异步解耦架构,成功提升了系统的稳定性、健壮性和吞吐量。下面我将重点分享订单模块的设计思路与实战代码。
2. 核心技术栈
- 后端框架:Spring Boot 3.x
- 数据持久化:MyBatis-Plus + MySQL 8.0
- 缓存与幂等:Redis
- 异步解耦:RabbitMQ
- 检索:Elasticsearch
- 部署与CI/CD:Docker + Jenkins
3.核心实战一:接口幂等性设计,彻底杜绝重复订单
3.1 问题场景
上线初期,我们收到反馈:“明明只点了一次,为什么生成了两个订单?”。经日志排查,是网络延迟导致用户重复提交或支付回调接口被重复触发。
– 幂等性:在分布式系统中,一个操作无论执行多少次,都能产生相同的结果。对于创建订单
、支付回调
等接口,这是刚性需求。
3.2 技术选型与设计
常见的方案有:
- 数据库唯一索引:利用订单号等业务唯一键约束。简单,但灵活性差,无法覆盖所有场景。
- 乐观锁:通过
version
字段控制更新。适用于更新操作,但对插入操作不友好。 - Token令牌机制:服务端下发令牌,请求时校验令牌,一次有效。通用、灵活、对业务零侵入,我最终选择了此方案。
核心流程设计:
- 进入下单页时,前端先请求服务端获取一个全局唯一的Token。
- 服务端将Token存入Redis,并设置有效期(如5分钟)。
- 用户提交订单时,将此Token放入HTTP请求头(如
X-Idempotent-Token
)中一并提交。 - 后端接口在执行业务逻辑前,先调用Redis的
DEL
命令尝试删除这个Token。- 删除成功(返回1):说明是第一次请求,执行后续业务逻辑(创建订单)。
- 删除失败(返回0):说明Token已失效(可能是重复请求),直接返回错误,拒绝业务执行。
3.3 代码实现(Spring Boot + Redis + AOP)
第1步:定义幂等性注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等Token在请求头中的名称,默认为'X-Idempotent-Token'
*/
String tokenHeader() default "X-Idempotent-Token";
/**
* Token有效期,默认5分钟(单位:秒)
*/
long expireTime() default 300L;
}
第2步:实现幂等性切面(AOP)—— 核心!
@Aspect
@Component
@Slf4j
public class IdempotentAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.example.annotation.Idempotent)")
public void idempotentPointcut() {}
@Around("idempotentPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取当前HTTP请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取方法上的注解
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Idempotent idempotentAnnotation = method.getAnnotation(Idempotent.class);
// 从请求头中获取Token
String token = request.getHeader(idempotentAnnotation.tokenHeader());
if (StrUtil.isBlank(token)) {
throw new RuntimeException("幂等Token缺失");
}
// 【核心逻辑】尝试删除Token:原子性操作,删除成功代表是首次请求
Boolean deleteResult = stringRedisTemplate.delete(token);
if (Boolean.TRUE.equals(deleteResult)) {
// 删除成功,放行业务方法执行(如创建订单)
log.info("[幂等校验通过] Token: {}", token);
return joinPoint.proceed();
} else {
// 删除失败,Token已不存在,认定为重复请求
log.warn("[检测到重复请求] Token: {}", token);
throw new RuntimeException("请勿重复提交请求");
}
}
}
第3步:在Controller中使用
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final StringRedisTemplate stringRedisTemplate;
// 1. 获取Token的接口
@GetMapping("/token")
public R<String> getIdempotentToken() {
String token = IdUtil.simpleUUID(); // 或使用UUID.randomUUID().toString()
// 将Token存入Redis,并设置5分钟有效期
stringRedisTemplate.opsForValue().set(token, "1", Duration.ofSeconds(300));
return R.ok(token);
}
// 2. 创建订单接口 - 使用幂等注解保护
@PostMapping("/create")
@Idempotent // 应用自定义注解
public R<OrderVO> createOrder(@RequestBody @Valid OrderCreateRequest request,
@RequestHeader("X-Idempotent-Token") String token) {
// 此方法只有在幂等校验通过后才会执行
OrderVO order = orderService.createOrder(request);
return R.ok(order);
}
}
第4步:前端调用序列
- 进入页面 -> 立马调用
GET /order/token
获取Token。 - 提交订单 -> 携带Token (
X-Idempotent-Token: ${token}
) 调用POST /order/create
。
3.4 成果
此方案上线后,彻底解决了重复订单和重复支付的问题。得益于AOP设计,该注解可轻松复用于支付回调、退款等重要接口,大大提升了系统的健壮性。
4. 核心实战二:异步解耦,订单状态同步毫秒级响应
订单状态变更(如“打印完成”)后,常需触发一系列“后续操作”:发送完成通知(调用api)、更新统计报表、清除缓存等。这些操作耗时且非核心链路。
解决方案:引入RabbitMQ消息队列,进行异步解耦。
设计思路:
- 生产者(订单服务):在完成核心的数据库状态更新后,立即向RabbitMQ发送一条消息(如
order.status.completed
),然后即刻返回响应。核心链路耗时极短。 - 消费者(多个独立服务):如通知服务、报表服务等,订阅该消息。它们异步地获取消息并处理自身业务,即使处理慢或暂时失败,也不会影响主流程。
代码示例:生产者(订单服务内)
@Service
@Slf4j
@RequiredArgsConstructor
public class OrderStatusService {
private final OrderMapper orderMapper;
private final RabbitTemplate rabbitTemplate;
@Transactional
public void updateToCompleted(Long orderId) {
// 1. 更新数据库核心状态(核心业务)
Order order = new Order();
order.setId(orderId);
order.setStatus("completed");
order.setCompleteTime(LocalDateTime.now());
orderMapper.updateById(order);
log.info("订单状态更新为已完成: {}", orderId);
// 2. 发送MQ消息,触发后续异步操作
OrderCompletedMessage msg = new OrderCompletedMessage();
msg.setOrderId(orderId);
msg.setUserId(order.getUserId());
// 确保消息持久化
rabbitTemplate.convertAndSend("order.exchange",
"order.completed",
msg,
message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
});
log.info("已发送订单完成消息: {}", msg);
// 3. 方法返回,事务提交,前端立即得到响应
}
}
成果:通过此设计,订单状态更新的系统响应时间从原来的分钟级(同步等待所有后续操作完成)优化至毫秒级,系统吞吐量提升了一个数量级,且各服务间职责更清晰,易于扩展。
5. 总结与思考
在本项目中,通过Token令牌+Redis+AOP实现幂等性和RabbitMQ实现异步解耦,是保障订单模块稳定、高效的两大核心技术手段。
- 幂等性是分布式系统的基石:任何可能重试的操作都应考虑幂等性,设计时应选择与业务场景最契合的方案。
- 异步是性能优化的银弹:凡是能异步的操作,就不要同步做。解耦不仅能提升性能,更能提升系统的可维护性和扩展性。
- 技术为业务服务:所有的架构设计和技术选型,最终目的都是为了更好地支撑业务,解决实际痛点。
6.完整的模拟工程
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version> <!-- 使用一个具体的Spring Boot 3.x版本 -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>idempotency-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>idempotency-demo</name>
<description>Demo project for Idempotency and Async with Spring Boot</description>
<properties>
<java.version>17</java.version> <!-- Spring Boot 3.x 需要 Java 17 或更高 -->
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok for less boilerplate code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version> <!-- 使用一个具体的Hutool版本 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
profiles:
active: dev
application:
name: idempotency-demo
server:
port: 8080
logging:
level:
com.example.demo: DEBUG
application-dev.yml
spring:
data:
redis:
host: # 请替换为你的Redis主机
port: 6379 # 请替换为你的Redis端口
# password: your_password # 如果有密码,请取消注释并设置
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: # 请替换为你的RabbitMQ主机
port: 5672 # 请替换为你的RabbitMQ端口
username: # 请替换为你的RabbitMQ用户名
password: # 请替换为你的RabbitMQ密码
virtual-host: /
listener:
simple:
acknowledge-mode: auto # 自动确认消息
publisher-confirm-type: correlated # 启用发布确认
publisher-returns: true # 启用发布返回
# 自定义配置
demo:
order:
exchange: order.exchange
routing-key: order.completed
queue: order.completed.queue
启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class IdempotencyDemoApplication {
public static void main(String[] args) {
SpringApplication.run(IdempotencyDemoApplication.class, args);
}
}
统一响应类R
package com.example.demo.common;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 通用的响应结果封装类
* 支持对象序列化
* @param <T>
*/
@Data
@NoArgsConstructor
public class R<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private int code;
private String message;
private T data;
public static <T> R<T> ok() {
return restResult(null, 200, "操作成功");
}
public static <T> R<T> ok(T data) {
return restResult(data, 200, "操作成功");
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, 200, msg);
}
public static <T> R<T> failed() {
return restResult(null, 500, "操作失败");
}
public static <T> R<T> failed(String msg) {
return restResult(null, 500, msg);
}
public static <T> R<T> failed(int code, String msg) {
return restResult(null, code, msg);
}
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMessage(msg);
return apiResult;
}
}
自定义幂等注解
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等Token在请求头中的名称,默认为'X-Idempotent-Token'
*/
String tokenHeader() default "X-Idempotent-Token";
/**
* Token有效期,默认5分钟(单位:秒)
*/
long expireTime() default 300L;
}
RedisConfig.java
Redis配置类
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
RabbitMQConfig.java
MQ配置类
package com.example.demo.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMQConfig {
@Value("${demo.order.exchange}")
private String exchangeName;
@Value("${demo.order.routing-key}")
private String routingKey;
@Value("${demo.order.queue}")
private String queueName;
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(exchangeName, true, false); // durable=true, autoDelete=false
}
@Bean
public Queue orderCompletedQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", ""); // 可配置死信交换机
args.put("x-message-ttl", 60000); // 可配置消息TTL
return new Queue(queueName, true, false, false, args); // durable=true
}
@Bean
public Binding bindingOrderCompleted(Queue orderCompletedQueue, DirectExchange orderExchange) {
return BindingBuilder.bind(orderCompletedQueue).to(orderExchange).with(routingKey);
}
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
// 配置 RabbitTemplate 使用 JSON 转换器
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(jsonMessageConverter());
// 可以在这里配置其他 RabbitTemplate 属性,如确认回调等
return template;
}
}
IdempotentAspect.java
(幂等切面)
package com.example.demo.aspect;
import com.example.demo.annotation.Idempotent;
import com.example.demo.common.R;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor // Lombok 注解,自动注入 final 字段
public class IdempotentAspect {
private final StringRedisTemplate stringRedisTemplate;
@Around("@annotation(com.example.demo.annotation.Idempotent)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
log.error("无法获取HTTP请求上下文");
return R.failed("无法获取HTTP请求上下文");
}
// 获取请求对象
HttpServletRequest request = attributes.getRequest();
// 获取幂等注解
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Idempotent idempotentAnnotation = method.getAnnotation(Idempotent.class);
String token = request.getHeader(idempotentAnnotation.tokenHeader());
if (token == null || token.isEmpty()) {
log.warn("幂等Token缺失");
return R.failed("幂等Token缺失");
}
// 【核心逻辑】使用 SET key value NX EX 命令实现原子性检查和设置
// 如果返回 true,说明 key 不存在,是首次请求,设置一个标记并允许执行
// 如果返回 false,说明 key 存在,是重复请求,拒绝执行
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(token, "1", idempotentAnnotation.expireTime(), TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
log.info("[幂等校验通过] Token: {}", token);
try {
// 执行业务逻辑
return joinPoint.proceed();
} catch (Throwable throwable) {
// 如果业务执行失败,删除Redis中的Token,允许重试
stringRedisTemplate.delete(token);
log.warn("[业务执行失败,Token已清除,允许重试] Token: {}", token, throwable);
throw throwable; // 重新抛出异常,让全局异常处理器处理
}
} else {
log.warn("[检测到重复请求] Token: {}", token);
return R.failed("请勿重复提交请求");
}
}
}
OrderService.java
& OrderServiceImpl.java
package com.example.demo.service;
import com.example.demo.common.R;
public interface OrderService {
R<String> createOrder(String modelId, String userId);
}
package com.example.demo.service.impl;
import com.example.demo.common.R;
import com.example.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Override
public R<String> createOrder(String modelId, String userId) {
// --- 模拟核心业务逻辑 ---
log.info("开始处理创建订单请求: modelId={}, userId={}", modelId, userId);
// 1. 参数校验 (已由Controller和Validation处理)
// 2. 查询用户信息 (模拟)
log.debug("查询用户信息: {}", userId);
// 3. 查询模型信息 (模拟)
log.debug("查询模型信息: {}", modelId);
// 4. 计算价格 (模拟)
String price = "99.99";
log.debug("计算价格: {}", price);
// 5. 生成订单号 (模拟)
String orderNumber = "ORD" + System.currentTimeMillis();
log.debug("生成订单号: {}", orderNumber);
// 6. 保存订单到数据库 (模拟)
log.info("模拟保存订单到数据库: OrderNumber={}", orderNumber);
// --- 模拟核心业务逻辑结束 ---
// 7. 返回结果
return R.ok("订单 " + orderNumber + " 创建成功");
}
}
OrderStatusService.java
package com.example.demo.service;
import com.example.demo.common.R;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor // 自动注入 final 字段
public class OrderStatusService {
private final RabbitTemplate rabbitTemplate;
@Value("${demo.order.exchange}")
private String exchangeName;
@Value("${demo.order.routing-key}")
private String routingKey;
public R<String> updateToCompleted(Long orderId) {
// --- 模拟核心业务逻辑 ---
log.info("开始处理订单状态更新为已完成: orderId={}", orderId);
// 1. 更新数据库核心状态 (模拟)
log.info("模拟更新数据库订单状态为'completed': {}", orderId);
log.info("模拟设置订单完成时间: {}", LocalDateTime.now());
// --- 模拟核心业务逻辑结束 ---
// --- 异步发送MQ消息 ---
try {
Map<String, Object> message = new HashMap<>();
message.put("orderId", orderId);
message.put("timestamp", System.currentTimeMillis());
message.put("status", "completed");
rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
log.info("已发送订单完成消息到MQ: {}", message);
} catch (Exception e) {
log.error("发送订单完成消息到MQ失败: orderId={}", orderId, e);
// 根据业务需求决定是否返回错误或进行补偿
// 这里简化处理,仅记录日志
}
// --- 异步发送MQ消息结束 ---
// 3. 立即返回响应
return R.ok("订单 " + orderId + " 状态更新请求已提交");
}
}
Controller
package com.example.demo.controller;
import cn.hutool.core.util.IdUtil;
import com.example.demo.annotation.Idempotent;
import com.example.demo.common.R;
import com.example.demo.service.OrderService;
import com.example.demo.service.OrderStatusService;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor // 自动注入 final 字段
@Slf4j
public class OrderController {
private final OrderService orderService;
private final OrderStatusService orderStatusService;
private final StringRedisTemplate stringRedisTemplate;
// 1. 获取Token的接口
@GetMapping("/token")
public R<String> getIdempotentToken() {
String token = IdUtil.simpleUUID(); // 生成Token
// 将Token存入Redis,并设置5分钟有效期
stringRedisTemplate.opsForValue().set(token, "1", Duration.ofSeconds(300));
log.info("生成并返回幂等Token: {}", token);
return R.ok(token);
}
// 2. 创建订单接口 - 使用幂等注解保护
@PostMapping("/create")
@Idempotent // 应用自定义注解
public R<String> createOrder(
@RequestParam @NotBlank(message = "模型ID不能为空") String modelId,
@RequestParam(defaultValue = "1") String userId,
@RequestHeader("X-Idempotent-Token") String token) {
log.info("收到创建订单请求: modelId={}, userId={}, token={}", modelId, userId, token);
// 此方法只有在幂等校验通过后才会执行
return orderService.createOrder(modelId, userId);
}
// 3. 模拟订单状态变更接口 (触发异步流程)
@PostMapping("/complete/{orderId}")
public R<String> completeOrder(@PathVariable Long orderId) {
log.info("收到订单完成请求: orderId={}", orderId);
return orderStatusService.updateToCompleted(orderId);
}
}
OrderMessageListener.java
(MQ监听器)
package com.example.demo.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 订单完成消息监听器
*/
@Slf4j
@Component
public class OrderMessageListener {
@RabbitListener(queues = "${demo.order.queue}")
public void handleOrderCompletedMessage(Map<String, Object> message) {
log.info("接收到订单完成消息: {}", message);
// --- 模拟后续异步操作 ---
Long orderId = (Long) message.get("orderId");
log.debug("开始处理订单 {} 的后续操作...", orderId);
// 1. 发送通知 (模拟)
log.debug("模拟发送订单完成通知给用户...");
// 2. 更新报表 (模拟)
log.debug("模拟更新销售统计报表...");
// 3. 清除缓存 (模拟)
log.debug("模拟清除相关缓存...");
log.info("订单 {} 的后续异步操作处理完成.", orderId);
// --- 模拟后续异步操作结束 ---
}
}
测试:
- 获取token
- 创建订单 (首次)
- 创建订单 (重复)
- 模拟订单完成 (触发异步)
核心流程:
应用启动时,Spring Boot 会加载配置 (
application.yml
,application-dev.yml
),初始化 Redis 和 RabbitMQ 的连接。RabbitMQConfig
会根据配置创建一个名为order.exchange
的 Direct Exchange,一个名为order.completed.queue
的持久化队列,并将它们通过order.completed
这个 Routing Key 绑定起来。OrderMessageListener
会监听order.completed.queue
队列,等待处理消息。获取 Token:
- 用户在前端准备下单时,前端会向后端发送一个请求:
GET /order/token
。 OrderController.getIdempotentToken()
方法被调用。- 服务端使用
IdUtil.simpleUUID()
生成一个全局唯一的 Token (例如a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8
)。 - 服务端将这个 Token 作为 Key,任意值(如 “1”)作为 Value,存入 Redis,并设置 5 分钟的过期时间。
- 服务端将这个 Token 返回给前端。
- 用户在前端准备下单时,前端会向后端发送一个请求:
提交订单 (幂等校验):
用户填写完订单信息并点击提交,前端会向后端发送创建订单的请求:
POST /order/create
。关键: 这个请求的 Header 中必须包含上一步获取的 Token,Header 名为
X-Idempotent-Token
。因为
OrderController.createOrder()
方法上标注了@Idempotent
注解,Spring AOP 会触发IdempotentAspect
切面。IdempotentAspect的around方法首先执行:
- 它从请求 Header 中取出 Token。
- 核心校验: 它尝试使用 Redis 的
delete(token)
命令删除这个 Token。- 第一次请求: Token 存在于 Redis 中,
delete
命令成功执行并返回true
。切面认为这是首次请求,记录日志[幂等校验通过]
,然后允许执行OrderController.createOrder()
方法中的业务逻辑(这里是模拟的日志输出)。 - 重复请求: 如果用户快速点击多次,后续的请求携带相同的 Token。当切面再次尝试
delete(token)
时,因为 Token 已经被第一次成功的请求删除了,所以delete
命令会失败并返回false
。切面识别出这是重复请求,记录日志[检测到重复请求]
,并阻止OrderController.createOrder()
方法的执行,直接返回错误信息 “请勿重复提交请求”。
- 第一次请求: Token 存在于 Redis 中,
如果幂等校验通过,
OrderController.createOrder()
方法继续执行,模拟创建订单的业务逻辑(打印日志),然后返回成功结果给前端。
触发状态变更:
- 假设某个事件(如后台打印完成)需要将订单 ID 为
1001
的订单状态更新为“已完成”。前端或某个服务会调用:POST /order/complete/1001
。 OrderController.completeOrder(1001)
方法被调用。- 该方法将请求转发给
OrderStatusService.updateToCompleted(1001)
。
- 假设某个事件(如后台打印完成)需要将订单 ID 为
核心状态更新与消息发送:
OrderStatusService.updateToCompleted(1001)方法执行:
- 核心业务: 模拟更新数据库订单状态为“completed”(打印日志)。
- 发送消息: 创建一个包含订单 ID 等信息的消息(
Map
),然后通过RabbitTemplate
将这个消息发送到之前定义的order.exchange
,并使用order.completed
作为 Routing Key。 - 立即返回: 消息发送是异步的,方法在发送完消息后(不等待消费者处理完毕)就立即返回“状态更新请求已提交”的响应给
OrderController
,然后返回给前端。这使得主流程响应极快。
异步处理:
- RabbitMQ 接收到消息后,根据绑定关系,将消息路由到
order.completed.queue
队列中。 OrderMessageListener
监听到队列中有新消息,其handleOrderCompletedMessage
方法被调用。- 该方法接收到消息(订单 ID 1001),然后模拟执行一系列耗时的后续操作(如发送通知、更新报表、清除缓存等,都是打印日志)。这些操作完全在后台异步进行,不影响主流程的快速响应。
- RabbitMQ 接收到消息后,根据绑定关系,将消息路由到
这个项目通过 Token + Redis + AOP 实现了强大的接口幂等性保护,有效防止了重复提交带来的问题。同时,通过引入 RabbitMQ 消息队列,将耗时的非核心操作异步化,实现了系统核心链路的快速响应和高吞吐量,并提高了系统的可扩展性和可维护性。整个流程清晰地展示了如何在 Spring Boot 项目中应用这些重要的分布式系统设计模式。
T /order/complete/1001`。
OrderController.completeOrder(1001)
方法被调用。该方法将请求转发给
OrderStatusService.updateToCompleted(1001)
。核心状态更新与消息发送:
OrderStatusService.updateToCompleted(1001)方法执行:
- 核心业务: 模拟更新数据库订单状态为“completed”(打印日志)。
- 发送消息: 创建一个包含订单 ID 等信息的消息(
Map
),然后通过RabbitTemplate
将这个消息发送到之前定义的order.exchange
,并使用order.completed
作为 Routing Key。 - 立即返回: 消息发送是异步的,方法在发送完消息后(不等待消费者处理完毕)就立即返回“状态更新请求已提交”的响应给
OrderController
,然后返回给前端。这使得主流程响应极快。
异步处理:
- RabbitMQ 接收到消息后,根据绑定关系,将消息路由到
order.completed.queue
队列中。 OrderMessageListener
监听到队列中有新消息,其handleOrderCompletedMessage
方法被调用。- 该方法接收到消息(订单 ID 1001),然后模拟执行一系列耗时的后续操作(如发送通知、更新报表、清除缓存等,都是打印日志)。这些操作完全在后台异步进行,不影响主流程的快速响应。
- RabbitMQ 接收到消息后,根据绑定关系,将消息路由到
这个项目通过 Token + Redis + AOP 实现了强大的接口幂等性保护,有效防止了重复提交带来的问题。同时,通过引入 RabbitMQ 消息队列,将耗时的非核心操作异步化,实现了系统核心链路的快速响应和高吞吐量,并提高了系统的可扩展性和可维护性。整个流程清晰地展示了如何在 Spring Boot 项目中应用这些重要的分布式系统设计模式。
希望我的这份实战复盘能对大家有所启发。欢迎在评论区留言交流!