基于RabbitMQ的企业级订单系统设计与实现

基于RabbitMQ的企业级订单系统设计与实现文档

一、文档概述

本文档整合了多轮沟通中关于"RabbitMQ解决订单顺序错乱""库存扣减并发控制""跨服务分布式事务""企业级下单流程"的核心内容,提供从技术方案到代码落地、从流程设计到状态流转的完整实现指南,适用于Spring Cloud微服务架构下的高并发订单场景。

二、RabbitMQ解决多订单同步顺序错乱方案

2.1 问题根源与设计原则

订单顺序错乱的核心原因是"消息生产无序"或"消息消费并行",需遵循以下设计原则:

  1. 生产端有序:同一订单的消息(创建→支付→发货)按业务顺序同步发送,通过订单号Hash路由到固定队列;
  2. 消费端串行 :同一队列配置concurrency=1(单线程消费),避免并行处理破坏顺序;
  3. 手动ACK机制:确保消息处理完成后再确认,防止消息丢失导致顺序断层;
  4. 幂等性防护:基于订单号+消息序号去重,避免重试导致重复执行业务;
  5. 队列分区路由:高并发场景下按订单号Hash分配队列,平衡"顺序性"与"并发性能"。

2.2 技术选型

组件 版本/类型 作用
核心框架 Spring Cloud 2023.0.0 微服务基础架构
消息中间件 RabbitMQ 3.12+ 消息投递与顺序控制
消息集成 Spring Cloud Stream 简化RabbitMQ配置与消息绑定
幂等性存储 Redis 订单消息去重标识存储
数据库 MySQL 订单与库存核心数据存储

2.3 核心配置与代码实现

2.3.1 生产者配置(订单服务)
yaml 复制代码
spring:
  application:
    name: order-service
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin123
    virtual-host: order-vhost
    publisher-confirm-type: correlated  # 消息确认机制
    publisher-returns: true              # 消息返回机制
  cloud:
    stream:
      rabbit:
        bindings:
          order-output:
            producer:
              durable: true              # 消息持久化
              channel-transacted: true   # 事务确保生产顺序
      bindings:
        order-output:
          destination: order-exchange    # 绑定交换机
          producer:
            exchange-type: topic         # 主题交换机(支持路由)
          content-type: application/json
      binders:
        rabbit-binder:
          type: rabbit
          environment:
            spring: rabbitmq: {host: 127.0.0.1, port: 5672, username: admin, password: admin123, virtual-host: order-vhost}
# 队列分区配置
order:
  rabbitmq:
    queue-count: 3                       # 3个队列实现并行
    routing-key-prefix: order.route.
2.3.2 消费者配置(订单同步服务)
yaml 复制代码
spring:
  application:
    name: order-sync-service
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin123
    virtual-host: order-vhost
    listener:
      simple:
        acknowledge-mode: manual         # 手动ACK
        concurrency: 1                   # 单线程消费
        max-concurrency: 1
        prefetch: 1                      # 预取1条消息,避免缓存乱序
  cloud:
    stream:
      rabbit:
        bindings:
          order-input:
            consumer:
              durable: true
              auto-bind-dlq: true        # 开启死信队列(处理失败消息)
              dlq-ttl: 86400000          # 死信消息过期时间(24h)
              republish-to-dlq: true     # 失败消息重发布到死信队列
      bindings:
        order-input:
          destination: order-exchange
          content-type: application/json
          consumer:
            exchange-type: topic
            routing-key-pattern: order.route.*  # 订阅所有订单路由键
2.3.3 核心代码(订单消息生产与消费)
(1)订单消息DTO
java 复制代码
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class OrderMessageDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String orderNo;          // 订单号(唯一标识)
    private Integer orderStatus;     // 订单状态(1-创建,2-支付,3-发货)
    private BigDecimal amount;       // 订单金额
    private LocalDateTime sendTime;  // 消息发送时间
    private Integer msgSeq;          // 消息序号(保证同订单内顺序)
}
(2)订单消息生产者(按订单号Hash路由)
typescript 复制代码
import cn.hutool.core.lang.hash.MurmurHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;

@Service
public class OrderMessageProducer {
    @Autowired
    private OrderOutputChannel orderOutputChannel;  // 自定义输出通道
    @Value("${order.rabbitmq.routing-key-prefix}")
    private String routingKeyPrefix;
    @Value("${order.rabbitmq.queue-count}")
    private Integer queueCount;

    // 发送订单消息(核心:按订单号Hash路由到固定队列)
    public void sendOrderMessage(OrderMessageDTO orderMessage) {
        // 生成路由键:订单号Hash取模,确保同订单进同一队列
        String routingKey = generateRoutingKey(orderMessage.getOrderNo());
        // 构建并发送消息
        org.springframework.messaging.Message<OrderMessageDTO> message = MessageBuilder
                .withPayload(orderMessage)
                .setHeader("spring.cloud.stream.sendto.destination", routingKey)
                .build();
        orderOutputChannel.output().send(message);
    }

    // 生成路由键(MurmurHash确保分布均匀)
    private String generateRoutingKey(String orderNo) {
        int hash = MurmurHash.hash32(orderNo);
        int queueIndex = Math.abs(hash) % queueCount;
        return routingKeyPrefix + queueIndex;
    }
}
(3)订单消息消费者(单线程+手动ACK)
java 复制代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Service;

@Service
public class OrderMessageConsumer {
    @Autowired
    private IdempotentHandler idempotentHandler;  // 幂等性工具类

    @StreamListener(OrderInputChannel.ORDER_INPUT)
    public void consumeOrderMessage(OrderMessageDTO message, Channel channel, Message amqpMessage) throws Exception {
        long deliveryTag = amqpMessage.getMessageProperties().getDeliveryTag();
        try {
            // 1. 幂等性检查:避免重复消费
            boolean isFirstProcess = idempotentHandler.checkAndSet(message.getOrderNo(), message.getMsgSeq());
            if (!isFirstProcess) {
                channel.basicAck(deliveryTag, false);
                return;
            }
            // 2. 业务处理(按订单状态执行同步逻辑)
            processOrderBusiness(message);
            // 3. 手动ACK:处理成功确认消息
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 4. 手动NACK:处理失败,消息进入死信队列
            channel.basicNack(deliveryTag, false, false);
        }
    }

    // 订单业务处理(创建/支付/发货逻辑)
    private void processOrderBusiness(OrderMessageDTO message) {
        switch (message.getOrderStatus()) {
            case 1:
                System.out.printf("同步订单创建:%s%n", message.getOrderNo());
                break;
            case 2:
                System.out.printf("同步订单支付:%s%n", message.getOrderNo());
                break;
            case 3:
                System.out.printf("同步订单发货:%s%n", message.getOrderNo());
                break;
        }
    }
}

三、多订单库存扣减并发控制

3.1 库存扣减核心风险

  1. 库存超卖:多订单同时读取库存并扣减,导致库存为负;
  2. 数据不一致:库存扣减成功但订单处理失败,或反之;
  3. 跨服务耦合:库存作为独立服务时,同步调用易导致超时或服务依赖故障。

3.2 解决方案:Redis预扣减+分布式锁+乐观锁

3.2.1 技术原理
  1. Redis预扣减:缓存商品库存,高并发下快速判断库存是否充足,减少数据库访问;
  2. 分布式锁(Redisson) :锁定商品维度,防止多订单同时扣减同一商品库存;
  3. 数据库乐观锁 :通过version字段确保库存扣减的原子性,避免并发写冲突。
3.2.2 核心代码实现
(1)库存实体与数据库设计
sql 复制代码
-- 商品库存表(含乐观锁)
CREATE TABLE product_stock (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  product_id VARCHAR(32) NOT NULL COMMENT '商品ID',
  stock_num INT NOT NULL COMMENT '当前库存',
  locked_num INT NOT NULL DEFAULT 0 COMMENT '已锁定库存(未支付订单占用)',
  version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
  create_time DATETIME NOT NULL,
  update_time DATETIME NOT NULL,
  UNIQUE KEY uk_product_id (product_id)
);
(2)库存扣减服务
typescript 复制代码
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class StockDeductService {
    @Autowired
    private ProductStockRepository stockRepository;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private StockLockUtil stockLockUtil;  // 分布式锁工具类
    private static final String REDIS_STOCK_KEY = "product:stock:";

    // 库存扣减核心方法
    public boolean deductStock(String productId, Integer buyNum, String orderNo) {
        // 1. Redis预判断库存
        String redisKey = REDIS_STOCK_KEY + productId;
        String stockStr = redisTemplate.opsForValue().get(redisKey);
        if (stockStr == null) loadStockToRedis(productId);  // 缓存未命中,从DB加载
        if (Integer.parseInt(stockStr) < buyNum) return false;

        // 2. 获取分布式锁(锁定商品)
        RLock lock = stockLockUtil.lock(productId, 3, 10);  // 等待3s,持有10s
        if (lock == null) return false;

        try {
            // 3. 数据库双重校验+乐观锁扣减
            ProductStock stock = stockRepository.findByProductId(productId)
                    .orElseThrow(() -> new RuntimeException("商品不存在:" + productId));
            if (stock.getStockNum() < buyNum) return false;
            // 乐观锁扣减:version不匹配则更新失败(返回0)
            int affectedRows = stockRepository.deductStock(stock.getId(), buyNum, stock.getVersion());
            if (affectedRows != 1) return false;

            // 4. 更新Redis缓存
            redisTemplate.opsForValue().decrement(redisKey, buyNum);
            return true;
        } finally {
            stockLockUtil.unlock(lock);  // 释放锁
        }
    }

    // 从DB加载库存到Redis
    private void loadStockToRedis(String productId) {
        ProductStock stock = stockRepository.findByProductId(productId)
                .orElseThrow(() -> new RuntimeException("商品不存在:" + productId));
        redisTemplate.opsForValue().set(REDIS_STOCK_KEY + productId, stock.getStockNum().toString());
    }

    // 库存回滚(订单取消时调用)
    @Transactional(rollbackFor = Exception.class)
    public void rollbackStock(String productId, Integer buyNum) {
        ProductStock stock = stockRepository.findByProductId(productId)
                .orElseThrow(() -> new RuntimeException("商品不存在:" + productId));
        stock.setStockNum(stock.getStockNum() + buyNum);
        stockRepository.save(stock);
        redisTemplate.opsForValue().increment(REDIS_STOCK_KEY + productId, buyNum);
    }
}
(3)分布式锁工具类
java 复制代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class StockLockUtil {
    @Autowired
    private RedissonClient redissonClient;

    // 获取商品库存锁
    public RLock lock(String productId, long waitTime, long leaseTime) {
        String lockKey = "stock:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS) ? lock : null;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    // 释放锁
    public void unlock(RLock lock) {
        if (lock != null && lock.isHeldByCurrentThread()) lock.unlock();
    }
}

四、跨服务分布式事务与一致性保障

4.1 核心问题:跨服务原子性

订单服务(A)与库存服务(B)独立部署,需确保"订单预创建+库存扣减"要么同时成功,要么同时失败,需通过分布式事务解决。

4.2 最优方案:可靠消息+最终一致性

4.2.1 方案原理

基于RabbitMQ实现"可靠消息投递+消费确认+定时补偿",确保跨服务数据最终一致:

  1. 可靠消息投递:订单服务预创建订单后,通过事务确保消息必入队;
  2. 可靠消息消费:库存服务消费消息并执行扣减,处理完成后发送结果消息;
  3. 结果处理:订单服务消费结果消息,更新订单状态(成功确认/失败回滚);
  4. 定时补偿:处理消息丢失或延迟导致的不一致。
4.2.2 跨服务流程设计
css 复制代码
graph TD
    A[用户下单] --> B[订单服务:预创建订单(状态0:待确认)]
    B --> C[订单服务:发送库存扣减消息(事务确保投递)]
    C --> D[库存服务:消费消息,执行扣减]
    D --> E{扣减成功?}
    E -->|是| F[库存服务:发送"扣减成功"消息]
    E -->|否| G[库存服务:发送"扣减失败"消息]
    F --> H[订单服务:更新订单状态为1(待支付)]
    G --> I[订单服务:删除预创建订单(回滚)]
    J[定时补偿任务] --> K[查询超时"待确认"订单]
    K --> L[调用库存服务查询扣减状态]
    L --> M{已扣减?}
    M -->|是| H
    M -->|否| I
4.2.3 核心代码实现
(1)订单服务:预创建订单与消息发送
scss 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMainRepository orderRepository;
    @Autowired
    private StockMessageChannel stockMessageChannel;  // 库存消息通道

    // 核心下单流程:预创建订单→发送库存扣减消息
    @Transactional(rollbackFor = Exception.class)
    public boolean createOrder(String orderNo, String productId, Integer buyNum) {
        try {
            // 1. 预创建订单(状态0:待确认,未占用库存)
            OrderMain preOrder = new OrderMain();
            preOrder.setOrderNo(orderNo);
            preOrder.setProductId(productId);
            preOrder.setBuyNum(buyNum);
            preOrder.setOrderStatus(0);  // 0-待确认
            orderRepository.save(preOrder);

            // 2. 发送库存扣减消息(事务确保:订单预创建成功则消息必发)
            StockDeductRequestDTO request = new StockDeductRequestDTO();
            request.setOrderNo(orderNo);
            request.setProductId(productId);
            request.setBuyNum(buyNum);
            request.setMessageId(IdUtil.fastSimpleUUID());  // 消息唯一ID(幂等)
            stockMessageChannel.stockDeductOutput().send(
                    MessageBuilder.withPayload(request).build()
            );
            return true;
        } catch (Exception e) {
            return false;  // 异常时本地事务回滚,预创建订单删除
        }
    }
}
(2)库存服务:消费扣减消息并返回结果
scss 复制代码
@Service
public class StockDeductHandler {
    @Autowired
    private StockDeductService stockDeductService;
    @Autowired
    private StockMessageChannel stockMessageChannel;
    @Autowired
    private IdempotentHandler idempotentHandler;

    @StreamListener(StockMessageChannel.STOCK_DEDUCT_INPUT)
    public void handleStockDeduct(StockDeductRequestDTO request, Channel channel, Message amqpMessage) throws Exception {
        long deliveryTag = amqpMessage.getMessageProperties().getDeliveryTag();
        String messageId = request.getMessageId();
        try {
            // 1. 幂等性检查:避免重复扣减
            if (!idempotentHandler.checkAndSet(messageId, 1)) {
                channel.basicAck(deliveryTag, false);
                return;
            }
            // 2. 执行库存扣减
            boolean success = stockDeductService.deductStock(
                    request.getProductId(), request.getBuyNum(), request.getOrderNo()
            );
            // 3. 发送扣减结果
            StockDeductResultDTO result = new StockDeductResultDTO();
            result.setOrderNo(request.getOrderNo());
            result.setMessageId(messageId);
            result.setSuccess(success);
            stockMessageChannel.stockResultOutput().send(
                    MessageBuilder.withPayload(result).build()
            );
            // 4. 确认消息
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 5. 失败处理:发送失败结果+拒绝消息
            StockDeductResultDTO failResult = new StockDeductResultDTO();
            failResult.setOrderNo(request.getOrderNo());
            failResult.setMessageId(messageId);
            failResult.setSuccess(false);
            stockMessageChannel.stockResultOutput().send(MessageBuilder.withPayload(failResult).build());
            channel.basicNack(deliveryTag, false, false);
        }
    }
}
(3)订单服务:消费扣减结果并更新状态
scss 复制代码
@Service
public class StockResultHandler {
    @Autowired
    private OrderMainRepository orderRepository;

    @StreamListener(StockMessageChannel.STOCK_RESULT_INPUT)
    @Transactional(rollbackFor = Exception.class)
    public void handleStockResult(StockDeductResultDTO result) {
        String orderNo = result.getOrderNo();
        OrderMain order = orderRepository.findByOrderNo(orderNo)
                .orElseThrow(() -> new RuntimeException("订单不存在:" + orderNo));
        
        if (result.isSuccess()) {
            // 扣减成功:更新订单状态为1(待支付)
            order.setOrderStatus(1);
            order.setStockDeductStatus(1);
        } else {
            // 扣减失败:删除预创建订单(回滚)
            orderRepository.delete(order);
        }
        orderRepository.save(order);
    }
}
(4)定时补偿任务(确保最终一致性)
scss 复制代码
@Component
public class OrderCompensateTask {
    @Autowired
    private OrderMainRepository orderRepository;
    @Autowired
    private StockFeignClient stockFeignClient;  // 调用库存服务的Feign接口

    // 每5分钟执行:处理超时"待确认"订单
    @Scheduled(cron = "0 0/5 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void compensatePendingOrder() {
        LocalDateTime tenMinutesAgo = LocalDateTime.now().minus(10, ChronoUnit.MINUTES);
        // 查询10分钟前仍为"待确认"的订单
        List<OrderMain> pendingOrders = orderRepository.findByOrderStatusAndCreateTimeBefore(0, tenMinutesAgo);
        
        for (OrderMain order : pendingOrders) {
            // 调用库存服务查询扣减状态
            boolean hasDeducted = stockFeignClient.checkStockDeducted(order.getOrderNo());
            if (hasDeducted) {
                // 库存已扣减:更新订单状态
                order.setOrderStatus(1);
                order.setStockDeductStatus(1);
                orderRepository.save(order);
            } else {
                // 库存未扣减:删除订单
                orderRepository.delete(order);
            }
        }
    }
}

五、企业级下单全流程与订单状态流转

5.1 预创建订单与普通创建的区别

对比维度 预创建订单(临时订单) 普通创建订单(最终订单)
核心目的 占订单号、为跨服务回滚留出口 确认交易生效,支持后续履约(支付/发货)
状态属性 临时状态(可删除/回滚) 正式状态(不可随意修改)
资源占用 不占用库存、优惠券等核心资源 占用库存、锁定优惠券
回滚成本 极低(直接删除临时记录) 极高(需回滚库存、解锁资源)
业务意义 告知用户"订单处理中" 告知用户"订单创建成功"

5.2 订单核心状态定义

状态值 状态名称 触发条件 业务含义
0 待确认(预创建) 用户提交订单,订单服务预创建记录 订单临时占位,等待库存扣减等前置条件
1 待支付 库存扣减成功,订单服务更新状态 订单正式创建,等待用户支付
2 已支付/待发货 用户完成支付,支付服务通知订单服务 支付完成,等待商家发货
3 已发货 商家操作发货,订单服务同步物流信息 商品已发出,等待用户收货
4 已完成 用户确认收货或系统超时自动确认 订单闭环,交易完成
5 已取消 用户取消未支付订单,或退款完成 订单终止,资源(库存/优惠券)回滚
6 创建失败 库存扣减失败、参数校验失败等 订单未创建成功,无资源占用

5.3 完整状态流转链路

5.3.1 正常下单链路

用户提交订单 → 预创建订单(0:待确认) → 库存扣减成功 → 订单状态1(待支付) → 用户支付成功 → 订单状态2(已支付/待发货) → 商家发货 → 订单状态3(已发货) → 用户确认收货 → 订单状态4(已完成)。

5.3.2 异常场景链路
  1. 库存不足:用户提交订单 → 预创建订单(0) → 库存扣减失败 → 删除订单 → 状态6(创建失败);
  2. 支付超时:订单状态1(待支付) → 超时未支付 → 订单状态5(已取消) → 库存回滚;
  3. 支付后退款:订单状态2(已支付) → 用户申请退款 → 商家同意 → 支付服务退款 → 订单状态5(已取消) → 库存回滚。

六、总结

  1. 订单顺序控制:基于RabbitMQ队列分区+单线程消费,确保同订单串行、多订单并行,解决顺序错乱问题;
  2. 库存并发控制:通过Redis预扣减+Redisson分布式锁+数据库乐观锁,防止超卖并保证数据一致性;
  3. 跨服务事务:采用"可靠消息+最终一致性"方案,结合预创建订单和定时补偿,解决跨服务原子性问题;
  4. 企业级流程:以"预创建→前置校验→确认订单→履约"为核心,通过状态流转实现订单全生命周期管理,兼顾高并发与数据一致性。

本方案可直接落地到Spring Cloud微服务架构的订单系统,支持高并发场景下的稳定运行,同时具备良好的扩展性(如新增优惠券、物流等模块可复用消息机制与事务方案)。

相关推荐
LSTM971 小时前
使用 Java 实现条形码生成与识别
后端
哈哈哈笑什么1 小时前
如何防止恶意伪造前端唯一请求id
前端·后端
哈哈哈笑什么1 小时前
Spring Cloud 微服务架构下幂等性的 业务场景、解决的核心问题、完整实现方案及可运行代码
后端
PieroPC1 小时前
飞牛Nas-通过Docker的Compose 安装WordPress
后端
shengjk11 小时前
当10万天分区来袭:一个让StarRocks崩溃、Kudu拒绝、HDFS微笑的架构故事
后端
一 乐1 小时前
鲜花销售|基于springboot+vue的鲜花销售系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
T.O.P_KING2 小时前
Common Go Mistakes(IV 字符串)
开发语言·后端·golang
盒马盒马2 小时前
Rust:Trait 标签 & 常见特征
开发语言·后端·rust
韩立学长2 小时前
基于Springboot儿童福利院规划管理系统o292y1v8(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端