3D打印云平台订单模块设计与优化(高并发+幂等性+异步解耦)

发布于:2025-09-05 ⋅ 阅读:(20) ⋅ 点赞:(0)

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 技术选型与设计

常见的方案有:

  1. 数据库唯一索引:利用订单号等业务唯一键约束。简单,但灵活性差,无法覆盖所有场景。
  2. 乐观锁:通过version字段控制更新。适用于更新操作,但对插入操作不友好。
  3. Token令牌机制:服务端下发令牌,请求时校验令牌,一次有效通用、灵活、对业务零侵入,我最终选择了此方案。

核心流程设计

  1. 进入下单页时,前端先请求服务端获取一个全局唯一的Token
  2. 服务端将Token存入Redis,并设置有效期(如5分钟)。
  3. 用户提交订单时,将此Token放入HTTP请求头(如X-Idempotent-Token)中一并提交。
  4. 后端接口在执行业务逻辑前,先调用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步:前端调用序列

  1. 进入页面 -> 立马调用 GET /order/token获取Token。
  2. 提交订单 -> 携带Token (X-Idempotent-Token: ${token}) 调用 POST /order/create

3.4 成果

此方案上线后,彻底解决了重复订单和重复支付的问题。得益于AOP设计,该注解可轻松复用于支付回调、退款等重要接口,大大提升了系统的健壮性。

4. 核心实战二:异步解耦,订单状态同步毫秒级响应

订单状态变更(如“打印完成”)后,常需触发一系列“后续操作”:发送完成通知(调用api)、更新统计报表、清除缓存等。这些操作耗时且非核心链路。

解决方案:引入RabbitMQ消息队列,进行异步解耦。

设计思路:

  1. 生产者(订单服务):在完成核心的数据库状态更新后,立即向RabbitMQ发送一条消息(如order.status.completed),然后即刻返回响应。核心链路耗时极短
  2. 消费者(多个独立服务):如通知服务、报表服务等,订阅该消息。它们异步地获取消息并处理自身业务,即使处理慢或暂时失败,也不会影响主流程。

代码示例:生产者(订单服务内)

@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() 方法的执行,直接返回错误信息 “请勿重复提交请求”。
    • 如果幂等校验通过,OrderController.createOrder() 方法继续执行,模拟创建订单的业务逻辑(打印日志),然后返回成功结果给前端。

  • 触发状态变更:

    • 假设某个事件(如后台打印完成)需要将订单 ID 为 1001 的订单状态更新为“已完成”。前端或某个服务会调用:POST /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),然后模拟执行一系列耗时的后续操作(如发送通知、更新报表、清除缓存等,都是打印日志)。这些操作完全在后台异步进行,不影响主流程的快速响应。

这个项目通过 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),然后模拟执行一系列耗时的后续操作(如发送通知、更新报表、清除缓存等,都是打印日志)。这些操作完全在后台异步进行,不影响主流程的快速响应。

这个项目通过 Token + Redis + AOP 实现了强大的接口幂等性保护,有效防止了重复提交带来的问题。同时,通过引入 RabbitMQ 消息队列,将耗时的非核心操作异步化,实现了系统核心链路的快速响应和高吞吐量,并提高了系统的可扩展性和可维护性。整个流程清晰地展示了如何在 Spring Boot 项目中应用这些重要的分布式系统设计模式。

希望我的这份实战复盘能对大家有所启发。欢迎在评论区留言交流!


网站公告

今日签到

点亮在社区的每一天
去签到