← 返回生产场景
🏗️ 秒杀系统整体架构(请求层层削减)
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 削峰,抢到=发消息,消费者慢慢下单)
核心心法:让尽量少的请求碰到数据库。