208|订单系统设计:状态机、库存、支付、超时关单、幂等和对账

架构设计与系统设计约 7 分钟阅读4,102 字符
知识库文档 209 / 251

阅读时间:约 45 分钟 | 难度:专家级 | 分类:13 架构设计与系统设计


一、问题定义与核心挑战

订单系统是电商、金融、O2O 等业务中最核心的领域之一。它不只是一个 CRUD 模块,而是一个需要同时处理高并发、强一致性、复杂状态流转、多系统协同的分布式系统。

1.1 订单系统的核心职责

  • 生命周期管理:从创建到完成的完整状态流转
  • 库存协调:下单时扣库存,取消时回补,保证不超卖
  • 支付对接:集成第三方支付,处理同步/异步通知
  • 超时管控:未支付订单自动关闭,释放资源
  • 幂等保障:防止重复下单、重复支付、重复回调
  • 对账清算:与支付渠道、库存、财务多方对账

1.2 核心挑战

挑战 描述 影响
高并发扣减 秒杀/大促瞬时流量冲击 超卖、库存不一致
状态一致性 订单状态在多个系统间同步 状态错乱、用户困惑
分布式事务 下单涉及订单、库存、优惠券、积分等多个服务 数据不一致
支付回调不可靠 第三方支付回调可能延迟、重复、丢失 订单状态卡死
超时处理精准性 海量订单需要精确的超时关单 资源浪费、库存错乱

二、订单状态机设计

文章配图
文章配图

图7:订单系统整体架构图

2.1 状态机建模原则

订单状态机是整个系统的骨架。一个设计良好的状态机应该满足:

  • 确定性:给定当前状态和事件,下一状态唯一确定
  • 完备性:覆盖所有合法路径和异常路径
  • 可追溯性:每个状态变更都有日志记录
  • 幂等性:同一事件重复到达不会导致状态跳变

2.2 典型订单状态流转

code
                    ┌──────────┐
                    │  待支付   │ ← 下单成功
                    └────┬─────┘
                         │
              ┌──────────┼──────────┐
              │ 支付成功  │          │ 超时/取消
              ▼          │          ▼
        ┌──────────┐     │    ┌──────────┐
        │  待发货   │     │    │  已取消   │
        └────┬─────┘     │    └──────────┘
             │           │
             ▼           │
        ┌──────────┐     │
        │  待收货   │     │
        └────┬─────┘     │
             │           │
        ┌────┴────┐      │
        │ 确认收货 │      │
        ▼         ▼      │
   ┌────────┐ ┌────────┐ │
   │ 已完成  │ │ 待评价  │ │
   └────────┘ └───┬────┘ │
                  │      │
                  ▼      │
             ┌────────┐  │
             │ 已完成  │  │
             └────────┘  │
                         │
              ┌──────────┘
              │ 退款/退货
              ▼
        ┌──────────┐
        │  退款中   │
        └────┬─────┘
             │
             ▼
        ┌──────────┐
        │  已退款   │
        └──────────┘

2.3 状态机实现方案

文章配图
文章配图

图1:订单状态机流转图

方案一:状态枚举 + if-else(不推荐)

java
// 反模式:状态流转逻辑散落各处
if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
    if (event == Event.PAY_SUCCESS) {
        order.setStatus(OrderStatus.PENDING_SHIPPING);
    } else if (event == Event.TIMEOUT) {
        order.setStatus(OrderStatus.CANCELLED);
    }
}

问题:状态流转逻辑分散在多个 Service 中,难以维护,新增状态需要改动大量代码。

方案二:状态模式(State Pattern)

java
public interface OrderState {
    OrderState onEvent(OrderEvent event);
    OrderStatus getStatus();
}

public class PendingPaymentState implements OrderState {
    @Override
    public OrderState onEvent(OrderEvent event) {
        return switch (event) {
            case PAY_SUCCESS -> new PendingShippingState();
            case USER_CANCEL, TIMEOUT -> new CancelledState();
            default -> throw new IllegalStateTransitionException(
                getStatus(), event);
        };
    }
}

优点:每个状态独立封装,符合开闭原则。 缺点:状态类膨胀,状态间共享数据不方便。

方案三:状态机框架(推荐)

使用 Spring Statemachine 或自研轻量状态机:

java
// 定义状态转移
StateMachineBuilder<OrderStatus, OrderEvent> builder = 
    StateMachineBuilder.builder();

builder.configureTransitions()
    .withExternal()
        .source(OrderStatus.PENDING_PAYMENT)
        .target(OrderStatus.PENDING_SHIPPING)
        .event(OrderEvent.PAY_SUCCESS)
    .and()
    .withExternal()
        .source(OrderStatus.PENDING_PAYMENT)
        .target(OrderStatus.CANCELLED)
        .event(OrderEvent.TIMEOUT)
    .and()
    .withExternal()
        .source(OrderStatus.PENDING_SHIPPING)
        .target(OrderStatus.PENDING_RECEIVING)
        .event(OrderEvent.SHIPPED);

StateMachine<OrderStatus, OrderEvent> machine = builder.build();

2.4 状态机持久化

状态变更必须持久化记录,这是对账和问题排查的基础:

sql
CREATE TABLE order_status_log (
    id BIGINT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    from_status VARCHAR(32),
    to_status VARCHAR(32) NOT NULL,
    event VARCHAR(64) NOT NULL,
    operator VARCHAR(64),      -- 操作者:用户/系统/定时任务
    request_id VARCHAR(128),   -- 幂等键
    remark VARCHAR(512),
    created_at DATETIME NOT NULL,
    INDEX idx_order_id (order_id),
    INDEX idx_request_id (request_id)
);

关键设计点:

  1. 状态变更使用 CAS(Compare-And-Swap) 更新,而非直接 SET:
sql
UPDATE orders SET status = 'PENDING_SHIPPING', version = version + 1
WHERE order_id = ? AND status = 'PENDING_PAYMENT' AND version = ?;

如果 affected rows = 0,说明状态已被其他请求修改,需要检查或重试。 2. 状态变更日志和订单状态更新放在同一个本地事务中,保证原子性。


三、库存扣减设计

3.1 核心矛盾:一致性 vs 性能

  • 强一致性:UPDATE inventory SET stock = stock - ? WHERE stock >= ?(行锁,性能差)
  • 最终一致性:异步扣减,存在超卖风险(性能好)

3.2 库存扣减方案对比

方案 原理 一致性 性能 适用场景
数据库行锁 UPDATE ... WHERE stock &amp;gt;= ? 强 低 低并发
Redis 扣减 DECR 原子操作 强(单机) 高 中等并发
Redis + Lua Lua 脚本原子校验+扣减 强(单机) 高 高并发
库存预热 提前将库存加载到 Redis 最终 极高 秒杀
异步扣减 下单后异步扣减,下单时只校验 最终 极高 超高并发

3.3 库存预热方案(秒杀场景)

code
┌─────────────────────────────────────────────────────┐
│                    库存预热流程                        │
├─────────────────────────────────────────────────────┤
│  DB: stock = 1000                                    │
│         │ 预热                                        │
│         ▼                                            │
│  Redis: stock = 1000                                 │
│         │ 下单扣减                                    │
│         ▼                                            │
│  Redis: DECR stock → 库存扣减完成                     │
│         │ 异步同步                                    │
│         ▼                                            │
│  DB: UPDATE stock = stock - delta                    │
│         │ 定时对账                                    │
│         ▼                                            │
│  DB vs Redis 差异补偿                                 │
└─────────────────────────────────────────────────────┘

Lua 脚本实现原子扣减:

lua
-- redis_stock_deduct.lua
local key = KEYS[1]          -- 库存 key
local quantity = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or 0)

if current >= quantity then
    redis.call('DECRBY', key, quantity)
    return 1  -- 扣减成功
else
    return 0  -- 库存不足
end

3.4 下单与库存扣减的时序

文章配图
文章配图

图2:库存预热与扣减流程

关键问题:先扣库存还是先生成订单?

推荐方案:先扣库存,成功后再生成订单。

code
1. 扣减库存(Redis DECR)
   ├─ 成功 → 2
   └─ 失败 → 返回"库存不足"

2. 生成订单(DB INSERT)
   ├─ 成功 → 3
   └─ 失败 → 回补库存(Redis INCR)

3. 发送订单创建消息(MQ)
   └─ 异步处理后续流程

回补库存的风险:如果 DB INSERT 失败但 Redis INCR 也失败,库存会丢失。解决方案:

  • 使用 Redis 事务或 Lua 脚本保证扣减+回补的原子性
  • 定时任务扫描"已扣库存但无订单"的记录,自动回补

3.5 库存扣减中的并发问题

热点库存问题

当大量请求同时扣减同一个 SKU 的库存时,单 Key 的 Redis 操作会成为瓶颈。

解决方案:库存分片

code
将 stock = 1000 拆分为 10 个分片:
stock:0 = 100
stock:1 = 100
...
stock:9 = 100

扣减时随机选一片,扣减失败再选下一片。

四、支付集成

文章配图
文章配图

图8:库存扣减并发场景示意图

4.1 支付流程全景

code
用户                订单系统              支付渠道(支付宝/微信)
 │                    │                         │
 │  1.提交订单         │                         │
 ├───────────────────►│                         │
 │                    │  2.生成支付单            │
 │                    │  3.调用支付接口           │
 │                    ├────────────────────────►│
 │  4.返回支付链接     │                         │
 │◄───────────────────┤                         │
 │                    │                         │
 │  5.用户完成支付 ──────────────────────────────►│
 │                    │                         │
 │                    │  6.同步回调(支付成功)      │
 │                    │◄────────────────────────┤
 │                    │                         │
 │                    │  7.异步通知(支付成功)      │
 │                    │◄────────────────────────┤
 │                    │                         │
 │  8.支付成功页面     │                         │
 │◄───────────────────┤                         │

4.2 支付回调的可靠性保障

文章配图
文章配图

图3:支付集成时序图

核心问题:支付渠道的回调可能延迟、丢失、重复。

同步回调处理

支付渠道在用户支付成功后立即回调(通常是跳转回商户页面),不可靠(用户可能关闭页面)。

异步通知处理

支付渠道后台异步通知商户服务器,相对可靠但仍有丢失可能。

主动查询兜底

java
// 定时任务:每5分钟扫描未支付订单
@Scheduled(fixedDelay = 300_000)
public void queryPaymentStatus() {
    List<Order> pendingOrders = orderRepository
        .findByStatusAndCreatedBefore(
            OrderStatus.PENDING_PAYMENT,
            LocalDateTime.now().minusMinutes(5)
        );
    
    for (Order order : pendingOrders) {
        PaymentResult result = paymentGateway.query(order.getPayNo());
        if (result.isSuccess()) {
            orderStateMachine.fire(order, OrderEvent.PAY_SUCCESS);
        }
    }
}

4.3 支付回调的幂等处理

支付回调可能重复到达,需要严格幂等(详见第六节)。

java
public void handlePaymentCallback(PaymentCallback callback) {
    String idempotentKey = callback.getOutTradeNo(); // 商户订单号作为幂等键
    
    // 1. 幂等检查
    if (paymentLogRepository.existsByRequestId(idempotentKey)) {
        log.info("重复回调,忽略: {}", idempotentKey);
        return;
    }
    
    // 2. 记录回调日志(幂等键唯一约束)
    try {
        paymentLogRepository.insert(callback, idempotentKey);
    } catch (DuplicateKeyException e) {
        log.info("并发回调,幂等拦截: {}", idempotentKey);
        return;
    }
    
    // 3. 更新订单状态(CAS)
    int updated = orderRepository.updateStatus(
        callback.getOrderId(),
        OrderStatus.PENDING_PAYMENT,  // 期望状态
        OrderStatus.PENDING_SHIPPING   // 目标状态
    );
    
    if (updated == 0) {
        log.warn("订单状态已变更,可能已处理: {}", callback.getOrderId());
    }
}

4.4 支付金额校验

必须在回调处理中校验支付金额,防止金额篡改:

java
public void handlePaymentCallback(PaymentCallback callback) {
    Order order = orderRepository.findById(callback.getOrderId());
    
    // 金额校验:支付金额必须等于订单金额
    if (!callback.getPayAmount().equals(order.getTotalAmount())) {
        log.error("支付金额异常: order={}, expected={}, actual={}",
            order.getId(), order.getTotalAmount(), callback.getPayAmount());
        // 人工介入处理
        alertService.sendAbnormalPaymentAlert(order, callback);
        return;
    }
    
    // 继续处理...
}

五、超时关单机制

5.1 为什么需要超时关单

  • 库存释放:未支付订单占用的库存需要及时释放
  • 资源清理:优惠券、积分等营销资源需要回退
  • 用户体验:避免用户看到大量"僵尸订单"
  • 财务合规:部分场景需要订单与支付时间绑定

5.2 超时关单方案对比

方案 原理 精度 资源消耗 适用量级
定时扫描 SELECT ... WHERE status='待支付' AND created&amp;#95;at &amp;lt; ? 分钟级 随数据量增长 十万级
延迟队列 下单时投递延迟消息到 MQ 秒级 低 百万级
Redis 过期回调 SETEX order:timeout:{id} 1800 1 + keyspace 通知 秒级 中 百万级
时间轮 内存时间轮管理超时任务 毫秒级 低 千万级

5.3 延迟队列方案(推荐)

文章配图
文章配图

图4:超时关单四种方案对比

java
// 下单时投递延迟消息
public void createOrder(OrderDTO dto) {
    // 1. 创建订单
    Order order = orderRepository.save(dto.toOrder());
    
    // 2. 投递延迟消息(30分钟后关单)
    OrderTimeoutMessage message = new OrderTimeoutMessage(order.getId());
    rocketMQTemplate.syncSendDelay(
        "order-timeout-topic",
        message,
        RocketMQTimeoutMillis.MINUTES_30
    );
}

// 消费者处理超时关单
@RocketMQMessageListener(
    topic = "order-timeout-topic",
    consumerGroup = "order-timeout-consumer"
)
public class OrderTimeoutConsumer implements RocketMQListener<OrderTimeoutMessage> {
    @Override
    public void onMessage(OrderTimeoutMessage message) {
        Long orderId = message.getOrderId();
        Order order = orderRepository.findById(orderId);
        
        if (order == null) return;
        
        // 只有待支付状态才关单
        if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
            // CAS 更新状态
            int updated = orderRepository.updateStatus(
                orderId,
                OrderStatus.PENDING_PAYMENT,
                OrderStatus.CANCELLED
            );
            
            if (updated > 0) {
                // 回补库存
                inventoryService.restoreStock(order.getItems());
                // 回退优惠券
                couponService.restoreCoupon(order.getCouponId());
                // 发送关单通知
                notifyService.sendOrderClosedNotification(order);
            }
        }
    }
}

5.4 时间轮方案(高精度场景)

对于需要毫秒级精度的超时处理(如支付倒计时展示),可以使用 Netty 的 HashedWheelTimer:

java
@Component
public class OrderTimeoutManager {
    private final HashedWheelTimer timer = new HashedWheelTimer(
        new DefaultThreadFactory("order-timeout"),
        100, TimeUnit.MILLISECONDS,  // tick 精度
        512  // 槽位数
    );
    
    private final Map<Long, Timeout> timeoutMap = new ConcurrentHashMap<>();
    
    public void scheduleTimeout(Long orderId, long delayMs) {
        Timeout timeout = timer.newTimeout(t -> {
            timeoutMap.remove(orderId);
            handleOrderTimeout(orderId);
        }, delayMs, TimeUnit.MILLISECONDS);
        
        timeoutMap.put(orderId, timeout);
    }
    
    public void cancelTimeout(Long orderId) {
        Timeout timeout = timeoutMap.remove(orderId);
        if (timeout != null) {
            timeout.cancel();
        }
    }
}

注意:时间轮方案是纯内存的,重启丢失。生产环境需要配合 MQ 延迟队列做兜底。

5.5 关单的补偿机制

即使有延迟队列,也可能因为 MQ 故障、消费失败等原因导致关单未执行。需要定时扫描兜底:

java
@Scheduled(cron = "0 */5 * * * ?")  // 每5分钟
public void compensateTimeoutOrders() {
    LocalDateTime timeout = LocalDateTime.now().minusMinutes(30);
    List<Long> timeoutOrders = orderRepository.findTimeoutOrderIds(
        OrderStatus.PENDING_PAYMENT, timeout, 1000  // 批量1000
    );
    
    for (Long orderId : timeoutOrders) {
        // 投递到延迟队列重新处理(不直接关单,避免与支付回调并发)
        rocketMQTemplate.syncSendDelay(
            "order-timeout-topic",
            new OrderTimeoutMessage(orderId),
            0  // 立即投递,但消费者会做状态判断
        );
    }
}

六、幂等保证

6.1 幂等场景全景

订单系统中的幂等问题无处不在:

场景 幂等键 实现方式
重复提交订单 客户端生成的 order_token DB 唯一索引
重复支付回调 支付渠道的 trade_no DB 唯一索引 + 状态判断
重复扣减库存 order_id + sku_id DB 唯一索引
重复发送消息 业务唯一键 消费端去重
重复关单 order_id CAS 状态更新

6.2 下单幂等:Token 机制

文章配图
文章配图

图6:幂等保证多层防护示意图

java
// 1. 进入下单页时,前端请求 token
@GetMapping("/order/token")
public Result<String> getOrderToken() {
    String token = UUID.randomUUID().toString().replace("-", "");
    redisTemplate.opsForValue().set(
        "order:token:" + token, "1", 5, TimeUnit.MINUTES);
    return Result.success(token);
}

// 2. 提交订单时,携带 token
@PostMapping("/order/submit")
public Result<OrderVO> submitOrder(@RequestBody SubmitOrderRequest request) {
    String token = request.getOrderToken();
    
    // 原子删除 token,防止重复提交
    Boolean deleted = redisTemplate.delete("order:token:" + token);
    if (!deleted) {
        throw new BizException("请勿重复提交订单");
    }
    
    // 后续下单逻辑...
}

为什么用 Redis 删除而不是 DB 唯一索引?

Redis 的 DELETE 是原子操作,天然支持分布式场景。如果使用 DB 唯一索引,INSERT 失败时事务已经部分执行,回滚复杂。

6.3 支付回调幂等

支付回调幂等需要多层防护:

java
@Transactional
public void handlePaymentNotify(PaymentNotify notify) {
    String tradeNo = notify.getTradeNo();  // 支付宝/微信交易号
    
    // 第一层:幂等表唯一索引
    try {
        paymentNotifyLogRepository.insert(notify);
    } catch (DuplicateKeyException e) {
        log.info("重复通知,已忽略: {}", tradeNo);
        return;  // 重复通知,直接返回成功(支付渠道要求返回 success)
    }
    
    // 第二层:订单状态判断
    Order order = orderRepository.findByPayNo(notify.getOutTradeNo());
    if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
        log.info("订单状态已变更: orderId={}, status={}", 
            order.getId(), order.getStatus());
        return;
    }
    
    // 第三层:金额校验
    if (!notify.getTotalAmount().equals(order.getTotalAmount())) {
        throw new BizException("支付金额与订单金额不一致");
    }
    
    // 第四层:CAS 更新订单状态
    int updated = orderRepository.updateStatusCAS(
        order.getId(),
        OrderStatus.PENDING_PAYMENT,
        OrderStatus.PENDING_SHIPPING,
        order.getVersion()
    );
    
    if (updated == 0) {
        throw new BizException("订单状态更新失败,可能并发冲突");
    }
}

6.4 数据库唯一索引设计

幂等键的数据库表设计:

sql
CREATE TABLE order_idempotent (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    idempotent_key VARCHAR(128) NOT NULL,
    biz_type VARCHAR(32) NOT NULL,       -- 业务类型:ORDER_CREATE, PAY_NOTIFY, etc.
    order_id BIGINT,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_idempotent (idempotent_key, biz_type),
    INDEX idx_created_at (created_at)
);

-- 定期清理7天前的幂等记录
DELETE FROM order_idempotent WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 DAY) LIMIT 10000;

七、对账体系

7.1 对账的三个层次

层次 对账内容 周期 目标
实时对账 订单状态 vs 支付状态 实时 及时发现异常订单
日终对账 内部订单 vs 支付渠道账单 T+1 发现资金差异
全量对账 订单、库存、支付、财务 周/月 全面数据一致性

7.2 实时对账设计

文章配图
文章配图

图5:对账体系架构图

java
@Component
public class RealtimeReconciliation {
    
    // 监听支付成功事件
    @EventListener
    public void onPaymentSuccess(PaymentSuccessEvent event) {
        Order order = orderRepository.findById(event.getOrderId());
        
        // 订单状态应该是"已支付"
        if (order.getStatus() != OrderStatus.PENDING_SHIPPING) {
            reconciliationAlertService.sendAlert(
                ReconciliationAlert.builder()
                    .type("ORDER_STATUS_MISMATCH")
                    .orderId(order.getId())
                    .expectedStatus(OrderStatus.PENDING_SHIPPING)
                    .actualStatus(order.getStatus())
                    .paymentTradeNo(event.getTradeNo())
                    .build()
            );
        }
    }
}

7.3 日终对账

以支付宝为例,每日下载对账单,与内部订单逐笔核对:

java
public void dailyReconciliation() {
    // 1. 下载支付渠道对账单
    List<PaymentBillRecord> channelRecords = alipayService.downloadBill(
        LocalDate.now().minusDays(1));
    
    // 2. 查询内部支付记录
    LocalDate billDate = LocalDate.now().minusDays(1);
    List<PaymentRecord> internalRecords = paymentRepository
        .findByPayDate(billDate);
    
    // 3. 构建 Map 便于比对
    Map<String, PaymentBillRecord> channelMap = channelRecords.stream()
        .collect(Collectors.toMap(
            PaymentBillRecord::getTradeNo,
            Function.identity()
        ));
    
    // 4. 逐笔核对
    List<ReconciliationDiff> diffs = new ArrayList<>();
    
    // 4a. 内部有,渠道无 → 长款(钱到账了但订单没记录)
    for (PaymentRecord internal : internalRecords) {
        if (!channelMap.containsKey(internal.getTradeNo())) {
            diffs.add(ReconciliationDiff.builder()
                .type(DiffType.INTERNAL_ONLY)
                .tradeNo(internal.getTradeNo())
                .amount(internal.getAmount())
                .build());
        }
    }
    
    // 4b. 渠道有,内部无 → 短款(订单有记录但钱没到账)
    Set<String> internalTradeNos = internalRecords.stream()
        .map(PaymentRecord::getTradeNo)
        .collect(Collectors.toSet());
    
    for (PaymentBillRecord channel : channelRecords) {
        if (!internalTradeNos.contains(channel.getTradeNo())) {
            diffs.add(ReconciliationDiff.builder()
                .type(DiffType.CHANNEL_ONLY)
                .tradeNo(channel.getTradeNo())
                .amount(channel.getAmount())
                .build());
        }
    }
    
    // 4c. 金额不一致
    for (PaymentRecord internal : internalRecords) {
        PaymentBillRecord channel = channelMap.get(internal.getTradeNo());
        if (channel != null && 
            !internal.getAmount().equals(channel.getAmount())) {
            diffs.add(ReconciliationDiff.builder()
                .type(DiffType.AMOUNT_MISMATCH)
                .tradeNo(internal.getTradeNo())
                .internalAmount(internal.getAmount())
                .channelAmount(channel.getAmount())
                .build());
        }
    }
    
    // 5. 生成对账报告
    if (!diffs.isEmpty()) {
        reconciliationReportService.generateAndSend(diffs);
        // 自动处理明确差异,人工处理存疑差异
        autoProcessDiffs(diffs);
    }
}

7.4 库存对账

java
@Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨3点
public void inventoryReconciliation() {
    // 1. 计算应有库存:DB 初始库存 - 已支付未退款订单的库存
    // 2. 对比实际库存
    // 3. 差异告警 + 自动补偿
    
    List<InventoryReconciliationResult> results = 
        inventoryReconciliationService.reconcile();
    
    for (InventoryReconciliationResult result : results) {
        if (result.getDiff() != 0) {
            log.warn("库存差异: skuId={}, expected={}, actual={}, diff={}",
                result.getSkuId(), result.getExpected(),
                result.getActual(), result.getDiff());
            
            // 差异超过阈值,告警
            if (Math.abs(result.getDiff()) > result.getThreshold()) {
                alertService.sendInventoryDiffAlert(result);
            }
            
            // 自动补偿
            inventoryReconciliationService.compensate(result);
        }
    }
}

八、面试提问

以下问题从面试官视角出发,考察候选人对订单系统设计的深度理解。阅读时间不计入正文。

Q1:如果下单时先创建订单再扣库存,会出现什么问题?反过来呢?

考察点:分布式系统下的操作顺序与异常处理。

参考思路:

  • 先创建订单再扣库存:扣库存失败时,订单已是"待支付"状态,需要回滚(删除订单或标记取消),增加复杂度
  • 先扣库存再创建订单:创建订单失败时,需要回补库存,如果回补也失败会导致库存丢失
  • 推荐:先扣库存,创建订单失败后用 Redis 事务保证回补,配合定时任务兜底

Q2:支付回调丢失了怎么办?如何保证最终一致性?

考察点:分布式系统的可靠性保障。

参考思路:三层保障机制

  1. 支付渠道的异步通知(最多8次重试)
  2. 定时主动查询(每5分钟查询未支付订单的支付状态)
  3. 对账兜底(T+1 日终对账发现差异并修复)

Q3:订单状态机的并发更新如何保证正确性?

考察点:并发控制与乐观锁。

参考思路:

  • 使用乐观锁(version 字段)+ CAS 更新
  • 数据库行锁(SELECT ... FOR UPDATE)
  • 分布式锁(Redis/ZK)作为最后手段
  • 状态变更日志的幂等键防止重复处理

Q4:在双十一场景下,订单系统的库存扣减如何设计才能扛住峰值流量?

考察点:高并发系统设计。

参考思路:

  • 库存预热到 Redis,纯内存操作
  • 库存分片,分散热点 Key
  • 使用 Lua 脚本保证原子性
  • 下单异步化,先返回"排队中",后台异步创建订单
  • 限流降级,超过系统容量时排队或拒绝

Q5:如何设计一个支持"部分退款"和"多次退款"的订单系统?

考察点:状态机扩展性设计。

参考思路:

  • 订单行级别(Order Item)管理退款,而非订单级别
  • 每行记录可退数量和已退数量
  • 退款单独立管理,关联订单行
  • 状态机从"订单级"扩展到"订单行级"
  • 退款需要关联原支付流水,支持原路退回

Q6:如何设计一个跨多个微服务的订单创建流程,保证数据一致性?

考察点:分布式事务与 Saga 模式。

参考思路:

  • 使用 Saga 编排模式:订单服务作为协调者,依次调用库存、优惠券、积分等服务
  • 每个服务提供补偿接口(如库存扣减 → 库存回补)
  • 下单失败时,按逆序调用补偿接口
  • 补偿失败时,记录到"待人工处理"表,告警通知
  • 也可以使用 Seata AT 模式,但需注意性能开销

延伸阅读:分布式事务(161-175)、秒杀系统设计(206)、数据库锁机制(122)、CQRS(200)


本文为"Java 技术专家与架构师面试:250 个高频考点"系列第 208 篇