【Java项目技术亮点】秒杀系统分层过滤架构:10万QPS到数据库只有100QPS的终极方案
·
秒杀活动一开,10万用户同时抢购,数据库瞬间被打挂——这是每个电商开发者的噩梦。本文将揭秘大麦网、京东等大厂如何通过分层过滤架构,让10万QPS的请求经过层层过滤后,最终到达数据库的只有100QPS,系统稳如泰山。

一、场景引入:一次秒杀活动的灾难
1.1 真实案例
某电商平台iPhone秒杀活动:
时间线:
T+0秒:秒杀开始,10万用户同时点击"立即抢购"
T+0.1秒:Nginx连接数飙到65535,部分请求直接502
T+0.5秒:应用服务器CPU飙到100%,线程池耗尽
T+1秒:Redis集群CPU飙到90%,大量命令排队
T+2秒:数据库连接池耗尽,大量超时
T+3秒:系统全面崩溃,服务不可用
T+5秒:运营发现库存超卖了2000台(实际只有1000台)
T+10秒:被迫中止活动,紧急退款
损失:
- 直接损失:超卖2000台,每台亏5000元 = 1000万
- 间接损失:品牌信誉受损,用户流失
- 技术代价:团队通宵3天修复
问题根源:没有分层过滤,10万QPS的请求全部穿透到数据库。
1.2 秒杀系统的核心矛盾
核心矛盾:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 流量特征: │
│ ├── 瞬时流量极大(10万QPS) │
│ ├── 持续时间极短(1-3秒) │
│ ├── 绝大多数请求是无效的(重复点击、脚本抢购) │
│ └── 只有极少数请求能成功(如1000台库存) │
│ │
│ 系统瓶颈: │
│ ├── 数据库:单机QPS上限约3000-5000 │
│ ├── Redis:单节点QPS上限约5万 │
│ ├── 应用服务器:线程池上限约500-1000 │
│ └── 网络带宽:有限 │
│ │
│ 解决思路: │
│ └── 在请求到达数据库之前,层层过滤掉无效请求 │
│ │
└─────────────────────────────────────────────────────────────┘
二、解决方案:分层过滤架构
2.1 整体架构
秒杀系统分层过滤架构(每层过滤掉一部分无效请求):
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 10万QPS ──→ 第一层:CDN + 页面静态化 ──→ 过滤80% → 2万QPS │
│ │
│ 2万QPS ──→ 第二层:Nginx限流 + 验证码 ──→ 过滤50% → 1万QPS │
│ │
│ 1万QPS ──→ 第三层:网关层Token校验 + 黑名单 ──→ 过滤50% → 5000QPS │
│ │
│ 5000QPS ──→ 第四层:Redis预扣库存(Lua脚本)──→ 过滤90% → 500QPS │
│ │
│ 500QPS ──→ 第五层:MQ异步下单 ──→ 削峰填谷 → 100QPS │
│ │
│ 100QPS ──→ 第六层:数据库扣减库存 ──→ 最终处理 │
│ │
└─────────────────────────────────────────────────────────────────────┘
最终到达数据库的请求:100QPS(仅为原始流量的0.1%)
2.2 各层过滤效果
| 层级 | 过滤手段 | 过滤率 | 剩余QPS | 说明 |
|---|---|---|---|---|
| 第一层 | CDN + 页面静态化 | 80% | 20,000 | 静态资源不走后端 |
| 第二层 | Nginx限流 + 验证码 | 50% | 10,000 | 限制请求频率 |
| 第三层 | Token校验 + 黑名单 | 50% | 5,000 | 拦截非法请求 |
| 第四层 | Redis预扣库存 | 90% | 500 | 库存不足直接拒绝 |
| 第五层 | MQ异步下单 | 80% | 100 | 削峰填谷 |
| 第六层 | 数据库扣减库存 | — | 100 | 最终处理 |
三、实战代码:分层过滤实现
3.1 第一层:CDN + 页面静态化
/**
* 秒杀活动页面静态化
* 将秒杀页面提前生成HTML,推送到CDN
*/
@Component
@Slf4j
public class SeckillPageService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RestHighLevelClient cdnClient;
/**
* 活动开始前,将页面静态化并推送到CDN
*/
@Scheduled(cron = "0 0 * * * ?")
public void preRenderPages() {
List<SeckillActivity> activities = seckillActivityMapper.selectToday();
for (SeckillActivity activity : activities) {
// 1. 生成静态HTML
String html = renderStaticPage(activity);
// 2. 推送到CDN
pushToCDN("/seckill/" + activity.getId() + ".html", html);
// 3. 缓存到Redis(CDN未命中时备用)
redisTemplate.opsForValue().set(
"seckill:page:" + activity.getId(),
html,
24,
TimeUnit.HOURS
);
log.info("✅ 秒杀页面静态化完成: activityId={}", activity.getId());
}
}
/**
* 渲染静态页面
*/
private String renderStaticPage(SeckillActivity activity) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html><html><head>");
html.append("<title>秒杀 - ").append(activity.getName()).append("</title>");
html.append("<script src='/static/js/seckill.js'></script>");
html.append("</head><body>");
html.append("<div class='seckill-info'>");
html.append("<h1>").append(activity.getName()).append("</h1>");
html.append("<p>价格: ¥").append(activity.getSeckillPrice()).append("</p>");
html.append("<p>库存: <span id='stock'>").append(activity.getStock()).append("</span></p>");
html.append("<button id='seckill-btn' onclick='seckill(")
.append(activity.getId()).append(")'>立即抢购</button>");
html.append("</div>");
html.append("<script>");
html.append("var startTime = ").append(activity.getStartTime().toEpochMilli()).append(";");
html.append("var now = Date.now();");
html.append("var countdown = Math.max(0, startTime - now);");
html.append("</script></body></html>");
return html.toString();
}
}
3.2 第二层:Nginx限流 + 前端验证码
# nginx.conf - 秒杀接口限流
# IP维度限流:单IP每秒最多5个请求
limit_req_zone $binary_remote_addr zone=seckill_ip:10m rate=5r/s;
# 接口维度限流:秒杀接口每秒最多2000个请求
limit_req_zone $uri zone=seckill_api:10m rate=2000r/s;
server {
listen 80;
# 秒杀接口
location /api/seckill {
# IP限流
limit_req zone=seckill_ip burst=10 nodelay;
# 接口限流
limit_req zone=seckill_api burst=500 nodelay;
# 限流后返回429
limit_req_status 429;
proxy_pass http://backend;
}
}
/**
* 秒杀Token发放接口
* 用户点击"立即抢购"前,需要先获取Token(验证码校验)
*/
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillTokenController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisRateLimiter rateLimiter;
/**
* 获取秒杀Token
* 1. 校验验证码(防机器人)
* 2. 限流(防刷)
* 3. 生成一次性Token
*/
@PostMapping("/token")
public Result<String> getSeckillToken(@RequestBody SeckillTokenDTO dto) {
Long userId = dto.getUserId();
Long activityId = dto.getActivityId();
// 1. 验证码校验
if (!verifyCaptcha(dto.getCaptchaKey(), dto.getCaptchaCode())) {
return Result.fail("验证码错误");
}
// 2. 用户维度限流(每秒最多2次请求)
String limitKey = "seckill:token:limit:" + userId;
if (!rateLimiter.tryAcquire(limitKey, 2, 5)) {
return Result.fail("操作太频繁,请稍后再试");
}
// 3. 检查黑名单
if (isBlacklisted(userId)) {
return Result.fail("账号异常,请联系客服");
}
// 4. 生成一次性Token(有效期60秒)
String token = UUID.randomUUID().toString().replace("-", "");
String tokenKey = "seckill:token:" + activityId + ":" + userId;
redisTemplate.opsForValue().set(tokenKey, token, 60, TimeUnit.SECONDS);
log.info("🎫 秒杀Token发放: userId={}, activityId={}", userId, activityId);
return Result.success(token);
}
}
3.3 第三层:Token校验 + 风控
/**
* 秒杀下单接口(核心逻辑)
*/
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillOrderController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SeckillService seckillService;
@Autowired
private RedisRateLimiter rateLimiter;
/**
* 秒杀下单
*/
@PostMapping("/order")
public Result<SeckillResult> doSeckill(@RequestBody SeckillOrderDTO dto) {
Long userId = dto.getUserId();
Long activityId = dto.getActivityId();
String token = dto.getToken();
// ====== 第三层:Token校验 + 风控 ======
// 1. Token校验(一次性Token,用后即删)
String tokenKey = "seckill:token:" + activityId + ":" + userId;
String cachedToken = (String) redisTemplate.opsForValue().get(tokenKey);
if (cachedToken == null || !cachedToken.equals(token)) {
return Result.fail("非法请求或Token已过期");
}
// 删除Token(防止重复提交)
redisTemplate.delete(tokenKey);
// 2. 风控检查
if (isRiskUser(userId)) {
return Result.fail("系统检测到异常行为");
}
// 3. 用户维度限流(每个用户只能抢一次)
String userLockKey = "seckill:user:lock:" + activityId + ":" + userId;
Boolean firstAttempt = redisTemplate.opsForValue()
.setIfAbsent(userLockKey, "1", 300, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(firstAttempt)) {
return Result.fail("您已参与过本次秒杀");
}
// ====== 第四层:Redis预扣库存 ======
return seckillService.doSeckill(userId, activityId, dto);
}
}
3.4 第四层:Redis预扣库存(Lua脚本)
/**
* 秒杀核心服务
*/
@Service
@Slf4j
public class SeckillService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 库存预扣Lua脚本
* 原子操作:判断库存 > 0 → 扣减 → 记录已售
*/
private static final String DEDUCT_STOCK_SCRIPT =
"local stockKey = KEYS[1]\n" +
"local soldKey = KEYS[2]\n" +
"local userId = ARGV[1]\n" +
"\n" +
"-- 检查是否已购买\n" +
"local isSold = redis.call('sismember', soldKey, userId)\n" +
"if tonumber(isSold) == 1 then\n" +
" return -2 -- 已购买\n" +
"end\n" +
"\n" +
"-- 检查并扣减库存\n" +
"local stock = tonumber(redis.call('get', stockKey))\n" +
"if stock == nil or stock <= 0 then\n" +
" return -1 -- 库存不足\n" +
"end\n" +
"\n" +
"-- 扣减库存\n" +
"redis.call('decr', stockKey)\n" +
"-- 记录已售用户\n" +
"redis.call('sadd', soldKey, userId)\n" +
"-- 记录销售流水\n" +
"redis.call('incr', 'seckill:sales:' .. ARGV[2])\n" +
"\n" +
"return stock - 1 -- 返回剩余库存\n";
/**
* 执行秒杀
*/
public Result<SeckillResult> doSeckill(Long userId, Long activityId, SeckillOrderDTO dto) {
String stockKey = "seckill:stock:" + activityId;
String soldKey = "seckill:sold:" + activityId;
// Lua脚本原子扣减库存
Long result = redisTemplate.execute(
new DefaultRedisScript<>(DEDUCT_STOCK_SCRIPT, Long.class),
Arrays.asList(stockKey, soldKey),
String.valueOf(userId),
String.valueOf(activityId)
);
if (result == null) {
return Result.fail("系统异常");
}
if (result == -1L) {
return Result.fail("商品已售罄");
}
if (result == -2L) {
return Result.fail("您已抢购成功,请勿重复操作");
}
log.info("🎉 秒杀成功: userId={}, activityId={}, 剩余库存={}",
userId, activityId, result);
// ====== 第五层:MQ异步下单 ======
// 发送MQ消息,异步创建订单
SeckillOrderMessage message = new SeckillOrderMessage();
message.setUserId(userId);
message.setActivityId(activityId);
message.setSkuId(dto.getSkuId());
message.setCreateTime(LocalDateTime.now());
rocketMQTemplate.convertAndSend("seckill-order-topic", message);
// 返回秒杀成功(不等待订单创建完成)
SeckillResult seckillResult = new SeckillResult();
seckillResult.setSuccess(true);
seckillResult.setMessage("抢购成功!订单正在生成中...");
seckillResult.setRemainingStock(result);
return Result.success(seckillResult);
}
}
3.5 第五层:MQ异步下单
/**
* 秒杀订单消费者
* 异步创建订单,削峰填谷
*/
@Component
@Slf4j
public class SeckillOrderConsumer {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 消费秒杀订单消息
*/
@RocketMQMessageListener(
topic = "seckill-order-topic",
consumerGroup = "seckill-order-consumer-group",
consumeMode = ConsumeMode.CONCURRENTLY,
maxReconsumeTimes = 3
)
public void onMessage(SeckillOrderMessage message) {
try {
log.info("📦 开始创建秒杀订单: userId={}, activityId={}",
message.getUserId(), message.getActivityId());
// 1. 创建订单
Order order = new Order();
order.setUserId(message.getUserId());
order.setActivityId(message.getActivityId());
order.setSkuId(message.getSkuId());
order.setStatus(OrderStatus.PAID);
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
// 2. 数据库扣减库存
stockMapper.deductStock(message.getActivityId());
// 3. 更新Redis中的库存(保持一致性)
String stockKey = "seckill:stock:" + message.getActivityId();
Long remaining = redisTemplate.opsForValue().decrement(stockKey);
log.info("✅ 秒杀订单创建成功: orderId={}, 剩余库存={}",
order.getId(), remaining);
} catch (Exception e) {
log.error("❌ 秒杀订单创建失败: userId={}", message.getUserId(), e);
// 消息会自动重试(maxReconsumeTimes=3)
// 超过重试次数后进入死信队列,人工处理
}
}
}
3.6 第六层:数据库操作
/**
* 库存Mapper
*/
@Mapper
public interface StockMapper {
/**
* 扣减库存(乐观锁)
* 使用version字段防止超卖
*/
@Update("UPDATE seckill_stock SET stock = stock - 1, version = version + 1 " +
"WHERE activity_id = #{activityId} AND stock > 0 AND version = #{version}")
int deductStock(@Param("activityId") Long activityId, @Param("version") Integer version);
}
四、高级进阶:防刷与风控
4.1 多维度防刷
/**
* 秒杀风控服务
*/
@Component
@Slf4j
public class SeckillRiskService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 用户风险评分
*/
public int getRiskScore(Long userId) {
int score = 0;
// 1. 注册时间太短(新号风险)
long registerDays = getUserRegisterDays(userId);
if (registerDays < 7) score += 30;
// 2. 历史秒杀次数异常多
int seckillCount = getSeckillCount(userId);
if (seckillCount > 50) score += 20;
// 3. 设备指纹异常(同设备多账号)
int deviceUsers = getDeviceUserCount(userId);
if (deviceUsers > 3) score += 30;
// 4. IP地址异常(同IP多账号)
int ipUsers = getIpUserCount(userId);
if (ipUsers > 5) score += 20;
return score;
}
/**
* 是否为风险用户
*/
public boolean isRiskUser(Long userId) {
int score = getRiskScore(userId);
if (score >= 50) {
// 加入黑名单
redisTemplate.opsForValue().set(
"seckill:blacklist:" + userId, "1", 24, TimeUnit.HOURS
);
return true;
}
return false;
}
}
4.2 库存预热
/**
* 秒杀库存预热
* 活动开始前,将库存加载到Redis
*/
@Component
@Slf4j
public class SeckillStockPreheatService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private StockMapper stockMapper;
/**
* 活动开始前5分钟预热
*/
@Scheduled(cron = "0 55 * * * ?")
public void preheatStock() {
List<SeckillActivity> activities = stockMapper.selectUpcomingActivities();
for (SeckillActivity activity : activities) {
String stockKey = "seckill:stock:" + activity.getId();
String soldKey = "seckill:sold:" + activity.getId();
// 设置库存
redisTemplate.opsForValue().set(stockKey,
String.valueOf(activity.getTotalStock()), 1, TimeUnit.HOURS);
// 初始化已售集合
redisTemplate.delete(soldKey);
// 初始化销售计数
redisTemplate.opsForValue().set("seckill:sales:" + activity.getId(), "0");
log.info("✅ 库存预热完成: activityId={}, stock={}",
activity.getId(), activity.getTotalStock());
}
}
}
五、预判问题与解答
Q1:Redis预扣库存和数据库扣库存怎么保证一致?
A:
一致性保障:
1. Redis预扣(快速拦截):
- Lua脚本原子操作:判断+扣减+记录
- 速度极快(< 1ms)
2. MQ异步下单(削峰填谷):
- Redis扣减成功后,发送MQ消息
- 消费者异步创建订单并扣减数据库库存
3. 最终一致性:
- 如果MQ消费失败(重试3次后进入死信队列)
- 定时任务对账:比对Redis和数据库的库存
- 发现不一致,人工介入或自动补偿
4. 超卖兜底:
- 数据库扣减使用乐观锁(version字段)
- 即使Redis出现异常,数据库也不会超卖
Q2:Token机制的作用是什么?
A:
Token机制的作用:
1. 防止重复提交:
- Token一次性使用,用后即删
- 同一个Token不能提交两次
2. 防止脚本抢购:
- 获取Token需要通过验证码
- 机器人无法自动获取Token
3. 控制请求节奏:
- 用户需要先点击"获取秒杀资格"
- 再点击"立即抢购"
- 将瞬间流量分散到两个阶段
4. 限制参与人数:
- 可以控制Token发放数量
- 只发放库存数量的Token
Q3:MQ消费失败怎么办?
A:
失败处理策略:
1. 自动重试:
- RocketMQ默认重试16次
- 我们设置maxReconsumeTimes=3
- 重试间隔:10s, 30s, 1min
2. 死信队列:
- 超过重试次数的消息进入死信队列
- 人工介入处理
3. 对账补偿:
- 定时任务比对Redis和数据库
- 发现不一致自动补偿
4. 库存回补:
- 如果订单创建失败
- Redis库存需要回补
- 可以在对账时统一处理
Q4:如何防止刷单(黄牛)?
A:
防刷策略:
1. 实名认证:
- 要求实名购买
- 限制每个身份证只能购买一件
2. 设备指纹:
- 同一设备只能购买一次
- 检测模拟器和虚拟机
3. 行为分析:
- 分析点击频率、鼠标轨迹
- 异常行为触发验证码
4. 风控评分:
- 多维度评分(注册时间、历史行为、设备信息)
- 高风险用户直接拦截
5. 限制购买数量:
- 每人限购一件
- 通过Redis Set记录已购买用户
Q5:秒杀系统的性能瓶颈在哪?
A:
性能瓶颈分析:
1. Redis:
- Lua脚本执行速度(单次<1ms)
- Redis集群QPS(单节点5万,集群可达50万)
- 优化:使用Lua脚本减少网络往返
2. MQ:
- 消息堆积处理能力
- 消费者数量可水平扩展
- 优化:增加消费者实例
3. 数据库:
- 扣减库存的写入QPS
- 优化:乐观锁 + 批量处理
4. 网络:
- 带宽瓶颈
- 优化:CDN静态化,减少后端流量
六、面试高频考点
考点1:秒杀系统的核心设计思路?
参考答案:
核心思路:分层过滤,逐层减少请求量。
1. CDN + 页面静态化:过滤80%的静态资源请求
2. Nginx限流 + 验证码:过滤50%的异常请求
3. Token校验 + 黑名单:过滤50%的非法请求
4. Redis预扣库存:过滤90%的无效请求(库存不足)
5. MQ异步下单:削峰填谷,平滑流量
6. 数据库扣减库存:最终处理(乐观锁防超卖)
关键:每一层都做减法,最终到达数据库的请求量极小。
考点2:如何防止超卖?
参考答案:
三层防超卖:
1. Redis Lua脚本:
- 原子操作:判断库存 > 0 → 扣减 → 记录已售
- 速度极快,拦截99%的超卖
2. Redis Set记录已售用户:
- 每个用户只能购买一次
- SISMEMBER检查 + SADD记录
3. 数据库乐观锁:
- UPDATE ... WHERE stock > 0 AND version = #{version}
- 最终兜底,即使Redis出问题也不会超卖
考点3:MQ在秒杀中的作用?
参考答案:
MQ的核心作用:削峰填谷
1. 异步解耦:
- 秒杀接口只负责Redis预扣库存
- 订单创建通过MQ异步处理
- 秒杀接口响应时间<10ms
2. 削峰填谷:
- 瞬时5000QPS的请求,通过MQ平滑处理
- 消费者按自己的节奏处理(如100QPS)
3. 提高可用性:
- 即使数据库短暂不可用,消息不会丢失
- 数据库恢复后继续消费
考点4:秒杀系统如何做压测?
参考答案:
压测步骤:
1. 单元压测:
- 分别压测Redis预扣库存、MQ发送、订单创建
- 找出每个环节的性能瓶颈
2. 全链路压测:
- 使用JMeter模拟真实流量
- 从10万QPS开始,逐步增加
3. 瓶颈分析:
- 监控各层QPS、响应时间、错误率
- 找到最慢的环节,针对性优化
4. 容量规划:
- 根据压测结果确定服务器配置
- 预留20%的缓冲空间
七、总结与最佳实践
7.1 核心要点回顾
秒杀系统分层过滤核心流程:
┌─────────────────────────────────────────────────────────────┐
│ 1. 活动前准备 │
│ ├── 页面静态化 → CDN │
│ ├── 库存预热 → Redis │
│ └── Token预热 → 按库存数量发放 │
│ │
│ 2. 秒杀流程 │
│ ├── 获取Token(验证码校验 + 限流) │
│ ├── 提交秒杀(Token校验 + 风控 + Redis预扣库存) │
│ ├── MQ异步下单(削峰填谷) │
│ └── 数据库扣减库存(乐观锁兜底) │
│ │
│ 3. 兜底保障 │
│ ├── 对账补偿(定时比对Redis和数据库) │
│ ├── 死信队列处理(人工介入) │
│ └── 库存回补(失败时恢复库存) │
└─────────────────────────────────────────────────────────────┘
7.2 性能提升数据
某电商平台实测数据:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 峰值QPS承受能力 | 3,000 | 100,000 | 33倍↑ |
| 秒杀接口响应时间 | 500ms | 10ms | 50倍↑ |
| 数据库峰值QPS | 10,000 | 100 | 99%↓ |
| 超卖率 | 5% | 0% | 100%↓ |
| 系统可用性 | 60% | 99.99% | 显著提升 |
八、参考与拓展
互动讨论:你们公司做过秒杀系统吗?遇到过什么问题?欢迎在评论区分享!
如果本文对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,持续获取更多Java后端技术干货!
更多推荐




所有评论(0)