阅读时间:约 45 分钟 | 难度:专家级 | 分类:13 架构设计与系统设计
一、问题定义与核心挑战
订单系统是电商、金融、O2O 等业务中最核心的领域之一。它不只是一个 CRUD 模块,而是一个需要同时处理高并发、强一致性、复杂状态流转、多系统协同的分布式系统。
1.1 订单系统的核心职责
- 生命周期管理:从创建到完成的完整状态流转
- 库存协调:下单时扣库存,取消时回补,保证不超卖
- 支付对接:集成第三方支付,处理同步/异步通知
- 超时管控:未支付订单自动关闭,释放资源
- 幂等保障:防止重复下单、重复支付、重复回调
- 对账清算:与支付渠道、库存、财务多方对账
1.2 核心挑战
| 挑战 | 描述 | 影响 |
|---|---|---|
| 高并发扣减 | 秒杀/大促瞬时流量冲击 | 超卖、库存不一致 |
| 状态一致性 | 订单状态在多个系统间同步 | 状态错乱、用户困惑 |
| 分布式事务 | 下单涉及订单、库存、优惠券、积分等多个服务 | 数据不一致 |
| 支付回调不可靠 | 第三方支付回调可能延迟、重复、丢失 | 订单状态卡死 |
| 超时处理精准性 | 海量订单需要精确的超时关单 | 资源浪费、库存错乱 |
二、订单状态机设计

图7:订单系统整体架构图
2.1 状态机建模原则
订单状态机是整个系统的骨架。一个设计良好的状态机应该满足:
- 确定性:给定当前状态和事件,下一状态唯一确定
- 完备性:覆盖所有合法路径和异常路径
- 可追溯性:每个状态变更都有日志记录
- 幂等性:同一事件重复到达不会导致状态跳变
2.2 典型订单状态流转
┌──────────┐
│ 待支付 │ ← 下单成功
└────┬─────┘
│
┌──────────┼──────────┐
│ 支付成功 │ │ 超时/取消
▼ │ ▼
┌──────────┐ │ ┌──────────┐
│ 待发货 │ │ │ 已取消 │
└────┬─────┘ │ └──────────┘
│ │
▼ │
┌──────────┐ │
│ 待收货 │ │
└────┬─────┘ │
│ │
┌────┴────┐ │
│ 确认收货 │ │
▼ ▼ │
┌────────┐ ┌────────┐ │
│ 已完成 │ │ 待评价 │ │
└────────┘ └───┬────┘ │
│ │
▼ │
┌────────┐ │
│ 已完成 │ │
└────────┘ │
│
┌──────────┘
│ 退款/退货
▼
┌──────────┐
│ 退款中 │
└────┬─────┘
│
▼
┌──────────┐
│ 已退款 │
└──────────┘
2.3 状态机实现方案

图1:订单状态机流转图
方案一:状态枚举 + if-else(不推荐)
// 反模式:状态流转逻辑散落各处
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)
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 或自研轻量状态机:
// 定义状态转移
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 状态机持久化
状态变更必须持久化记录,这是对账和问题排查的基础:
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)
);
关键设计点:
- 状态变更使用 CAS(Compare-And-Swap) 更新,而非直接 SET:
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 &gt;= ? |
强 | 低 | 低并发 |
| Redis 扣减 | DECR 原子操作 |
强(单机) | 高 | 中等并发 |
| Redis + Lua | Lua 脚本原子校验+扣减 | 强(单机) | 高 | 高并发 |
| 库存预热 | 提前将库存加载到 Redis | 最终 | 极高 | 秒杀 |
| 异步扣减 | 下单后异步扣减,下单时只校验 | 最终 | 极高 | 超高并发 |
3.3 库存预热方案(秒杀场景)
┌─────────────────────────────────────────────────────┐
│ 库存预热流程 │
├─────────────────────────────────────────────────────┤
│ DB: stock = 1000 │
│ │ 预热 │
│ ▼ │
│ Redis: stock = 1000 │
│ │ 下单扣减 │
│ ▼ │
│ Redis: DECR stock → 库存扣减完成 │
│ │ 异步同步 │
│ ▼ │
│ DB: UPDATE stock = stock - delta │
│ │ 定时对账 │
│ ▼ │
│ DB vs Redis 差异补偿 │
└─────────────────────────────────────────────────────┘
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:库存预热与扣减流程
关键问题:先扣库存还是先生成订单?
推荐方案:先扣库存,成功后再生成订单。
1. 扣减库存(Redis DECR)
├─ 成功 → 2
└─ 失败 → 返回"库存不足"
2. 生成订单(DB INSERT)
├─ 成功 → 3
└─ 失败 → 回补库存(Redis INCR)
3. 发送订单创建消息(MQ)
└─ 异步处理后续流程
回补库存的风险:如果 DB INSERT 失败但 Redis INCR 也失败,库存会丢失。解决方案:
- 使用 Redis 事务或 Lua 脚本保证扣减+回补的原子性
- 定时任务扫描"已扣库存但无订单"的记录,自动回补
3.5 库存扣减中的并发问题
热点库存问题
当大量请求同时扣减同一个 SKU 的库存时,单 Key 的 Redis 操作会成为瓶颈。
解决方案:库存分片
将 stock = 1000 拆分为 10 个分片:
stock:0 = 100
stock:1 = 100
...
stock:9 = 100
扣减时随机选一片,扣减失败再选下一片。
四、支付集成

图8:库存扣减并发场景示意图
4.1 支付流程全景
用户 订单系统 支付渠道(支付宝/微信)
│ │ │
│ 1.提交订单 │ │
├───────────────────►│ │
│ │ 2.生成支付单 │
│ │ 3.调用支付接口 │
│ ├────────────────────────►│
│ 4.返回支付链接 │ │
│◄───────────────────┤ │
│ │ │
│ 5.用户完成支付 ──────────────────────────────►│
│ │ │
│ │ 6.同步回调(支付成功) │
│ │◄────────────────────────┤
│ │ │
│ │ 7.异步通知(支付成功) │
│ │◄────────────────────────┤
│ │ │
│ 8.支付成功页面 │ │
│◄───────────────────┤ │
4.2 支付回调的可靠性保障

图3:支付集成时序图
核心问题:支付渠道的回调可能延迟、丢失、重复。
同步回调处理
支付渠道在用户支付成功后立即回调(通常是跳转回商户页面),不可靠(用户可能关闭页面)。
异步通知处理
支付渠道后台异步通知商户服务器,相对可靠但仍有丢失可能。
主动查询兜底
// 定时任务:每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 支付回调的幂等处理
支付回调可能重复到达,需要严格幂等(详见第六节)。
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 支付金额校验
必须在回调处理中校验支付金额,防止金额篡改:
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&#95;at &lt; ? |
分钟级 | 随数据量增长 | 十万级 |
| 延迟队列 | 下单时投递延迟消息到 MQ | 秒级 | 低 | 百万级 |
| Redis 过期回调 | SETEX order:timeout:{id} 1800 1 + keyspace 通知 |
秒级 | 中 | 百万级 |
| 时间轮 | 内存时间轮管理超时任务 | 毫秒级 | 低 | 千万级 |
5.3 延迟队列方案(推荐)

图4:超时关单四种方案对比
// 下单时投递延迟消息
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:
@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 故障、消费失败等原因导致关单未执行。需要定时扫描兜底:
@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:幂等保证多层防护示意图
// 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 支付回调幂等
支付回调幂等需要多层防护:
@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 数据库唯一索引设计
幂等键的数据库表设计:
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:对账体系架构图
@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 日终对账
以支付宝为例,每日下载对账单,与内部订单逐笔核对:
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 库存对账
@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:支付回调丢失了怎么办?如何保证最终一致性?
考察点:分布式系统的可靠性保障。
参考思路:三层保障机制
- 支付渠道的异步通知(最多8次重试)
- 定时主动查询(每5分钟查询未支付订单的支付状态)
- 对账兜底(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 篇