订单系统设计
2026/1/15大约 3 分钟
订单系统设计
场景一:订单超时取消
需求:订单创建后30分钟未支付自动取消。
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 定时任务扫描 | 简单 | 时效性差、数据库压力 |
| 延迟队列 | 时效性好 | 依赖 MQ |
| Redis 过期 | 简单 | 可能丢失 |
| 时间轮 | 高效 | 单机 |
推荐:RocketMQ 延迟消息
// 创建订单时发送延迟消息
public Order createOrder(OrderDTO dto) {
Order order = orderService.save(dto);
// 发送30分钟延迟消息
Message msg = new Message("order-timeout", order.getId().toString().getBytes());
msg.setDelayTimeLevel(16); // 30分钟
producer.send(msg);
return order;
}
// 消费延迟消息
@RocketMQMessageListener(topic = "order-timeout")
public class OrderTimeoutConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String orderId) {
Order order = orderService.getById(orderId);
if (order != null && order.getStatus() == OrderStatus.UNPAID) {
orderService.cancel(orderId, "超时未支付");
// 释放库存
stockService.release(order.getProductId(), order.getQuantity());
}
}
}场景二:接口幂等性
需求:防止重复提交导致重复下单。
方案
// 1. Token 机制
@GetMapping("/order/token")
public String getToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("order:token:" + token, "1", 10, TimeUnit.MINUTES);
return token;
}
@PostMapping("/order/create")
public Result createOrder(@RequestHeader("X-Token") String token, @RequestBody OrderDTO dto) {
// 验证并删除 token(原子操作)
String script = "if redis.call('get', KEYS[1]) then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("order:token:" + token));
if (result != 1) {
return Result.fail("请勿重复提交");
}
return orderService.create(dto);
}
// 2. 唯一索引
// 数据库订单表添加唯一索引:user_id + product_id + create_time
// 3. 状态机
// 订单状态只能单向流转,重复请求会因状态不对而失败场景三:分布式事务
需求:下单扣库存,保证数据一致性。
方案:本地消息表
@Transactional
public Order createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = new Order(dto);
orderMapper.insert(order);
// 2. 写入本地消息表
LocalMessage msg = new LocalMessage();
msg.setMessageId(UUID.randomUUID().toString());
msg.setContent(JSON.toJSONString(new StockMessage(dto.getProductId(), dto.getQuantity())));
msg.setStatus(0); // 待发送
localMessageMapper.insert(msg);
return order;
}
// 定时任务发送消息
@Scheduled(fixedRate = 1000)
public void sendMessage() {
List<LocalMessage> messages = localMessageMapper.selectPending();
for (LocalMessage msg : messages) {
try {
mqTemplate.send("stock-deduct", msg.getContent());
localMessageMapper.updateStatus(msg.getId(), 1); // 已发送
} catch (Exception e) {
// 重试
}
}
}方案:Seata AT 模式
@GlobalTransactional
public Order createOrder(OrderDTO dto) {
// 创建订单
Order order = orderService.create(dto);
// 扣减库存(远程调用)
stockService.deduct(dto.getProductId(), dto.getQuantity());
// 扣减余额(远程调用)
accountService.deduct(dto.getUserId(), dto.getAmount());
return order;
}场景四:订单号生成
需求:生成全局唯一、有序、高性能的订单号。
方案:雪花算法
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095;
if (sequence == 0) {
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - 1609459200000L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
}方案:业务前缀 + 时间 + 序列
// 格式:OD + 年月日时分秒 + 机器号 + 序列号
// 例如:OD20240115143052001000001
public String generateOrderNo() {
String prefix = "OD";
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String machineId = String.format("%03d", getMachineId());
String sequence = String.format("%06d", redisTemplate.opsForValue().increment("order:seq:" + time));
return prefix + time + machineId + sequence;
}场景五:库存扣减
需求:高并发下准确扣减库存。
// 1. 数据库乐观锁
UPDATE product SET stock = stock - #{quantity}
WHERE id = #{id} AND stock >= #{quantity}
// 2. Redis + 数据库
public boolean deductStock(Long productId, int quantity) {
// Redis 预扣减
String key = "stock:" + productId;
Long stock = redisTemplate.opsForValue().decrement(key, quantity);
if (stock < 0) {
// 回滚 Redis
redisTemplate.opsForValue().increment(key, quantity);
return false;
}
// 异步扣减数据库
mqTemplate.send("stock-deduct", new StockMessage(productId, quantity));
return true;
}