秒杀系统设计全指南:从痛点分析到高可用落地
秒杀是电商、零售等领域常见的营销场景 —— 通过限时、限量的低价商品吸引用户抢购(如 “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 最终扣减(保一致)
- 秒杀前:库存预热到 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());
}
}
}
- 秒杀中: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>
更多推荐
所有评论(0)