秒杀是电商、零售等领域常见的营销场景 —— 通过限时、限量的低价商品吸引用户抢购(如 “618 1 元抢手机”“双 11 秒杀优惠券”)。但秒杀场景的 “瞬时高并发、库存有限、一致性要求高” 三大特性,对系统提出了极致挑战:若设计不当,轻则出现 “超卖”“少卖”,重则导致系统崩溃、服务不可用。本文将从底层逻辑到实际落地,拆解秒杀系统的设计要点,并补充关键模块的实例代码。

一、先搞懂秒杀场景的核心痛点

在设计前,必须先明确秒杀系统面临的核心问题,避免 “无的放矢”:

痛点类型

具体表现

瞬时高并发

秒杀开始前用户大量涌入,瞬时请求量可达平时的 100-1000 倍(如 10 万用户抢 100 件商品)

库存一致性

多用户同时抢购同一商品,需避免 “超卖”(实际卖出超过库存)或 “少卖”(库存未卖完)

响应延迟

大量请求阻塞导致页面加载慢、按钮点击无响应,影响用户体验

恶意请求攻击

黄牛用脚本高频请求、刷库存,挤占正常用户名额

数据一致性

秒杀后库存更新、订单创建、支付状态同步需保证最终一致,避免 “下单成功但库存未扣”

二、秒杀系统的整体架构设计

秒杀系统需遵循 “流量分层过滤、核心链路解耦、数据异步处理” 的原则,从接入层到数据层逐层拦截无效请求,减少核心服务压力。整体架构分为 5 层,如下所示:

用户层 → 接入层 → 应用层 → 缓存层 → 数据层

1. 各层核心职责与技术选型

架构分层

核心职责

常用技术选型

设计目标

用户层

展示秒杀页面、接收用户请求

H5 / 小程序 / APP、静态资源 CDN

降低用户端加载延迟

接入层

拦截恶意请求、限流、负载均衡

Nginx、云 WAF、API 网关(Gateway/Spring Cloud Gateway)

过滤 80% 以上无效请求

应用层

秒杀逻辑处理(资格校验、库存预扣、订单创建)

微服务(Spring Boot/Spring Cloud)、分布式任务调度(XXL-Job)

核心逻辑解耦、水平扩展

缓存层

存储库存、热点数据、限流计数器

Redis(主从 + 哨兵)、Redisson

支撑高并发读写、减少 DB 压力

数据层

持久化订单、库存最终更新

MySQL(主从分离)、分库分表(Sharding-JDBC)

保证数据最终一致性

三、核心模块设计:从请求到下单的全链路拆解(含实例代码)

秒杀的核心链路是 “用户抢购→资格校验→库存扣减→订单创建→支付回调”,每个环节都需针对性设计,避免瓶颈。

1. 接入层:先拦掉 80% 的无效请求

接入层是秒杀的 “第一道防线”,核心目标是 “尽可能早地过滤无效请求”,减少下游服务压力。

(1)限流:控制请求总量
  • 限流粒度:按 IP、用户 ID、商品 ID 限流(如单个 IP 每秒最多 10 次请求,单个用户只能参与 1 次秒杀)。
  • 技术实现:用 Nginx 的limit_req模块或 Redis 的INCR+EXPIRE实现分布式限流。
    • Nginx 限流配置示例(按 IP 限流):

http {

# 定义限流策略:每秒最多2个请求, burst=5(允许5个请求排队,nodelay=不等待)

limit_req_zone $binary_remote_addr zone=seckill:10m rate=2r/s;

server {

listen 80;

server_name seckill.example.com;

# 秒杀接口限流

location /api/seckill/start {

limit_req zone=seckill burst=5 nodelay;

proxy_pass http://seckill-service-cluster; # 转发到应用层集群

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

}

# 静态资源直接返回(CDN未命中时)

location ~* \.(jpg|png|css|js)$ {

root /usr/local/nginx/html/seckill;

expires 1h; # 缓存1小时

}

}

}
    • Redis 分布式限流示例(按用户 + 商品维度):
import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component

public class SeckillLimiter {

    private final RedisTemplate < String, Object > redisTemplate;

    // 构造注入RedisTemplate

    public SeckillLimiter(RedisTemplate < String, Object > redisTemplate) {

        this.redisTemplate = redisTemplate;

    }

    /**

    * 检查用户是否超过秒杀请求次数

    * @param userId 用户ID

    * @param goodsId 商品ID

    * @param maxCount 最大请求次数

    * @param expireSeconds 过期时间(秒)

    * @return true=未超限,false=已超限

    */

    public boolean checkLimit(Long userId, Long goodsId, int maxCount, int expireSeconds) {

        // 限流Key:seckill:limit:用户ID:商品ID

        String limitKey = String.format("seckill:limit:%d:%d", userId, goodsId);

        // 原子递增计数器

        Long currentCount = redisTemplate.opsForValue().increment(limitKey, 1);

        // 首次设置过期时间(避免重复设置)

        if (currentCount != null && currentCount == 1) {

            redisTemplate.expire(limitKey, expireSeconds, TimeUnit.SECONDS);

        }

        // 超过最大次数则拦截

        return currentCount != null && currentCount <= maxCount;

    }

}

// 调用示例(在秒杀接口中)

@RestController

@RequestMapping("/api/seckill")

public class SeckillController {

    @Autowired

    private SeckillLimiter seckillLimiter;

    @PostMapping("/start")

    public Result < String > startSeckill(@RequestParam Long userId, @RequestParam Long goodsId) {

        // 1. 先做限流校验:10秒内最多1次请求

        boolean hasLimit = seckillLimiter.checkLimit(userId, goodsId, 1, 10);

        if (!hasLimit) {

            return Result.fail("请求过于频繁,请10秒后再试");

        }

        // 2. 后续逻辑(资格校验、库存扣减等)

        // ...

    }

}
(2)静态资源 CDN:减少动态请求
  • 将秒杀页面的图片、CSS、JS 等静态资源放到 CDN(如阿里云 CDN、腾讯云 CDN),用户直接从 CDN 加载,无需访问应用服务器。
  • 秒杀按钮默认置灰,通过前端定时器(如秒杀开始前 10 秒)动态激活,避免用户提前点击发送无效请求。

前端按钮控制示例(Vue):


<template>

<button :disabled="!isSeckillStart" @click="handleSeckill">

{{ isSeckillStart ? '立即抢购' : `距离开始还有${countdown}秒` }}

</button>

</template>

<script>

export default {

data() {

return {

seckillStartTime: 1718000000000, // 秒杀开始时间(时间戳)

isSeckillStart: false,

countdown: 0,

timer: null

};

},

mounted() {

this.initCountdown();

},

beforeDestroy() {

clearInterval(this.timer);

},

methods: {

initCountdown() {

const now = Date.now();

// 已开始

if (now >= this.seckillStartTime) {

this.isSeckillStart = true;

return;

}

// 倒计时计算

this.countdown = Math.ceil((this.seckillStartTime - now) / 1000);

// 每秒更新倒计时

this.timer = setInterval(() => {

this.countdown--;

if (this.countdown <= 0) {

this.isSeckillStart = true;

clearInterval(this.timer);

}

}, 1000);

},

handleSeckill() {

// 调用秒杀接口

this.$axios.post("/api/seckill/start", {

userId: this.$store.state.userId,

goodsId: this.goodsId

}).then(res => {

if (res.data.success) {

alert("抢购成功,订单生成中!");

} else {

alert(res.data.msg);

}

});

}

}

};

</script>

2. 应用层:核心逻辑的 “防错” 设计

应用层是秒杀的 “核心大脑”,需处理 “资格校验、库存扣减、订单创建” 三大核心逻辑,重点解决 “库存一致性” 和 “并发安全” 问题。

(1)资格校验:避免重复抢购

在处理抢购请求前,先校验用户是否具备抢购资格:

  • 校验 1:用户是否已参与过该商品的秒杀(通过 Redis 存储 “seckill:user:goodsId:userId”,存在则拦截)。
  • 校验 2:商品是否在秒杀时间内(避免秒杀未开始或已结束的请求)。
  • 校验 3:用户是否满足抢购条件(如会员等级、绑定手机号等)。

资格校验实例代码:

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service

public class SeckillQualificationService {

    private final RedisTemplate < String, Object > redisTemplate;

    private final SeckillGoodsMapper seckillGoodsMapper; // 秒杀商品DAO

    private final UserMapper userMapper; // 用户DAO

    // 构造注入

    public SeckillQualificationService(RedisTemplate < String, Object > redisTemplate,

        SeckillGoodsMapper seckillGoodsMapper,

        UserMapper userMapper) {

        this.redisTemplate = redisTemplate;

        this.seckillGoodsMapper = seckillGoodsMapper;

        this.userMapper = userMapper;

    }

    /**

    * 校验用户抢购资格

    * @param userId 用户ID

    * @param goodsId 商品ID

    * @throws BusinessException 资格不满足时抛出异常

    */

    public void checkQualification(Long userId, Long goodsId) throws BusinessException {

        // 1. 校验商品是否存在且在秒杀时间内

        SeckillGoods seckillGoods = seckillGoodsMapper.selectById(goodsId);

        if (seckillGoods == null) {

            throw new BusinessException("秒杀商品不存在");

        }

        long now = System.currentTimeMillis();

        if (now < seckillGoods.getStartTime().getTime()) {

            throw new BusinessException("秒杀尚未开始");

        }

        if (now > seckillGoods.getEndTime().getTime()) {

            throw new BusinessException("秒杀已结束");

        }

        // 2. 校验用户是否已抢购过该商品(Redis标记)

        String userSeckillKey = String.format("seckill:user:%d:%d", goodsId, userId);

        Boolean hasSeckilled = redisTemplate.hasKey(userSeckillKey);

        if (Boolean.TRUE.equals(hasSeckilled)) {

            throw new BusinessException("您已参与过该商品的秒杀,不可重复抢购");

        }

        // 3. 校验用户条件(如会员等级≥2)

        User user = userMapper.selectById(userId);

        if (user == null) {

            throw new BusinessException("用户不存在");

        }

        if (user.getMemberLevel() < 2) {

            throw new BusinessException("仅2级及以上会员可参与本次秒杀");

        }

    }

}
(2)库存扣减:解决 “超卖” 的关键

库存扣减是秒杀最容易出问题的环节,需用 “Redis 预扣减 + MySQL 最终确认” 的双层机制,兼顾性能和一致性。

方案:Redis 预扣减(高性能)+ MySQL 最终扣减(保一致)
  1. 秒杀前:库存预热到 Redis

秒杀开始前,将商品库存从 MySQL 加载到 Redis(如seckill:stock:goodsId = 100),后续库存扣减先操作 Redis。

库存预热实例代码(XXL-Job 定时任务):

import com.xxl.job.core.handler.annotation.XxlJob;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

@Component

public class SeckillStockWarmUpJob {

    private final RedisTemplate < String, Object > redisTemplate;

    private final SeckillGoodsMapper seckillGoodsMapper;

    // 构造注入

    public SeckillStockWarmUpJob(RedisTemplate < String, Object > redisTemplate,

        SeckillGoodsMapper seckillGoodsMapper) {

        this.redisTemplate = redisTemplate;

        this.seckillGoodsMapper = seckillGoodsMapper;

    }

    /**

    * 秒杀前30分钟预热库存到Redis

    * @param param 任务参数(如商品ID列表,格式:"1,2,3")

    */

    @XxlJob("seckillStockWarmUpJob")

    public void warmUpStock(String param) {

        if (param == null || param.isEmpty()) {

            throw new BusinessException("任务参数不能为空(需传入商品ID列表)");

        }

        // 解析商品ID列表

        String[] goodsIds = param.split(",");

        for (String goodsIdStr: goodsIds) {

            Long goodsId = Long.parseLong(goodsIdStr);

            // 从MySQL查询秒杀商品库存

            SeckillGoods seckillGoods = seckillGoodsMapper.selectById(goodsId);

            if (seckillGoods == null) {

                System.err.printf("商品ID:%d 不存在,跳过预热%n", goodsId);

                continue;

            }

            // 库存存入Redis(seckill:stock:goodsId = 库存数量)

            String stockKey = String.format("seckill:stock:%d", goodsId);

            redisTemplate.opsForValue().set(stockKey, seckillGoods.getStockCount());

            // 设置过期时间(秒杀结束后2小时,避免Redis内存占用)

            long expireHours = 2;

            redisTemplate.expire(stockKey, expireHours, TimeUnit.HOURS);

            System.out.printf("商品ID:%d 预热完成,库存:%d%n",

                goodsId, seckillGoods.getStockCount());

        }

    }

}
  1. 秒杀中:Redis 原子扣减库存

用 Redis 的DECR命令原子性扣减库存(避免多线程并发导致的超卖),扣减前先判断库存是否大于 0。

Redis 原子扣减实例代码:

@Service

public class SeckillStockService {

    private final RedisTemplate < String, Object > redisTemplate;

    private final RabbitTemplate rabbitTemplate; // 消息队列

    // 构造注入

    public SeckillStockService(RedisTemplate < String, Object > redisTemplate,

        RabbitTemplate rabbitTemplate) {

        this.redisTemplate = redisTemplate;

        this.rabbitTemplate = rabbitTemplate;

    }

    /**

    * Redis原子扣减库存

    * @param goodsId 商品ID

    * @param count 扣减数量(默认1)

    * @return 扣减成功返回true,失败返回false

    */

    public boolean deductStockInRedis(Long goodsId, int count) {

            String stockKey = String.format("seckill:stock:%d", goodsId);

            // 1. 原子扣减库存(DECR命令返回扣减后的库存)

            Long remainStock = redisTemplate.opsForValue().decrement(stockKey < /doubaocanvas>

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐