Skip to content

面试官:我们来聊聊你们公司做的那个“618大促秒杀”项目,能详细说说整个秒杀模块是怎么设计和实现的吗?


面试者:好的,没问题。

我们做的这个秒杀系统是为电商平台“优购商城”设计的,主要支持每年618、双11等大促活动。比如:iPhone 15 原价6999,秒杀价1元,限量100台。

这种场景下,瞬间可能有几十万甚至上百万用户同时点击“立即秒杀”,系统必须扛得住、不崩溃、不超卖、用户体验还不能太差。


📌 一、整体架构:我们用了“五层防护”架构

用户 → Nginx → API网关 → 秒杀服务 → Redis → RabbitMQ → MySQL → Canal → 通知
  • Nginx:做负载均衡 + 限流(防刷)
  • API网关:统一鉴权、黑白名单、接口限流
  • 秒杀服务:核心逻辑,判断能不能抢
  • Redis:提前把库存放进去,快速扣减(抗住90%请求)
  • RabbitMQ:异步下单,削峰填谷
  • MySQL:最终落单,保证数据持久
  • Canal:监听订单表,发短信/APP通知

✅ 这个架构的核心思想是:前端快,中间缓,后端稳


📌 二、数据库表设计(真实生产表结构)

我们设计了3张核心表:

1. seckill_activity:秒杀活动表

sql
CREATE TABLE `seckill_activity` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `title` VARCHAR(100) NOT NULL COMMENT '活动标题,如:iPhone 15 1元秒杀',
  `product_id` BIGINT NOT NULL COMMENT '商品ID',
  `start_time` DATETIME NOT NULL COMMENT '开始时间',
  `end_time` DATETIME NOT NULL COMMENT '结束时间',
  `total_stock` INT NOT NULL COMMENT '总库存,如100',
  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未开始,1进行中,2已结束',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_product_id (product_id),
  INDEX idx_time (start_time, end_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

📌 说明:status 字段用于控制活动状态,避免用户在未开始或已结束时还能请求。


2. seckill_product:秒杀商品详情

sql
CREATE TABLE `seckill_product` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `activity_id` BIGINT NOT NULL,
  `product_id` BIGINT NOT NULL,
  `seckill_price` DECIMAL(10,2) NOT NULL COMMENT '秒杀价,如1.00',
  `stock` INT NOT NULL COMMENT '可用库存',
  `version` INT DEFAULT 0 COMMENT '乐观锁版本号',
  FOREIGN KEY (activity_id) REFERENCES seckill_activity(id),
  FOREIGN KEY (product_id) REFERENCES product(id),
  UNIQUE KEY uk_activity_product (activity_id, product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

📌 说明:version 是乐观锁,防止MySQL层超卖。


3. seckill_order:秒杀订单表

sql
CREATE TABLE `seckill_order` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `product_id` BIGINT NOT NULL,
  `activity_id` BIGINT NOT NULL,
  `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号,雪花算法生成',
  `amount` DECIMAL(10,2) NOT NULL,
  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0创建中,1成功,2失败',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user (user_id),
  INDEX idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

📌 说明:order_no 全局唯一,防止重复下单;status 用于对账。


📌 三、Redis 缓存设计(真实Key规范)

Key类型说明
seckill:info:1001Hash活动1001的商品信息(title, price, time)
seckill:stock:2001String商品2001的剩余库存(用于预减)
seckill:user:10001:2001String用户10001是否已参与过商品2001的秒杀
seckill:order:no:20250823123456String订单号是否存在(防重)

✅ 所有Key的TTL设置为:活动结束时间 + 1小时,避免长期占用内存。


📌 四、缓存预热(真实流程)

问题:如果活动一开始,Redis里没有数据,所有请求直接打到MySQL,系统就崩了。

解决方案:我们做了缓存预热

预热时机:

  • 活动开始前 10分钟,定时任务自动触发。

预热内容:

java
// 伪代码
List<SeckillActivity> activities = activityService.getStartingSoon();
for (SeckillActivity act : activities) {
    SeckillProduct product = productMapper.selectByActId(act.getId());
    
    // 写入商品信息
    redis.set("seckill:info:" + act.getId(), product);
    
    // 写入库存
    redis.set("seckill:stock:" + product.getProductId(), product.getStock());
}

预热失败怎么办?

  • 重试3次;
  • 仍失败 → 钉钉告警 → 运维人工介入;
  • 设置“降级开关”,预热失败则关闭秒杀入口。

📌 五、前端优化(真实用户体验策略)

我们做了4点优化,让用户“感觉快”:

  1. 页面静态化:秒杀页面提前生成HTML,不走后端渲染;
  2. 按钮置灰:活动未开始时,“立即秒杀”按钮不可点击;
  3. 滑块验证码:点击后弹出滑块,增加脚本成本;
  4. 异步返回:不立即返回订单号,而是说“秒杀成功,请等待出单”,避免用户反复刷新。

✅ 这些优化让90%的用户在100ms内得到响应,体验很好。


📌 六、核心流程(真实请求处理链路)

1. 用户点击“立即秒杀”

2. Nginx:限流(单IP QPS ≤ 2)

3. API网关:校验登录 + 黑名单拦截

4. 秒杀服务:
   - 检查活动是否开始(查Redis)
   - 检查用户是否已参与(Redis)
   - Lua脚本预减库存:
        if redis.get(stock) > 0 then
            redis.decr(stock)
            return 1
        else
            return 0
        end

   成功 → 发MQ消息 → 返回“秒杀成功”
   失败 → 返回“已售罄”

5. RabbitMQ消费者:
   - 查MySQL,防止重复下单
   - UPDATE seckill_product SET stock = stock - 1, version = version + 1 WHERE id = ? AND stock >= 1 AND version = ?
   - 成功:插入订单表 status=1
         失败:插入订单表 status=2

6. Canal监听订单表 → 发短信:“恭喜您抢到iPhone!”

📌 七、难点与解决方案(真实项目踩坑总结)

难点问题描述解决方案
🔴 超卖多个请求同时扣库存,导致库存为负1. Redis Lua 脚本原子扣减
2. MySQL 乐观锁 + version字段
3. 唯一索引防重复下单
🔴 热点Key某个商品太火,seckill:stock:2001 成为热点库存分段:seckill:stock:2001:0 ~ 9,随机选段减库存
🔴 Redis宕机缓存不可用,请求打穿到MySQL1. Redis Cluster高可用
2. 本地缓存降级(Caffeine)
3. 降级开关控制
🔴 MQ积压下单消息太多,处理不过来1. 消费者扩容
2. 死信队列 + 重试机制
3. Prometheus监控告警
🔴 数据不一致Redis库存为0,MySQL还有库存1. 每日凌晨对账任务
2. 差异>10条则告警
3. 提供“同步库存”人工工具
🔴 脚本刷单黑产用脚本疯狂抢购1. 滑块验证码
2. 设备指纹
3. 用户级限流(QPS≤1)

📌 八、监控与压测(真实运维保障)

项目做法
监控Prometheus + Grafana 监控:
- Redis内存/连接数
- MQ队列长度
- 秒杀成功率
告警钉钉/企业微信:
- 预热失败
- MQ积压>1000条
- 缓存命中率<95%
压测JMeter全链路压测:
- 目标QPS:5万
- RT(99%)< 200ms
- 提前3天演练

📌 九、总结:我们是怎么做到“零超卖、高可用”的?

  1. Redis预减库存:抗住90%的请求;
  2. MQ异步下单:削峰填谷,保护数据库;
  3. MySQL乐观锁:最终一致性保障;
  4. 缓存预热 + 降级开关:确保系统稳定;
  5. 对账 + 人工补偿:兜底,万无一失。

✅ 这套系统在去年双11支持了 8万QPS零超卖无重大故障,用户满意度很高。

完整项目结构

新建一个 Spring Boot 项目,依次创建这些文件。


1. pom.xml —— Maven 依赖

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>seckill-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>Seckill Demo</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.21</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. SeckillApplication.java —— 启动类

java
package com.example.seckill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SeckillApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class, args);
    }
}

3. application.yml —— 配置文件

yaml
server:
  port: 8080

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 5s
    lettuce:
      pool:
        max-active: 20

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        retry:
          enabled: true
          max-attempts: 3
          max-interval: 10000ms

  datasource:
    url: jdbc:mysql://localhost:3306/seckill_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

4. model/SeckillOrder.java —— 订单实体

java
package com.example.seckill.model;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

@Data
public class SeckillOrder {
    private Long id;
    private Long userId;
    private Long productId;
    private Long activityId;
    private String orderNo;
    private BigDecimal amount;
    private Integer status; // 0创建中,1成功,2失败
    private Date createdAt;
}

5. model/SeckillResult.java —— 统一返回格式

java
package com.example.seckill.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillResult {
    private boolean success;
    private String message;

    public static SeckillResult success(String msg) {
        return new SeckillResult(true, msg);
    }

    public static SeckillResult fail(String msg) {
        return new SeckillResult(false, msg);
    }
}

6. mq/SeckillMessage.java —— MQ消息体

java
package com.example.seckill.mq;

import java.io.Serializable;

public class SeckillMessage implements Serializable {
    private Long userId;
    private Long productId;
    private Long activityId;
    private Long timestamp;

    // 省略 getter/setter/toString
    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }
    public Long getProductId() { return productId; }
    public void setProductId(Long productId) { this.productId = productId; }
    public Long getActivityId() { return activityId; }
    public void setActivityId(Long activityId) { this.activityId = activityId; }
    public Long getTimestamp() { return timestamp; }
    public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }

    @Override
    public String toString() {
        return "SeckillMessage{" +
                "userId=" + userId +
                ", productId=" + productId +
                ", activityId=" + activityId +
                ", timestamp=" + timestamp +
                '}';
    }
}

7. mq/RabbitConfig.java —— RabbitMQ 配置

java
package com.example.seckill.mq;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitConfig {

    public static final String SECKILL_ORDER_QUEUE = "seckill.order.queue";
    public static final String SECKILL_DEAD_QUEUE = "seckill.dead.queue";

    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable(SECKILL_ORDER_QUEUE).build();
    }

    @Bean
    public Queue deadQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", "");
        args.put("x-dead-letter-routing-key", SECKILL_ORDER_QUEUE);
        args.put("x-message-ttl", 5000); // 5秒后重试
        return QueueBuilder.durable(SECKILL_DEAD_QUEUE).withArguments(args).build();
    }

    @Bean
    public DirectExchange exchange() {
        return new DirectExchange("");
    }

    @Bean
    public Binding binding() {
        return BindingBuilder.bind(orderQueue()).to(exchange()).with(SECKILL_ORDER_QUEUE);
    }
}

8. mapper/SeckillOrderMapper.java —— MyBatis Mapper

java
package com.example.seckill.mapper;

import org.apache.ibatis.annotations.*;
import com.example.seckill.model.SeckillOrder;

@Mapper
public interface SeckillOrderMapper {

    @Insert("INSERT INTO seckill_order (user_id, product_id, activity_id, order_no, amount, status, created_at) " +
            "VALUES (#{userId}, #{productId}, #{activityId}, #{orderNo}, #{amount}, #{status}, #{createdAt})")
    void insert(SeckillOrder order);

    @Select("SELECT COUNT(*) FROM seckill_order WHERE user_id = #{userId} AND product_id = #{productId}")
    int countByUserAndProduct(@Param("userId") Long userId, @Param("productId") Long productId);

    @Update("UPDATE seckill_product SET stock = stock - #{count}, version = version + 1 " +
            "WHERE id = #{activityId} AND stock >= #{count} " +
            "AND version = (SELECT version FROM (SELECT version FROM seckill_product WHERE id = #{activityId}) AS tmp)")
    int reduceStockWithVersion(@Param("activityId") Long activityId, @Param("count") Integer count);
}

9. util/SnowflakeIdGenerator.java —— 雪花ID生成器

java
package com.example.seckill.util;

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;

public class SnowflakeIdGenerator {
    private static final Snowflake snowflake = IdUtil.createSnowflake(1, 1);

    public static long nextId() {
        return snowflake.nextId();
    }

    public static String nextIdStr() {
        return String.valueOf(nextId());
    }
}

10. service/SeckillService.java —— 核心服务

java
package com.example.seckill.service;

import com.example.seckill.mq.SeckillMessage;
import com.example.seckill.mapper.SeckillOrderMapper;
import com.example.seckill.model.SeckillResult;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;

@Service
public class SeckillService {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private SeckillOrderMapper orderMapper;

    private static final String LUA_REDUCE_STOCK =
        "local stock = redis.call('GET', KEYS[1])\n" +
        "if not stock or tonumber(stock) <= 0 then\n" +
        "    return 0\n" +
        "end\n" +
        "redis.call('DECR', KEYS[1])\n" +
        "return 1";

    public SeckillResult handleSeckill(Long userId, Long productId) {
        try {
            if (userId == null || productId == null) {
                return SeckillResult.fail("参数错误");
            }

            Long activityId = 1001L; // 模拟活动ID

            // 检查活动状态
            String status = redisTemplate.opsForValue().get("seckill:status:" + activityId);
            if (!"1".equals(status)) {
                return SeckillResult.fail("活动未开始或已结束");
            }

            // 防重:用户是否已参与
            String userKey = "seckill:user:" + userId + ":" + productId;
            Boolean hasParticipated = redisTemplate.hasKey(userKey);
            if (Boolean.TRUE.equals(hasParticipated)) {
                return SeckillResult.fail("您已参与过该活动");
            }

            // Redis 预减库存
            String stockKey = "seckill:stock:" + productId;
            List<String> keys = Collections.singletonList(stockKey);

            Long result = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(LUA_REDUCE_STOCK, Long.class),
                keys
            );

            if (result == 1L) {
                redisTemplate.opsForValue().set(userKey, "1", java.time.Duration.ofHours(24));

                SeckillMessage message = new SeckillMessage();
                message.setUserId(userId);
                message.setProductId(productId);
                message.setActivityId(activityId);
                message.setTimestamp(System.currentTimeMillis());

                rabbitTemplate.convertAndSend("seckill.order.queue", message);

                return SeckillResult.success("秒杀成功,请等待出单");
            } else {
                return SeckillResult.fail("手慢了,已售罄");
            }
        } catch (Exception e) {
            e.printStackTrace();
            return SeckillResult.fail("系统繁忙,请稍后再试");
        }
    }
}

11. controller/SeckillController.java —— 控制器

java
package com.example.seckill.controller;

import com.example.seckill.model.SeckillResult;
import com.example.seckill.service.SeckillService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {

    @Resource
    private SeckillService seckillService;

    @PostMapping("/execute")
    public SeckillResult execute(
            @RequestParam Long userId,
            @RequestParam Long productId) {
        return seckillService.handleSeckill(userId, productId);
    }
}

12. mq/SeckillOrderConsumer.java —— 消费者

java
package com.example.seckill.mq;

import com.example.seckill.mapper.SeckillOrderMapper;
import com.example.seckill.model.SeckillOrder;
import com.example.seckill.util.SnowflakeIdGenerator;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.Date;

@Component
public class SeckillOrderConsumer {

    @Autowired
    private SeckillOrderMapper orderMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RabbitListener(queues = RabbitConfig.SECKILL_ORDER_QUEUE)
    public void listen(SeckillMessage message, 
                       org.springframework.amqp.core.Message amqpMessage,
                       @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        try {
            // 防重
            int count = orderMapper.countByUserAndProduct(message.getUserId(), message.getProductId());
            if (count > 0) {
                System.out.println("重复下单,跳过: " + message.getUserId());
                rabbitTemplate.getRabbitAdmin().getRabbitTemplate().getChannel().basicAck(deliveryTag, false);
                return;
            }

            // 扣库存
            int updated = orderMapper.reduceStockWithVersion(message.getActivityId(), 1);
            boolean success = updated == 1;

            // 创建订单
            SeckillOrder order = new SeckillOrder();
            order.setUserId(message.getUserId());
            order.setProductId(message.getProductId());
            order.setActivityId(message.getActivityId());
            order.setOrderNo(SnowflakeIdGenerator.nextIdStr());
            order.setAmount(new BigDecimal("1.00"));
            order.setStatus(success ? 1 : 2);
            order.setCreatedAt(new Date());
            orderMapper.insert(order);

            rabbitTemplate.getRabbitAdmin().getRabbitTemplate().getChannel().basicAck(deliveryTag, false);
        } catch (Exception e) {
            e.printStackTrace();
            try {
                // 简单重试逻辑(实际可用死信队列)
                Integer retry = (Integer) amqpMessage.getMessageProperties().getHeaders().get("x-retry-count");
                retry = retry == null ? 0 : retry;
                if (retry < 3) {
                    amqpMessage.getMessageProperties().getHeaders().put("x-retry-count", retry + 1);
                    rabbitTemplate.convertAndSend(RabbitConfig.SECKILL_ORDER_QUEUE, amqpMessage);
                }
                rabbitTemplate.getRabbitAdmin().getRabbitTemplate().getChannel().basicNack(deliveryTag, false, false);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

✅ 数据库建表 SQL

sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS seckill_db CHARACTER SET utf8mb4;

USE seckill_db;

-- 秒杀商品表
CREATE TABLE seckill_product (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    activity_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    seckill_price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    version INT NOT NULL DEFAULT 0,
    UNIQUE KEY uk_product (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 秒杀订单表
CREATE TABLE seckill_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    activity_id BIGINT NOT NULL,
    order_no VARCHAR(32) UNIQUE NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id),
    INDEX idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

✅ 如何测试?

  1. 启动 MySQL,执行建表 SQL
  2. 启动 Redis
  3. 启动 RabbitMQ
  4. 运行 Spring Boot 项目
  5. 使用 Postman 或 curl 发起请求:
bash
POST http://localhost:8080/api/seckill/execute?userId=1001&productId=2001