【架构实战】订单系统架构设计:电商核心系统的演进

一、一次库存超卖让我损失了10万

2019年双十一,我负责的订单系统出了一个大Bug:库存超卖了。

当时库存只剩100件,但一下卖了300件。供应商那边打电话过来问责,财务也算不出来到底多发了多少货。

后来复盘原因:并发情况下,库存扣减的SQL没有加锁,导致重复扣减。

那一次,我花了3天时间才把数据对清楚。从此我对订单系统充满了敬畏。


二、订单系统核心设计

2.1 订单系统架构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                         订单系统架构                              │
│                                                                  │
│  ┌──────────────┐      ┌──────────────┐      ┌──────────────┐ │
│  │   用户端     │      │   商家端     │      │   运营端     │ │
│  │  Web/APP    │      │  Web后台     │      │  管理后台    │ │
│  └──────┬───────┘      └──────┬───────┘      └──────┬───────┘ │
│         │                     │                     │          │
│         └─────────────────────┼─────────────────────┘          │
│                               ↓                                  │
│                    ┌──────────────────┐                          │
│                    │   API Gateway    │                          │
│                    └────────┬─────────┘                          │
│                             ↓                                    │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                      订单服务                                ││
│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐           ││
│  │  │ 订单创建   │  │ 订单支付   │  │ 订单发货   │           ││
│  │  └────────────┘  └────────────┘  └────────────┘           ││
│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐           ││
│  │  │ 订单完成   │  │ 订单取消   │  │ 订单售后   │           ││
│  │  └────────────┘  └────────────┘  └────────────┘           ││
│  └─────────────────────────────────────────────────────────────┘│
│                               ↓                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────┐   │
│  │ 库存服务 │  │ 支付服务  │  │ 用户服务  │  │  消息队列    │   │
│  └──────────┘  └──────────┘  └──────────┘  └──────────────┘   │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

2.2 订单核心模型

java 复制代码
/**
 * 订单主表
 */
@Entity
@Table(name = "orders")
@Data
public class Order {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 订单号(唯一)
     */
    @Column(unique = true, nullable = false)
    private String orderNo;
    
    /**
     * 用户ID
     */
    private Long userId;
    
    /**
     * 订单状态
     * PENDING-待支付, PAID-已支付, SHIPPED-已发货, 
     * CONFIRMED-已确认, COMPLETED-已完成, CANCELLED-已取消
     */
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    /**
     * 订单总金额(分)
     */
    private Integer totalAmount;
    
    /**
     * 实付金额
     */
    private Integer payAmount;
    
    /**
     * 运费
     */
    private Integer freight;
    
    /**
     * 优惠金额
     */
    private Integer discountAmount;
    
    /**
     * 支付方式
     */
    @Enumerated(EnumType.STRING)
    private PayType payType;
    
    /**
     * 支付时间
     */
    private LocalDateTime payTime;
    
    /**
     * 发货时间
     */
    private LocalDateTime shipTime;
    
    /**
     * 完成时间
     */
    private LocalDateTime completeTime;
    
    /**
     * 收货地址ID
     */
    private Long addressId;
    
    /**
     * 备注
     */
    private String remark;
    
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

/**
 * 订单商品表
 */
@Entity
@Table(name = "order_items")
@Data
public class OrderItem {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 订单ID
     */
    private Long orderId;
    
    /**
     * 订单号
     */
    private String orderNo;
    
    /**
     * 商品ID
     */
    private Long productId;
    
    /**
     * 商品名称
     */
    private String productName;
    
    /**
     * 商品图片
     */
    private String productImage;
    
    /**
     * SKU ID
     */
    private Long skuId;
    
    /**
     * SKU规格
     */
    private String skuSpec;
    
    /**
     * 单价(分)
     */
    private Integer price;
    
    /**
     * 购买数量
     */
    private Integer quantity;
    
    /**
     * 小计金额
     */
    private Integer totalAmount;
}

三、订单状态机设计

3.1 状态流转图

复制代码
                    ┌──────────┐
                    │  待支付  │
                    │ PENDING │
                    └────┬─────┘
                         │ 支付成功
                         ↓
         ┌───────────────┴───────────────┐
         │                               │
    ┌────▼─────┐                   ┌────▼─────┐
    │  已支付  │                   │  已取消  │
    │  PAID   │                   │CANCELLED│
    └────┬─────┘                   └─────────┘
         │ 发货
         ↓
    ┌────▼─────┐
    │  已发货  │
    │ SHIPPED  │
    └────┬─────┘
         │ 确认收货
         ↓
    ┌────▼─────┐
    │ 已确认   │
    │CONFIRMED │
    └────┬─────┘
         │ 完成(系统/用户)
         ↓
    ┌────▼─────┐
    │  已完成  │
    │COMPLETED │
    └──────────┘

3.2 状态机实现

java 复制代码
/**
 * 订单状态机
 */
@Service
@Slf4j
public class OrderStateMachine {
    
    /**
     * 状态流转规则
     */
    private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of(
        OrderStatus.PENDING, Set.of(OrderStatus.PAID, OrderStatus.CANCELLED),
        OrderStatus.PAID, Set.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED),
        OrderStatus.SHIPPED, Set.of(OrderStatus.CONFIRMED),
        OrderStatus.CONFIRMED, Set.of(OrderStatus.COMPLETED),
        OrderStatus.COMPLETED, Set.of(),  // 终态,不可再变
        OrderStatus.CANCELLED, Set.of()   // 终态,不可再变
    );
    
    /**
     * 检查状态是否可以流转
     */
    public boolean canTransition(OrderStatus current, OrderStatus target) {
        Set<OrderStatus> allowed = TRANSITIONS.get(current);
        return allowed != null && allowed.contains(target);
    }
    
    /**
     * 执行状态流转
     */
    @Transactional
    public void transition(Order order, OrderStatus targetStatus) {
        OrderStatus currentStatus = order.getStatus();
        
        // 检查是否可以流转
        if (!canTransition(currentStatus, targetStatus)) {
            throw new BusinessException(
                String.format("订单状态不允许从 %s 流转到 %s", currentStatus, targetStatus)
            );
        }
        
        // 执行流转前的钩子
        beforeTransition(order, currentStatus, targetStatus);
        
        // 更新状态
        order.setStatus(targetStatus);
        order.setUpdateTime(LocalDateTime.now());
        
        // 记录状态变更日志
        saveStatusLog(order, currentStatus, targetStatus);
        
        // 执行流转后的钩子
        afterTransition(order, currentStatus, targetStatus);
    }
    
    private void beforeTransition(Order order, OrderStatus from, OrderStatus to) {
        // 支付前:检查库存
        if (to == OrderStatus.PAID) {
            checkInventory(order);
        }
        // 取消前:检查是否可取消
        if (to == OrderStatus.CANCELLED) {
            checkCancelable(order);
        }
    }
    
    private void afterTransition(Order order, OrderStatus from, OrderStatus to) {
        // 支付成功:发送消息通知
        if (to == OrderStatus.PAID) {
            onOrderPaid(order);
        }
        // 发货:通知物流
        if (to == OrderStatus.SHIPPED) {
            onOrderShipped(order);
        }
    }
}

四、库存扣减方案

4.1 乐观锁方案

java 复制代码
/**
 * 库存服务
 */
@Service
@Slf4j
public class InventoryService {
    
    /**
     * 乐观锁扣减库存
     * 
     * UPDATE inventory 
     * SET stock = stock - #{quantity}, version = version + 1 
     * WHERE sku_id = #{skuId} AND stock >= #{quantity} AND version = #{version}
     */
    public boolean deductStockOptimistic(Long skuId, Integer quantity) {
        int affected = inventoryMapper.deductWithOptimisticLock(skuId, quantity);
        return affected > 0;
    }
    
    /**
     * 库存扣减(带重试)
     */
    public boolean deductStockWithRetry(Long skuId, Integer quantity, int maxRetries) {
        for (int i = 0; i < maxRetries; i++) {
            try {
                if (deductStockOptimistic(skuId, quantity)) {
                    return true;
                }
            } catch (Exception e) {
                log.warn("库存扣减重试 {}/{}: skuId={}, quantity={}", 
                    i + 1, maxRetries, skuId, quantity);
            }
            // 短暂等待后重试
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return false;
    }
}

/**
 * Mapper
 */
@Mapper
public interface InventoryMapper {
    
    @Update("UPDATE inventory SET stock = stock - #{quantity}, " +
            "version = version + 1, update_time = NOW() " +
            "WHERE sku_id = #{skuId} AND stock >= #{quantity} AND version = #{version}")
    int deductWithOptimisticLock(@Param("skuId") Long skuId, 
                                  @Param("quantity") Integer quantity);
}

4.2 Redis分布式锁方案

java 复制代码
@Service
@Slf4j
public class InventoryLockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LOCK_PREFIX = "inventory:lock:";
    private static final Duration LOCK_EXPIRE = Duration.ofSeconds(10);
    
    /**
     * 扣减库存(带分布式锁)
     */
    public boolean deductStockWithLock(Long skuId, Integer quantity) {
        String lockKey = LOCK_PREFIX + skuId;
        
        // 获取锁
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", LOCK_EXPIRE);
        
        if (!Boolean.TRUE.equals(acquired)) {
            log.warn("获取库存锁失败: skuId={}", skuId);
            return false;
        }
        
        try {
            // 执行业务逻辑
            return doDeductStock(skuId, quantity);
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
    }
    
    /**
     * Lua脚本扣减库存(原子操作)
     * 
     * KEYS[1]: 库存key
     * ARGV[1]: 扣减数量
     */
    private static final String DEDUCT_SCRIPT = 
        "if redis.call('exists', KEYS[1]) == 1 then " +
        "    local stock = tonumber(redis.call('get', KEYS[1])) " +
        "    if stock >= tonumber(ARGV[1]) then " +
        "        return redis.call('decrby', KEYS[1], ARGV[1]) " +
        "    end " +
        "    return -1 " +
        "end " +
        "return -2";
    
    public Long deductStockWithLua(Long skuId, Integer quantity) {
        String stockKey = "inventory:stock:" + skuId;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(DEDUCT_SCRIPT, Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(stockKey), 
            String.valueOf(quantity));
        return result;
    }
}

五、订单幂等设计

5.1 幂等Token机制

java 复制代码
/**
 * 幂等服务
 */
@Service
@Slf4j
public class IdempotenceService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String IDEMPOTENCE_PREFIX = "idempotence:";
    private static final Duration EXPIRE_TIME = Duration.ofHours(24);
    
    /**
     * 生成幂等Token
     */
    public String generateToken(String bizType, String bizId) {
        String token = UUID.randomUUID().toString().replace("-", "");
        String key = IDEMPOTENCE_PREFIX + bizType + ":" + bizId + ":" + token;
        redisTemplate.opsForValue().set(key, "1", EXPIRE_TIME);
        return token;
    }
    
    /**
     * 检查并使用幂等Token
     * 
     * @return true: 首次执行, false: 重复请求
     */
    public boolean checkAndUseToken(String bizType, String bizId, String token) {
        String key = IDEMPOTENCE_PREFIX + bizType + ":" + bizId + ":" + token;
        Boolean success = redisTemplate.delete(key);
        return Boolean.TRUE.equals(success);
    }
}

/**
 * 订单Controller
 */
@RestController
@RequestMapping("/api/order")
@Slf4j
public class OrderController {
    
    @Autowired
    private IdempotenceService idempotenceService;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 获取幂等Token
     */
    @GetMapping("/idempotence-token")
    public Result<String> getIdempotenceToken() {
        String token = idempotenceService.generateToken("create_order", 
            String.valueOf(System.currentTimeMillis()));
        return Result.success(token);
    }
    
    /**
     * 创建订单(幂等)
     */
    @PostMapping("/create")
    public Result<Order> createOrder(
            @RequestBody @Validated CreateOrderRequest request,
            @RequestHeader("X-Idempotence-Token") String token) {
        
        // 验证幂等性
        if (!idempotenceService.checkAndUseToken("create_order", 
                request.getUserId() + ":" + request.getIdempotenceKey(), token)) {
            throw new BusinessException("重复请求,请勿重复提交");
        }
        
        Order order = orderService.createOrder(request);
        return Result.success(order);
    }
}

5.2 数据库唯一索引

java 复制代码
/**
 * 订单创建(防重)
 */
@Service
@Slf4j
public class OrderIdempotenceService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Transactional
    public Order createOrderIdempotent(CreateOrderRequest request) {
        // 幂等Key:用户ID + 业务类型 + 业务ID
        String idempotentKey = request.getUserId() + "_" + request.getBizType() 
            + "_" + request.getBizId();
        
        // 1. 检查是否已存在
        Order existing = orderMapper.findByIdempotentKey(idempotentKey);
        if (existing != null) {
            log.info("订单已存在,返回已有订单: {}", existing.getOrderNo());
            return existing;
        }
        
        // 2. 创建订单
        Order order = buildOrder(request);
        order.setIdempotentKey(idempotentKey);
        orderMapper.insert(order);
        
        return order;
    }
}
sql 复制代码
-- 订单表添加唯一索引
ALTER TABLE orders ADD UNIQUE INDEX idx_idempotent_key (idempotent_key);

六、实战:订单创建流程

6.1 完整流程

java 复制代码
@Service
@Slf4j
public class OrderCreateService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private MessageProducer messageProducer;
    
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(CreateOrderRequest request) {
        long startTime = System.currentTimeMillis();
        
        try {
            // 1. 验证请求参数
            validateRequest(request);
            
            // 2. 查询用户信息
            User user = userService.getUserById(request.getUserId());
            if (user == null) {
                throw new BusinessException("用户不存在");
            }
            
            // 3. 校验商品和价格
            List<OrderItem> items = validateAndBuildItems(request);
            
            // 4. 计算订单金额
            Integer totalAmount = calculateTotalAmount(items);
            Integer payAmount = calculatePayAmount(totalAmount, request.getCouponIds());
            
            // 5. 校验库存并扣减(乐观锁)
            for (OrderItem item : items) {
                boolean deducted = inventoryService.deductStockWithRetry(
                    item.getSkuId(), item.getQuantity(), 3);
                if (!dedicated) {
                    throw new BusinessException("商品库存不足: " + item.getProductName());
                }
            }
            
            // 6. 创建订单
            Order order = new Order();
            order.setOrderNo(generateOrderNo());
            order.setUserId(request.getUserId());
            order.setStatus(OrderStatus.PENDING);
            order.setTotalAmount(totalAmount);
            order.setPayAmount(payAmount);
            order.setFreight(calculateFreight(totalAmount));
            order.setDiscountAmount(totalAmount - payAmount);
            order.setAddressId(request.getAddressId());
            order.setRemark(request.getRemark());
            order.setCreateTime(LocalDateTime.now());
            order.setUpdateTime(LocalDateTime.now());
            
            orderMapper.insert(order);
            
            // 7. 保存订单商品
            for (OrderItem item : items) {
                item.setOrderId(order.getId());
                item.setOrderNo(order.getOrderNo());
                orderItemMapper.insert(item);
            }
            
            // 8. 发送订单创建消息(异步)
            messageProducer.send("order-created", order);
            
            log.info("订单创建成功: orderNo={}, userId={}, duration={}ms",
                order.getOrderNo(), order.getUserId(), 
                System.currentTimeMillis() - startTime);
            
            return order;
            
        } catch (Exception e) {
            log.error("订单创建失败: userId={}", request.getUserId(), e);
            throw e;
        }
    }
}

七、踩坑实录

坑1:库存超卖

并发情况下,多个请求同时读取库存余额,都认为还有库存,导致超卖。

解决:使用乐观锁或分布式锁,确保原子性扣减。

坑2:订单重复创建

用户快速点击两次创建订单按钮,产生重复订单。

解决:前端防重 + 后端幂等Token + 数据库唯一索引。

坑3:状态机混乱

手动修改数据库状态后,系统逻辑出现问题。

解决:所有状态变更必须通过状态机,不允许直接修改。

坑4:分库分表后订单号冲突

订单号自增在分库分表后出现重复。

解决:使用雪花算法生成订单号,不依赖数据库自增。

坑5:消息重复消费

消息队列消费失败重试后,产生重复处理。

解决:消费者端实现幂等,通过Redis或数据库记录处理状态。


八、总结

订单系统是电商核心:

  • 状态机:规范订单状态流转
  • 库存扣减:乐观锁/分布式锁确保原子性
  • 幂等设计:防止重复提交
  • 消息异步:解耦和提升性能

最佳实践:

  1. 库存扣减必须用乐观锁或分布式锁
  2. 订单状态变更必须走状态机
  3. 核心接口必须支持幂等
  4. 异步消息要保证最终一致性

血的教训:

订单系统的坑太多了,每一个都可能导致资损。建议新系统上线前充分压测,并且做好对账和异常处理预案。

思考题: 你的订单系统有没有遇到过并发问题?是如何解决的?


个人观点,仅供参考

相关推荐
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章03:MapReduce编程模型深度解读
大数据·人工智能·hadoop·学习·架构·高炉炼铁·高炉智能化
蓝速科技1 小时前
3D 数字人全息舱算力部署方案对比:本地 X86 独显架构与云端 RK 架构怎么选才好
数据结构·人工智能·算法·架构·排序算法
Regentsoft丽晶软件1 小时前
传统单体架构拖垮分销效率:2026品牌分销系统微服务化升级的价值拆解
微服务·云原生·架构
企客宝CRM1 小时前
从需求到架构:企客宝企微版小红书聚光获客链接系统设计方法论
架构·企业微信
2601_957888561 小时前
数字化转型下,企业新媒体矩阵系统的底层架构与选型实践
矩阵·架构·媒体
元气少女小圆丶1 小时前
SenseGlove Nova 2+Unity开发笔记3
笔记·unity·游戏引擎
杨云龙UP2 小时前
Oracle CDB巡检脚本使用SOP:从HTML原始报告到Word正式交付_2026-05-29
运维·服务器·数据库·oracle·架构·html·巡检
2301_780029042 小时前
互联网架构演进精读:从单机到云原生
云原生·架构
正在走向自律2 小时前
架构进阶:从 Docker 环境变量到 Nacos 统一配置中心实战
docker·容器·架构