← 返回生产场景

⚡ 秒杀与大促

100 件商品 10 万人抢 —— 瞬时高并发场景下会遇到的所有问题和解法

🏗️ 秒杀系统整体架构(请求层层削减)
10万用户
点击抢购
CDN静态页
+验证码
(挡掉80%)
Nginx限流
(再挡15%)
Redis预减库存
(原子操作)
(只放100个)
MQ异步下单
(削峰)
DB下单
(慢慢处理)
核心思想:让尽量少的请求碰到数据库。每层都削减流量,最终到 DB 的只有 100 个真实订单。
问题1:超卖(卖了110件只有100件) 最严重
"查库存-减库存"非原子,两个请求同时读到 stock=1,都通过检查,都下单。
📊 超卖发生过程
stock=1 线程A 读 stock=1,>0 通过 线程B 读 stock=1,>0 通过 stock=0 stock=-1 ❌超卖 下单成功 下单成功(超卖!)

❌ 错误:查+减非原子

int s = dao.getStock(id); if (s > 0) { dao.decrStock(id); order.create(); }

✅ 正确:原子扣减

// DB 条件更新(乐观锁) int n = dao.decrIfPositive(id); // UPDATE SET stock=stock-1 // WHERE id=? AND stock>0 if (n==0) throw "售罄"; // 或 Redis 原子 long left = redis.decr(key); if (left<0){redis.incr(key);throw"售罄";}
解法核心:把"查+减"变成一个原子操作。DB 用条件更新(WHERE stock>0),Redis 用 decr(原子)。不要先查再判断再减
🌊 问题2:10万请求瞬间涌入(DB扛不住) 限流
DB 能扛几千 QPS,10 万瞬间请求直接打挂。要在请求到达 DB 前层层削减。
削峰五层(从外到内):
CDN + 静态页:商品详情页静态化,用户访问 CDN 不碰后端
前端防抖:按钮点一次置灰 3 秒,防连点
验证码/答题:拉长请求时间,削平瞬时尖峰
Nginx 限流:limit_req 每秒只放 N 个进后端
Redis 预减库存:库存提前放 Redis,原子扣减,不碰 DB
// Nginx 限流:每秒只放 1000 个请求 limit_req_zone $binary_remote_addr zone=seckill:10m rate=1000r/s; location /seckill { limit_req zone=seckill burst=100 nodelay; proxy_pass http://backend; } // Sentinel 限流(Java 应用层) @SentinelResource("seckill", blockHandler = "tooBusy") public Result seckill(Long itemId) { ... }
📬 问题3:抢到的用户下单慢(要扣库存+创订单+发短信) 异步
抢到名额后,下单流程涉及多个服务,同步处理太慢。用 MQ 异步削峰。
// 抢到名额后,不发订单,发消息 if (redisDecrSuccess) { mq.send("order.create", {userId, itemId}); return "抢购成功,正在创建订单..."; } // 消费者慢慢处理(不阻塞用户) @RabbitListener(queues = "order.create") public void handleOrder(OrderMsg msg) { orderService.create(msg); // 创建订单 inventoryService.decr(msg.itemId); // 扣DB库存 smsService.send(msg.userId); // 发短信 }
用户体验:点击抢购 → 1 秒返回"抢购成功,订单创建中" → 异步处理 → 几秒后看到订单。用户不用等完整流程走完
🤖 问题4:黄牛用脚本刷(一人抢多个) 风控

❌ 不防刷

黄牛写脚本循环调用 → 一人抢 50 个 → 真实用户抢不到

✅ 多层防刷

① 限购(userId+itemId 唯一索引,一人一件)
② IP/设备限频(同 IP 每分钟 1 次)
③ 风控规则(识别机器人)
④ 验证码(人机区分)

// 限购:用户ID+商品ID 唯一索引 try { seckillDao.insertRecord(userId, itemId); // UNIQUE(userId,itemId) } catch (DuplicateKeyException e) { return "您已抢购过"; } // Redis 限频:同用户60秒只能一次 boolean ok = redis.setIfAbsent("seckill:"+userId, "1", 60, SECONDS); if (!ok) return "操作太频繁";
🔥 问题5:库存什么时候放 Redis 预热
秒杀开始前,把库存从 DB 预热到 Redis。开始时 Redis 已有库存,直接原子扣减。
// 秒杀前 10 分钟,定时任务预热 @Scheduled(cron = "0 50 * * * ?") // 每50分检查 public void preheatStock() { if (seckillStartingSoon()) { int stock = inventoryDao.getStock(itemId); redis.set("stock:"+itemId, stock); redis.set("seckill:"+itemId+":start", "true"); } } // 活动结束后同步回 DB public void syncBack() { int left = redis.get("stock:"+itemId); inventoryDao.updateStock(itemId, left); // 回写DB }
秒杀三板斧:
限流(CDN+验证码+Nginx+Sentinel,层层削减流量)
缓存(Redis 预减库存,原子操作,不碰 DB)
异步(MQ 削峰,抢到=发消息,消费者慢慢下单)

核心心法:让尽量少的请求碰到数据库。