🛒 设计一个电商系统的订单服务:双11的战场!

64 阅读14分钟

📖 开场:买买买的背后

想象你在双11抢购 🛍️:

10:00:00 秒杀开始:

你:点击"立即购买" 🖱️
    ↓
订单服务:创建订单 📝
    ↓
支付服务:扣款 💰
    ↓
库存服务:扣库存 📦
    ↓
物流服务:发货 🚚
    ↓
你:收到商品 ✅

看似简单,实则复杂:
- 100万人同时下单
- 库存不能超卖
- 支付失败要回滚
- 订单状态要正确
- 数据要一致
- 性能要高

这就是订单服务:电商系统的核心!


🤔 订单服务的核心挑战

挑战1:高并发 🔥

双11秒杀:
10:00:00  开始
    ↓
100万用户同时点击
    ↓
QPS:100万
    ↓
订单服务能承受吗?💀

解决方案:
- 限流
- 削峰填谷(消息队列)
- 分库分表
- 缓存

挑战2:分布式事务 🔄

订单创建流程:
1. 创建订单
2. 扣减库存
3. 扣减余额
4. 发送短信

问题:
- 库存扣减成功
- 余额扣减失败 💀
- 订单已创建,但没支付
- 数据不一致 ❌

解决方案:
- TCC
- Saga
- 本地消息表

挑战3:订单状态管理 🎯

订单状态:
待支付 → 已支付 → 待发货 → 已发货 → 已签收 → 已完成

问题:
- 状态流转复杂
- 不能随意跳转
- 需要状态机

例子:
已发货 → 待支付 ❌(不合法)
待支付 → 已支付 ✅(合法)

挑战4:超时关单 ⏰

问题:
用户下单后不支付
    ↓
订单占用库存
    ↓
其他用户无法购买 💀

解决方案:
30分钟未支付 → 自动关闭订单
    ↓
释放库存 ✅

挑战5:幂等性 🔐

问题:
用户点击"支付"
    ↓
网络抖动,重试
    ↓
支付请求发送2次
    ↓
扣款2次 💀

解决方案:
- 订单号唯一
- 幂等性校验
- 防止重复支付 ✅

🎯 核心设计

设计1:订单状态机 🎯

状态定义

public enum OrderStatus {
    WAIT_PAY(1, "待支付"),
    PAID(2, "已支付"),
    WAIT_SEND(3, "待发货"),
    SENT(4, "已发货"),
    RECEIVED(5, "已签收"),
    COMPLETED(6, "已完成"),
    CANCELLED(7, "已取消"),
    REFUNDING(8, "退款中"),
    REFUNDED(9, "已退款");
    
    private int code;
    private String desc;
    
    // ...
}

状态流转规则

状态机:

     待支付
       ↓
    (支付)
       ↓
     已支付
       ↓
   (商家发货)
       ↓
     已发货
       ↓
   (用户签收)
       ↓
     已签收
       ↓
  (自动确认)
       ↓
     已完成

任何状态都可以 → 已取消(用户取消或超时)
已支付后可以 → 退款中 → 已退款

状态机实现

@Component
public class OrderStateMachine {
    
    // ⭐ 状态流转规则
    private static final Map<OrderStatus, Set<OrderStatus>> STATE_MACHINE = new HashMap<>();
    
    static {
        // 待支付 → 已支付、已取消
        STATE_MACHINE.put(OrderStatus.WAIT_PAY, 
            EnumSet.of(OrderStatus.PAID, OrderStatus.CANCELLED));
        
        // 已支付 → 待发货、已取消、退款中
        STATE_MACHINE.put(OrderStatus.PAID, 
            EnumSet.of(OrderStatus.WAIT_SEND, OrderStatus.CANCELLED, OrderStatus.REFUNDING));
        
        // 待发货 → 已发货、已取消、退款中
        STATE_MACHINE.put(OrderStatus.WAIT_SEND, 
            EnumSet.of(OrderStatus.SENT, OrderStatus.CANCELLED, OrderStatus.REFUNDING));
        
        // 已发货 → 已签收、退款中
        STATE_MACHINE.put(OrderStatus.SENT, 
            EnumSet.of(OrderStatus.RECEIVED, OrderStatus.REFUNDING));
        
        // 已签收 → 已完成、退款中
        STATE_MACHINE.put(OrderStatus.RECEIVED, 
            EnumSet.of(OrderStatus.COMPLETED, OrderStatus.REFUNDING));
        
        // 退款中 → 已退款
        STATE_MACHINE.put(OrderStatus.REFUNDING, 
            EnumSet.of(OrderStatus.REFUNDED));
    }
    
    /**
     * ⭐ 校验状态流转是否合法
     */
    public boolean canTransition(OrderStatus from, OrderStatus to) {
        Set<OrderStatus> allowedStates = STATE_MACHINE.get(from);
        return allowedStates != null && allowedStates.contains(to);
    }
    
    /**
     * ⭐ 状态流转
     */
    public void transition(Order order, OrderStatus toStatus, String reason) {
        OrderStatus fromStatus = order.getStatus();
        
        // 校验状态流转是否合法
        if (!canTransition(fromStatus, toStatus)) {
            throw new IllegalStateException(
                String.format("订单状态不能从 %s 流转到 %s", fromStatus.getDesc(), toStatus.getDesc())
            );
        }
        
        // 更新订单状态
        order.setStatus(toStatus);
        
        // 记录状态变更日志
        OrderStatusLog log = new OrderStatusLog();
        log.setOrderId(order.getId());
        log.setFromStatus(fromStatus);
        log.setToStatus(toStatus);
        log.setReason(reason);
        log.setCreateTime(new Date());
        orderStatusLogMapper.insert(log);
        
        // 更新订单
        orderMapper.updateById(order);
    }
}

使用示例

@Service
public class OrderService {
    
    @Autowired
    private OrderStateMachine stateMachine;
    
    /**
     * ⭐ 支付订单
     */
    public void pay(Long orderId, String paymentId) {
        // 查询订单
        Order order = orderMapper.selectById(orderId);
        
        if (order == null) {
            throw new OrderNotFoundException("订单不存在");
        }
        
        // ⭐ 状态流转:待支付 → 已支付
        stateMachine.transition(order, OrderStatus.PAID, "用户支付成功,支付单号:" + paymentId);
    }
    
    /**
     * ⭐ 发货
     */
    public void send(Long orderId, String expressNo) {
        Order order = orderMapper.selectById(orderId);
        
        // ⭐ 状态流转:待发货 → 已发货
        stateMachine.transition(order, OrderStatus.SENT, "商家发货,快递单号:" + expressNo);
    }
}

设计2:分库分表 📊

为什么要分库分表?

订单表数据量:
- 用户:1亿
- 每人每年:10个订单
- 每年:10亿订单
- 10年:100亿订单 💀

单表问题:
- 查询慢(全表扫描)
- 写入慢(索引维护)
- 表锁竞争

解决方案:
分库分表 ✅

分库分表策略

水平分表:
- 按用户ID分表
- 1024个表

例子:
用户ID = 123456789
    ↓
hash = 123456789 % 1024 = 789
    ↓
存储到:order_789 表

优点:
- 数据均匀分布 ✅
- 支持千亿级数据 ✅

ShardingSphere实现

引入依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.1.0</version>
</dependency>

配置

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1  # 2个数据源
      
      # 数据源1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_0
        username: root
        password: root
      
      # 数据源2
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_1
        username: root
        password: root
    
    rules:
      sharding:
        tables:
          # ⭐ 订单表分片规则
          t_order:
            actual-data-nodes: ds$->{0..1}.t_order_$->{0..1023}  # 2个库,每个库1024张表
            
            # 分库策略(按用户ID)
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-inline
            
            # 分表策略(按用户ID)
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-inline
        
        # 分片算法
        sharding-algorithms:
          # 分库算法
          db-inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{user_id % 2}  # 2个库
          
          # 分表算法
          table-inline:
            type: INLINE
            props:
              algorithm-expression: t_order_$->{user_id % 1024}  # 1024张表
    
    props:
      sql-show: true  # 打印SQL

使用

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * ⭐ 创建订单(自动路由到正确的库表)
     */
    public Order createOrder(Long userId, List<OrderItem> items) {
        Order order = new Order();
        order.setUserId(userId);  // ⭐ 根据userId自动路由
        order.setStatus(OrderStatus.WAIT_PAY);
        order.setCreateTime(new Date());
        
        orderMapper.insert(order);
        
        return order;
    }
    
    /**
     * ⭐ 查询用户订单(自动路由)
     */
    public List<Order> getUserOrders(Long userId) {
        // ⭐ 根据userId自动路由到正确的库表
        return orderMapper.selectByUserId(userId);
    }
}

分布式ID生成

问题:
分库分表后,订单ID如何生成?
    ↓
不能用自增ID(会重复)💀

解决方案:
雪花算法(Snowflake)⭐

雪花算法

订单ID(64位):
┌─────────┬───────┬───────┬────────────┐
│时间戳   │机器ID │序列号 │
│41位     │10位   │12位   │
└─────────┴───────┴───────┴────────────┘

时间戳:41位,可用69年
机器ID:10位,支持1024台机器
序列号:12位,每毫秒可生成4096个ID

优点:
- 全局唯一 ✅
- 趋势递增(有利于数据库索引)✅
- 高性能(本地生成)✅

代码实现

@Component
public class SnowflakeIdGenerator {
    
    // ⭐ 起始时间戳(2020-01-01)
    private final long startTimestamp = 1577808000000L;
    
    // 机器ID(10位,0-1023)
    private final long workerId;
    
    // 序列号(12位,0-4095)
    private long sequence = 0L;
    
    // 上次生成ID的时间戳
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > 1023) {
            throw new IllegalArgumentException("workerId must be between 0 and 1023");
        }
        this.workerId = workerId;
    }
    
    /**
     * ⭐ 生成订单ID
     */
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,拒绝生成ID");
        }
        
        if (timestamp == lastTimestamp) {
            // 同一毫秒内,序列号+1
            sequence = (sequence + 1) & 4095;
            
            if (sequence == 0) {
                // 序列号用完,等待下一毫秒
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 新的一毫秒,序列号重置为0
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        // ⭐ 组装ID
        long id = ((timestamp - startTimestamp) << 22)  // 时间戳左移22位
                | (workerId << 12)                      // 机器ID左移12位
                | sequence;                             // 序列号
        
        return id;
    }
    
    /**
     * 等待下一毫秒
     */
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

设计3:超时关单 ⏰

方案1:定时任务(不推荐)❌

定时任务:
每分钟扫描一次订单表
    ↓
查询30分钟前的待支付订单
    ↓
关闭订单

缺点:
- 延迟高(最多1分钟)❌
- 数据库压力大(全表扫描)❌
- 不支持分库分表 ❌

方案2:延迟消息(推荐)⭐⭐⭐

创建订单时:
1. 创建订单(待支付)
2. 发送延迟消息(30分钟后)
    ↓
30分钟后:
消息队列 → 消费者
    ↓
消费者:检查订单状态
    ↓
如果还是待支付 → 关闭订单 ✅

优点:
- 精准(刚好30分钟)✅
- 高性能(无需扫描数据库)✅
- 支持分库分表 ✅

RocketMQ实现

@Service
public class OrderService {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * ⭐ 创建订单
     */
    public Order createOrder(Long userId, List<OrderItem> items) {
        // 1. 创建订单
        Order order = new Order();
        order.setId(idGenerator.nextId());
        order.setUserId(userId);
        order.setStatus(OrderStatus.WAIT_PAY);
        order.setCreateTime(new Date());
        orderMapper.insert(order);
        
        // ⭐ 2. 发送延迟消息(30分钟后关单)
        rocketMQTemplate.syncSend(
            "order-timeout-topic",
            MessageBuilder.withPayload(order.getId()).build(),
            3000,  // 超时时间
            18     // 延迟级别:18 = 30分钟
        );
        
        return order;
    }
}

/**
 * ⭐ 超时关单消费者
 */
@Component
@RocketMQMessageListener(
    topic = "order-timeout-topic",
    consumerGroup = "order-timeout-consumer"
)
public class OrderTimeoutConsumer implements RocketMQListener<Long> {
    
    @Autowired
    private OrderService orderService;
    
    @Override
    public void onMessage(Long orderId) {
        // 查询订单
        Order order = orderService.getById(orderId);
        
        if (order == null) {
            return;
        }
        
        // ⭐ 如果还是待支付,关闭订单
        if (order.getStatus() == OrderStatus.WAIT_PAY) {
            orderService.closeOrder(orderId, "超时未支付,自动关闭");
        }
    }
}

RocketMQ延迟级别

延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

级别18 = 30分钟

方案3:Redis有序集合(轻量级)⭐⭐

创建订单时:
1. 创建订单(待支付)
2. 添加到Redis ZSet
   score = 当前时间 + 30分钟

定时任务:
每10秒执行一次
    ↓
查询Redis ZSet(score < 当前时间)
    ↓
关闭这些订单 ✅

优点:
- 简单(无需消息队列)✅
- 高性能(Redis内存操作)✅

缺点:
- 可靠性稍低(Redis宕机会丢失)❌

代码实现

@Service
public class OrderService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String ORDER_TIMEOUT_KEY = "order:timeout";
    
    /**
     * ⭐ 创建订单
     */
    public Order createOrder(Long userId, List<OrderItem> items) {
        // 1. 创建订单
        Order order = new Order();
        order.setId(idGenerator.nextId());
        order.setUserId(userId);
        order.setStatus(OrderStatus.WAIT_PAY);
        order.setCreateTime(new Date());
        orderMapper.insert(order);
        
        // ⭐ 2. 添加到Redis ZSet(30分钟后)
        long expireTime = System.currentTimeMillis() + 30 * 60 * 1000;
        redisTemplate.opsForZSet().add(ORDER_TIMEOUT_KEY, 
                                       String.valueOf(order.getId()), 
                                       expireTime);
        
        return order;
    }
    
    /**
     * ⭐ 支付成功,从Redis中移除
     */
    public void pay(Long orderId) {
        // 更新订单状态
        // ...
        
        // 从Redis中移除
        redisTemplate.opsForZSet().remove(ORDER_TIMEOUT_KEY, String.valueOf(orderId));
    }
}

/**
 * ⭐ 超时关单定时任务
 */
@Component
public class OrderTimeoutJob {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private OrderService orderService;
    
    private static final String ORDER_TIMEOUT_KEY = "order:timeout";
    
    /**
     * 每10秒执行一次
     */
    @Scheduled(fixedRate = 10000)
    public void closeTimeoutOrders() {
        long now = System.currentTimeMillis();
        
        // ⭐ 查询超时的订单(score < now)
        Set<String> orderIds = redisTemplate.opsForZSet()
            .rangeByScore(ORDER_TIMEOUT_KEY, 0, now);
        
        if (orderIds == null || orderIds.isEmpty()) {
            return;
        }
        
        // 关闭订单
        for (String orderIdStr : orderIds) {
            Long orderId = Long.valueOf(orderIdStr);
            
            try {
                orderService.closeOrder(orderId, "超时未支付,自动关闭");
                
                // 从Redis中移除
                redisTemplate.opsForZSet().remove(ORDER_TIMEOUT_KEY, orderIdStr);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

设计4:幂等性保证 🔐

问题

用户点击"支付":
    ↓
网络抖动
    ↓
前端重试
    ↓
支付请求发送2次
    ↓
扣款2次 💀

解决方案:订单号 + 状态机

@Service
public class OrderService {
    
    @Autowired
    private OrderStateMachine stateMachine;
    
    /**
     * ⭐ 支付订单(幂等)
     */
    @Transactional
    public void pay(Long orderId, String paymentId) {
        // 查询订单
        Order order = orderMapper.selectById(orderId);
        
        if (order == null) {
            throw new OrderNotFoundException("订单不存在");
        }
        
        // ⭐ 幂等性校验:如果订单已支付,直接返回
        if (order.getStatus() != OrderStatus.WAIT_PAY) {
            log.info("订单已支付,幂等返回。orderId={}", orderId);
            return;  // 幂等返回 ✅
        }
        
        // 调用支付服务扣款
        paymentService.deduct(paymentId, order.getAmount());
        
        // 更新订单状态:待支付 → 已支付
        stateMachine.transition(order, OrderStatus.PAID, "用户支付成功");
    }
}

关键点

  • 订单状态校验
  • 数据库乐观锁(version字段)
  • 支付单号唯一性

设计5:分布式事务 🔄

场景

创建订单流程:
1. 创建订单
2. 扣减库存
3. 扣减余额

问题:
- 订单创建成功
- 库存扣减成功
- 余额扣减失败 💀
    ↓
数据不一致!

方案:本地消息表 ⭐⭐⭐

流程:
1. 开启事务
2. 创建订单
3. 写入本地消息表(扣减库存消息)
4. 提交事务
    ↓
定时任务:
扫描消息表
    ↓
发送MQ消息 → 库存服务
    ↓
库存服务:扣减库存,返回ACK
    ↓
删除消息表记录 ✅

优点:
- 最终一致性 ✅
- 高性能 ✅

代码实现

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    /**
     * ⭐ 创建订单(本地消息表)
     */
    @Transactional
    public Order createOrder(Long userId, List<OrderItem> items) {
        // 1. 创建订单
        Order order = new Order();
        order.setId(idGenerator.nextId());
        order.setUserId(userId);
        order.setStatus(OrderStatus.WAIT_PAY);
        orderMapper.insert(order);
        
        // ⭐ 2. 写入本地消息表(扣减库存消息)
        LocalMessage message = new LocalMessage();
        message.setId(UUID.randomUUID().toString());
        message.setTopic("stock-deduct-topic");
        message.setContent(JSON.toJSONString(items));
        message.setStatus(MessageStatus.PENDING);
        message.setCreateTime(new Date());
        localMessageMapper.insert(message);
        
        // 3. 提交事务(订单和消息原子性写入)
        
        return order;
    }
}

/**
 * ⭐ 消息发送定时任务
 */
@Component
public class LocalMessageSender {
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 每5秒执行一次
     */
    @Scheduled(fixedRate = 5000)
    public void sendMessages() {
        // 查询待发送的消息
        List<LocalMessage> messages = localMessageMapper.selectPending();
        
        for (LocalMessage message : messages) {
            try {
                // ⭐ 发送MQ消息
                rocketMQTemplate.syncSend(message.getTopic(), message.getContent());
                
                // 更新消息状态
                message.setStatus(MessageStatus.SUCCESS);
                localMessageMapper.updateById(message);
                
            } catch (Exception e) {
                // 发送失败,下次重试
                e.printStackTrace();
            }
        }
    }
}

📊 数据库设计

订单表

-- ⭐ 订单主表
CREATE TABLE t_order (
    id BIGINT PRIMARY KEY COMMENT '订单ID(雪花算法)',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    status TINYINT NOT NULL COMMENT '订单状态',
    total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
    pay_amount DECIMAL(10,2) COMMENT '实付金额',
    payment_id VARCHAR(100) COMMENT '支付单号',
    payment_time DATETIME COMMENT '支付时间',
    express_no VARCHAR(100) COMMENT '快递单号',
    express_time DATETIME COMMENT '发货时间',
    receive_time DATETIME COMMENT '签收时间',
    complete_time DATETIME COMMENT '完成时间',
    cancel_time DATETIME COMMENT '取消时间',
    cancel_reason VARCHAR(500) COMMENT '取消原因',
    version INT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
    create_time DATETIME NOT NULL COMMENT '创建时间',
    update_time DATETIME COMMENT '更新时间',
    
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_create_time (create_time)
) COMMENT '订单表';

-- ⭐ 订单明细表
CREATE TABLE t_order_item (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL COMMENT '订单ID',
    product_id BIGINT NOT NULL COMMENT '商品ID',
    product_name VARCHAR(200) NOT NULL COMMENT '商品名称',
    product_image VARCHAR(500) COMMENT '商品图片',
    quantity INT NOT NULL COMMENT '购买数量',
    price DECIMAL(10,2) NOT NULL COMMENT '商品单价',
    total_amount DECIMAL(10,2) NOT NULL COMMENT '小计金额',
    
    INDEX idx_order_id (order_id)
) COMMENT '订单明细表';

-- ⭐ 订单状态日志表
CREATE TABLE t_order_status_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL COMMENT '订单ID',
    from_status TINYINT COMMENT '原状态',
    to_status TINYINT NOT NULL COMMENT '新状态',
    reason VARCHAR(500) COMMENT '变更原因',
    create_time DATETIME NOT NULL COMMENT '创建时间',
    
    INDEX idx_order_id (order_id)
) COMMENT '订单状态日志表';

🎓 面试题速答

Q1: 订单状态如何管理?

A: 状态机

// 状态流转规则
private static final Map<OrderStatus, Set<OrderStatus>> STATE_MACHINE;

// 校验状态流转
public boolean canTransition(OrderStatus from, OrderStatus to) {
    Set<OrderStatus> allowedStates = STATE_MACHINE.get(from);
    return allowedStates != null && allowedStates.contains(to);
}

优点

  • 防止非法状态流转
  • 清晰的状态管理

Q2: 订单表如何分库分表?

A: 按用户ID分片

# 分片规则
database-strategy:
  sharding-column: user_id
  algorithm-expression: ds$->{user_id % 2}  # 2个库

table-strategy:
  sharding-column: user_id
  algorithm-expression: t_order_$->{user_id % 1024}  # 1024张表

订单ID生成:雪花算法(Snowflake)


Q3: 超时关单如何实现?

A: **延迟消息(RocketMQ)**⭐:

// 创建订单时,发送延迟消息
rocketMQTemplate.syncSend(
    "order-timeout-topic",
    orderId,
    3000,
    18  // 延迟级别:18 = 30分钟
);

// 30分钟后,消费者收到消息
@Override
public void onMessage(Long orderId) {
    if (order.getStatus() == OrderStatus.WAIT_PAY) {
        orderService.closeOrder(orderId);
    }
}

Q4: 如何保证幂等性?

A: 订单状态校验

public void pay(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    
    // ⭐ 幂等性校验
    if (order.getStatus() != OrderStatus.WAIT_PAY) {
        return;  // 已支付,直接返回 ✅
    }
    
    // 支付逻辑
    // ...
}

Q5: 分布式事务如何处理?

A: 本地消息表⭐:

@Transactional
public Order createOrder() {
    // 1. 创建订单
    orderMapper.insert(order);
    
    // 2. 写入本地消息表(扣减库存消息)
    localMessageMapper.insert(message);
    
    // 3. 提交事务(原子性)
}

// 定时任务扫描消息表,发送MQ消息
@Scheduled(fixedRate = 5000)
public void sendMessages() {
    List<LocalMessage> messages = localMessageMapper.selectPending();
    for (LocalMessage message : messages) {
        rocketMQTemplate.send(message);
    }
}

最终一致性


🎬 总结

       订单服务核心设计

┌────────────────────────────────────┐
│ 1. 订单状态机                      │
│    - 状态流转规则                  │
│    - 防止非法流转                  │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 2. 分库分表                        │
│    - 按用户ID分片                  │
│    - 雪花算法生成订单ID            │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 3. 超时关单                        │
│    - 延迟消息(RocketMQ)⭐        │
│    - Redis ZSet                    │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 4. 幂等性保证                      │
│    - 订单状态校验                  │
│    - 乐观锁                        │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 5. 分布式事务                      │
│    - 本地消息表                    │
│    - 最终一致性                    │
└────────────────────────────────────┘

🎉 恭喜你!

你已经完全掌握了电商订单服务的设计!🎊

核心要点

  1. 订单状态机:管理状态流转,防止非法操作
  2. 分库分表:按用户ID分片,支持海量数据
  3. 超时关单:延迟消息,精准关单
  4. 幂等性保证:状态校验,防止重复支付
  5. 分布式事务:本地消息表,最终一致性

下次面试,这样回答

"订单服务使用状态机管理订单状态。定义了待支付、已支付、待发货等状态,并通过Map定义状态流转规则。每次状态变更前校验是否允许流转,防止非法操作,如已发货不能流转回待支付。

订单表按用户ID进行分库分表。使用ShardingSphere配置分片规则,2个库每个库1024张表,根据用户ID取模路由到对应的库表。订单ID使用雪花算法生成,保证全局唯一且趋势递增。

超时关单使用RocketMQ延迟消息实现。创建订单时发送延迟消息,延迟级别设置为18(30分钟)。30分钟后消费者收到消息,检查订单状态,如果还是待支付则关闭订单并释放库存。这种方式精准且高性能,无需扫描数据库。

幂等性通过订单状态校验实现。支付接口先查询订单状态,如果不是待支付则直接返回,避免重复支付。配合数据库乐观锁(version字段)保证并发安全。

分布式事务使用本地消息表实现最终一致性。创建订单时在同一事务中写入订单记录和本地消息记录。定时任务扫描消息表,将消息发送到MQ,库存服务消费消息后扣减库存。这种方式性能高且可靠,适合高并发场景。"

面试官:👍 "很好!你对订单服务的设计理解很深刻!"


本文完 🎬

上一篇: 207-设计一个分布式配置中心.md
下一篇: 209-设计一个IM即时通讯系统.md

作者注:写完这篇,我都想去做电商架构师了!🛒
如果这篇文章对你有帮助,请给我一个Star⭐!