设计一个秒杀系统,30分钟没付款就自动关闭交易

4 阅读9分钟

引言:秒杀背后的技术挑战

大家好!今天我们来聊聊电商系统中最具挑战的场景之一——秒杀系统。想象一下,1万件商品在1秒内被抢购一空,同时还要处理30分钟未支付的自动关闭订单。这不仅是业务需求,更是对技术架构的极限考验。

一、需求拆解:不只是"秒杀"那么简单

核心需求

  1. 秒杀活动:高并发下的库存扣减

  2. 订单管理:创建、支付、关闭等状态流转

  3. 超时关闭:30分钟未支付自动取消订单

  4. 库存回滚:订单关闭后库存恢复

  5. 用户体验:流畅的下单、支付流程

技术挑战

  • 瞬间高并发:百万级QPS冲击

  • 数据一致性:库存不超卖

  • 系统可用性:99.99%的可靠性

  • 延迟敏感:毫秒级响应

二、整体架构设计

核心设计思路是:前端限流、异步削峰、服务解耦、数据分治

整个请求处理流程如下:

  1. 用户端(App/H5/小程序/PC)发起秒杀请求。

  2. 负载均衡层(Nginx集群)接收请求,进行初步的分发和负载均衡。

  3. 网关层进行统一管控:

    API网关:负责路由转发、用户鉴权、请求聚合。

    Sentinel:实现限流、熔断、降级,保护下游业务服务。

  4. 业务服务层是核心业务逻辑所在:

    秒杀服务:处理秒杀资格校验、库存扣减(通常与Redis配合)。

    订单服务:生成订单。

    支付服务:处理支付流程。

    各服务均为集群部署,保证高可用。

  5. 中间件层提供关键支撑:

    Redis集群:缓存热点数据(如商品库存)、存放秒杀令牌,支撑高并发读和原子扣减。

    RabbitMQ集群:将下单等耗时操作异步化,实现流量削峰,提升系统吞吐量。

    xxl-job:执行定时任务,如活动状态同步、对账等。

  6. 数据存储层

    MySQL分库分表:应对海量订单数据的存储和查询。

    TiDB(可选):作为分布式数据库的补充,解决分库分表带来的复杂性问题。

  7. 监控告警层(Prometheus + Grafana):对整个系统的性能指标、业务指标进行监控和可视化,便于及时发现问题。

三、核心模块设计

1. 秒杀服务设计

/**
* 秒杀核心服务
* 采用Redis + Lua脚本保证原子性
*/
@Component
@Slf4j
public class SeckillService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    /**
* 秒杀核心逻辑
*/
    public SeckillResult seckill(Long userId, Long seckillId) {
        // 1. 前置检查
        if (!checkUserLimit(userId, seckillId)) {
            return SeckillResult.error("已达购买限制");
        }
        // 2. Redis预减库存(Lua脚本保证原子性)
        Long stock = decrStockByLua(seckillId);
        if (stock < 0) {
            // 库存不足,异步恢复用户购买资格
            restoreUserLimit(userId, seckillId);
            return SeckillResult.error("库存不足");
        }
        // 3. 发送异步消息创建订单
        String orderNo = sendCreateOrderMessage(userId, seckillId);
        return SeckillResult.success(orderNo);
    }
    /**
* Lua脚本实现原子库存扣减
*/
    private Long decrStockByLua(Long seckillId) {
        String luaScript =
            "local stockKey = KEYS[1] " +
            "local stock = redis.call('get', stockKey) " +
            "if stock and tonumber(stock) > 0 then " +
            "    redis.call('decr', stockKey) " +
            "    return tonumber(stock) - 1 " +
            "else " +
            "    return -1 " +
            "end";
        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        return redisTemplate.execute(script, 
            Collections.singletonList("seckill:stock:" + seckillId));
    }
    /**
* 检查用户购买限制
*/
    private boolean checkUserLimit(Long userId, Long seckillId) {
        String key = String.format("seckill:limit:%s:%s", seckillId, userId);
        RAtomicLong counter = redissonClient.getAtomicLong(key);
        // 设置过期时间30分钟
        if (counter.isExists()) {
            counter.expire(30, TimeUnit.MINUTES);
        }
        return counter.incrementAndGet() <= 1; // 每个用户限购1件
    }
}

2. 订单状态机设计

/**
* 订单状态机
*/
@Component
public class OrderStateMachine {
    public enum OrderStatus {
        CREATED(0, "已创建"),
        PAID(1, "已支付"),
        CANCELED(2, "已取消"),
        CLOSED(3, "已关闭"),
        COMPLETED(4, "已完成");
        private final int code;
        private final String desc;
        OrderStatus(int code, String desc) {
            this.code = code;
            this.desc = desc;
        }
    }
    /**
* 状态转移规则
*/
    private static final Map<OrderStatus, Set<OrderStatus>> STATE_TRANSITIONS =
        new EnumMap<>(OrderStatus.class);
    static {
        STATE_TRANSITIONS.put(OrderStatus.CREATED, 
            EnumSet.of(OrderStatus.PAID, OrderStatus.CANCELED, OrderStatus.CLOSED));
        STATE_TRANSITIONS.put(OrderStatus.PAID, 
            EnumSet.of(OrderStatus.COMPLETED, OrderStatus.CANCELED));
        STATE_TRANSITIONS.put(OrderStatus.CANCELED, EnumSet.of());
        STATE_TRANSITIONS.put(OrderStatus.CLOSED, EnumSet.of());
        STATE_TRANSITIONS.put(OrderStatus.COMPLETED, EnumSet.of());
    }
    /**
* 校验状态转移是否合法
*/
    public boolean canTransition(OrderStatus from, OrderStatus to) {
        Set<OrderStatus> allowed = STATE_TRANSITIONS.get(from);
        return allowed != null && allowed.contains(to);
    }
    /**
* 执行状态转移
*/
    public Order executeTransition(Order order, OrderStatus newStatus, String remark) {
        if (!canTransition(order.getStatus(), newStatus)) {
            throw new IllegalStateException(
                String.format("状态转移非法: %s -> %s", order.getStatus(), newStatus));
        }
        order.setStatus(newStatus);
        order.setUpdateTime(new Date());
        // 记录状态变更日志
        OrderStatusLog log = new OrderStatusLog();
        log.setOrderId(order.getId());
        log.setFromStatus(order.getStatus());
        log.setToStatus(newStatus);
        log.setRemark(remark);
        log.setCreateTime(new Date());
        return order;
    }
}

3. 订单关闭的三种实现方案

方案一:定时任务扫描(简单但低效)

/**
* 定时任务扫描未支付订单
* 优点:实现简单
* 缺点:性能差,时间不精确
*/
@Component
@Slf4j
public class OrderCloseJob {
    @Autowired
    private OrderService orderService;
    // 每5分钟执行一次
    @Scheduled(fixedDelay = 5 * 60 * 1000)
    public void closeExpiredOrders() {
        log.info("开始扫描超时未支付订单...");
        // 查询30分钟前创建的未支付订单
        Date expireTime = DateUtils.addMinutes(new Date(), -30);
        List<Order> expiredOrders = orderService.findExpiredOrders(expireTime, 1000);
        for (Order order : expiredOrders) {
            try {
                orderService.closeOrder(order.getId(), "超时未支付");
                log.info("关闭超时订单: {}", order.getOrderNo());
            } catch (Exception e) {
                log.error("关闭订单失败: {}", order.getOrderNo(), e);
            }
        }
    }
}

方案二:延迟消息队列(推荐)

/**
* 基于RocketMQ延迟消息的实现
*/
@Component
@Slf4j
public class OrderCloseByDelayMessage {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    /**
* 创建订单时发送延迟消息
*/
    public void sendCloseOrderMessage(Order order) {
        try {
            // RocketMQ支持18个延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
            int delayLevel = 16; // 对应30分钟
            Message<String> message = MessageBuilder
                .withPayload(order.getId().toString())
                .setHeader(MessageConst.PROPERTY_KEYS, order.getOrderNo())
                .build();
            // 发送延迟消息
            rocketMQTemplate.syncSend("ORDER_CLOSE_TOPIC", message, 3000, delayLevel);
            log.info("发送订单关闭延迟消息: orderNo={}", order.getOrderNo());
        } catch (Exception e) {
            log.error("发送延迟消息失败", e);
            // 降级方案:记录到数据库,由定时任务补偿
            orderService.saveCloseTask(order.getId(), new Date());
        }
    }
    /**
* 消费延迟消息
*/
    @RocketMQMessageListener(
        topic = "ORDER_CLOSE_TOPIC",
        consumerGroup = "ORDER_CLOSE_CONSUMER_GROUP"
    )
    public class OrderCloseConsumer implements RocketMQListener<MessageExt> {
        @Override
        public void onMessage(MessageExt message) {
            String orderId = new String(message.getBody());
            log.info("收到订单关闭消息: orderId={}", orderId);
            try {
                orderService.closeOrder(Long.parseLong(orderId), "超时未支付");
            } catch (Exception e) {
                log.error("关闭订单失败,重试...", e);
                // 重试机制
                throw new RuntimeException(e);
            }
        }
    }
}

方案三:Redis过期键监听(高精度)

/**
* 基于Redis键过期事件的实现
*/
@Component
@Slf4j
public class OrderCloseByRedisExpire {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private OrderService orderService;
    /**
* 创建订单时设置Redis键,30分钟后过期
*/
    public void setOrderExpireKey(Order order) {
        String key = String.format("order:expire:%s", order.getOrderNo());
        String value = order.getId().toString();
        redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
        log.info("设置订单过期键: key={}, expire=30m", key);
    }
    /**
* 监听Redis过期事件
* 需要在Redis配置中开启:notify-keyspace-events Ex
*/
    @Component
    public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
        public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
            super(listenerContainer);
        }
        @Override
        public void onMessage(Message message, byte[] pattern) {
            String expiredKey = message.toString();
            if (expiredKey.startsWith("order:expire:")) {
                String orderNo = expiredKey.substring("order:expire:".length());
                log.info("检测到订单过期: orderNo={}", orderNo);
                // 异步处理,避免阻塞
                CompletableFuture.runAsync(() -> {
                    try {
                        orderService.closeOrderByNo(orderNo, "超时未支付");
                    } catch (Exception e) {
                        log.error("处理过期订单失败: orderNo={}", orderNo, e);
                    }
                });
            }
        }
    }
}

4. 库存管理策略

/**
* 库存管理服务
* 三级库存设计:Redis -> MySQL -> 预警
*/
@Service
@Slf4j
public class StockService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private ProductMapper productMapper;
    // 库存同步锁
    private final Lock syncLock = new ReentrantLock();
    /**
* 预减库存
*/
    public boolean preDeductStock(Long productId, Integer quantity) {
        String key = "product:stock:" + productId;
        // 使用Lua脚本保证原子性
        String luaScript =
            "local stock = redis.call('get', KEYS[1]) " +
            "if not stock then " +
            "    return -2 " +  // Redis中无库存数据
            "end " +
            "stock = tonumber(stock) " +
            "if stock >= tonumber(ARGV[1]) then " +
            "    redis.call('decrby', KEYS[1], ARGV[1]) " +
            "    return stock - tonumber(ARGV[1]) " +
            "else " +
            "    return -1 " +  // 库存不足
            "end";
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            quantity.toString()
        );
        if (result == -2) {
            // Redis中无数据,从数据库加载
            syncStockFromDB(productId);
            return preDeductStock(productId, quantity);
        }
        return result >= 0;
    }
    /**
* 关闭订单时恢复库存
*/
    public void restoreStock(Long productId, Integer quantity) {
        String key = "product:stock:" + productId;
        // 异步恢复,避免影响主流程
        CompletableFuture.runAsync(() -> {
            try {
                redisTemplate.opsForValue().increment(key, quantity);
                log.info("恢复库存成功: productId={}, quantity={}", productId, quantity);
                // 异步更新数据库
                updateStockInDB(productId, quantity);
            } catch (Exception e) {
                log.error("恢复库存失败", e);
            }
        });
    }
    /**
* 定时同步数据库库存到Redis
*/
    @Scheduled(fixedRate = 60000)  // 每分钟同步一次
    public void syncStockToRedis() {
        if (!syncLock.tryLock()) {
            return;
        }
        try {
            List<Product> products = productMapper.selectAll();
            for (Product product : products) {
                String key = "product:stock:" + product.getId();
                redisTemplate.opsForValue().set(key, product.getStock());
            }
            log.info("库存同步完成,同步{}个商品", products.size());
        } finally {
            syncLock.unlock();
        }
    }
}

四、高可用与容错设计

1. 限流与降级

/**
* 网关层限流配置
*/
@Configuration
public class RateLimitConfig {
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getHeaders().getFirst("X-User-Id")
        );
    }
    @Bean
    public RedisRateLimiter redisRateLimiter() {
        // 每秒100个请求,突发200个请求
        return new RedisRateLimiter(100, 200);
    }
}

2. 熔断与降级

/**
* 订单服务熔断降级
*/
@Service
@Slf4j
public class OrderService {
    // 创建订单的熔断器
    @CircuitBreaker(name = "createOrder", fallbackMethod = "createOrderFallback")
    @TimeLimiter(name = "createOrder")
    public CompletableFuture<Order> createOrderAsync(OrderDTO orderDTO) {
        return CompletableFuture.supplyAsync(() -> {
            // 业务逻辑
            return doCreateOrder(orderDTO);
        });
    }
    // 降级方法
    public CompletableFuture<Order> createOrderFallback(OrderDTO orderDTO, Exception e) {
        log.warn("创建订单降级,进入排队队列", e);
        // 1. 将订单请求放入队列
        // 2. 返回排队中状态
        // 3. 异步处理
        Order order = new Order();
        order.setStatus(OrderStatus.PENDING);
        order.setMessage("系统繁忙,您的订单正在排队处理");
        return CompletableFuture.completedFuture(order);
    }
}

3. 数据一致性保障

/**
* 分布式事务解决方案
*/
@Service
@Slf4j
public class OrderTransactionService {
    /**
* TCC模式解决订单创建事务
*/
    @Transactional(rollbackFor = Exception.class)
    public boolean createOrderWithTCC(Order order) {
        try {
            // Try阶段
            boolean tryResult = orderTccService.tryCreateOrder(order);
            if (!tryResult) {
                throw new RuntimeException("Try阶段失败");
            }
            // Confirm阶段
            orderTccService.confirmCreateOrder(order);
            return true;
        } catch (Exception e) {
            // Cancel阶段
            orderTccService.cancelCreateOrder(order);
            throw e;
        }
    }
}

五、监控与报警

# Prometheus监控配置
spring:
  application:
    name: seckill-service
management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus"
  metrics:
    export:
      prometheus:
        enabled: true
# 自定义指标
seckill:
  metrics:
    requests_total: "seckill_requests_total"
    success_total: "seckill_success_total"
    fail_total: "seckill_fail_total"
    duration_seconds: "seckill_duration_seconds"
/**
* 业务指标监控
*/
@Component
public class SeckillMetrics {
    private final MeterRegistry meterRegistry;
    private final Counter requestCounter;
    private final Counter successCounter;
    private final Timer seckillTimer;
    public SeckillMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.requestCounter = Counter.builder("seckill.requests.total")
            .description("秒杀请求总数")
            .register(meterRegistry);
        this.successCounter = Counter.builder("seckill.success.total")
            .description("秒杀成功总数")
            .register(meterRegistry);
        this.seckillTimer = Timer.builder("seckill.duration")
            .description("秒杀处理耗时")
            .register(meterRegistry);
    }
    public void recordRequest() {
        requestCounter.increment();
    }
    public void recordSuccess() {
        successCounter.increment();
    }
    public Timer.Sample startTimer() {
        return Timer.start(meterRegistry);
    }
    public void stopTimer(Timer.Sample sample) {
        sample.stop(seckillTimer);
    }
}

六、压测方案

/**
* JMeter压测脚本配置示例
*/
public class SeckillLoadTest {
    public static void main(String[] args) {
        // 压测场景
        // 1. 库存预热:1000件商品
        // 2. 模拟用户:10000并发
        // 3. 压测时间:5分钟
        // 4. 监控指标:
        //    - QPS:目标5000+
        //    - 响应时间:P99 < 200ms
        //    - 错误率:< 0.1%
        //    - CPU使用率:< 70%
        //    - 内存使用率:< 80%
    }
}

七、部署架构

# Docker Compose部署配置
version: '3.8'
services:
  # 应用服务
  seckill-service:
    image: seckill-service:latest
    deploy:
      replicas: 10
      resources:
        limits:
          cpus: '2'
          memory: 4G
    environment:
      - JAVA_OPTS=-Xmx3g -Xms3g -XX:+UseG1GC
  # Redis集群
  redis-cluster:
    image: redis:7-alpine
    command: redis-server /usr/local/etc/redis/redis.conf
    deploy:
      mode: global
  # MySQL集群
  mysql-master:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
  mysql-slave:
    image: mysql:8.0
    scale: 3

八、总结与展望

核心要点回顾

  1. 分层架构:清晰的系统分层,各司其职

  2. 异步化:消息队列解耦,提升吞吐量

  3. 缓存策略:多级缓存,减少数据库压力

  4. 限流降级:保护系统不被压垮

  5. 监控报警:快速发现和定位问题

优化方向

  1. 弹性伸缩:基于K8s的HPA自动扩缩容

  2. 智能库存:基于机器学习的库存预测

  3. 边缘计算:CDN边缘节点处理静态资源

  4. 服务网格:Istio实现更精细的流量控制

写给开发者的建议

  1. 代码可读性:复杂系统更需要清晰的代码结构

  2. 文档完整性:架构图、接口文档、部署手册

  3. 监控完备性:没有监控的系统就像"盲人摸象"

  4. 容错设计:总是假设任何环节都可能失败

结语

秒杀系统的设计是一个系统工程,涉及架构、算法、网络、数据库等多个领域。30分钟未支付自动关闭只是其中的一个环节,但却体现了分布式系统设计的精髓:在保证数据一致性的前提下,提供高可用、高性能的服务


思考题: 如果你的秒杀系统要在双11支持1000万QPS,你会如何调整架构? 欢迎在评论区分享你的设计方案!