Skip to content

对外提供一个API服务,客户端说超时了怎么排查 ​

如何设计一个秒杀系统 ​

秒杀的特点是:时间极短、请求量极大、库存极少、读远多于写。比如 20 万人同时抢 50 件商品,真正能成交的只有 50 个请求。设计的核心思路是漏斗式过滤:在请求到达数据库之前,层层拦截,越靠前拦下越好。

需要解决的问题有四个:扛住高并发、库存不超卖、用户体验不崩、防止脚本刷单。

一、前端层:减少无效请求 ​

  • 动静分离:商品详情、图片、JS、CSS 全部放 CDN,只有点击“抢购”时才产生动态请求。
  • 按钮控制:活动开始前按钮置灰,点击后短时间内禁止重复提交。
  • 验证码或答题:既能拦截机器脚本,又能把集中在同一瞬间的点击打散到几秒内,起到错峰作用。
  • 秒杀地址动态化:活动开始时才下发带签名的秒杀接口地址,防止提前刷接口。

二、网关层:限流与鉴权 ​

  • 按用户 ID 和 IP 限流,例如单用户每秒只允许一次请求。
  • 黑名单拦截可疑账号和 IP。
  • 校验登录 Token 和请求签名,未登录请求直接拒绝。
  • 超出系统承载能力的请求快速返回“活动太火爆,请稍后再试”。

三、缓存层:Redis 预扣库存 ​

活动开始前把库存预热到 Redis,秒杀期间不查 MySQL。

防超卖的关键是“判断库存”和“扣减库存”必须是原子操作。如果先 GET 再 DECR,高并发下两步之间可能被别的请求插入,导致多卖。用 Lua 脚本把两步合成一步:

lua
-- KEYS[1]: 库存 key,KEYS[2]: 已购用户集合,ARGV[1]: 用户 ID
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
    return -1  -- 已经买过
end
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock <= 0 then
    return 0   -- 已售罄
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1       -- 抢购成功

库存一旦变为 0,后续请求全部直接返回“已售罄”。还可以在应用本地加一个“售罄标记”,连 Redis 都不用访问。

四、消息队列:异步下单 ​

Redis 扣减成功后,不立刻同步走下单、支付全链路,而是发一条消息到 MQ,直接告诉用户“抢购成功,订单生成中”,前端轮询结果。

订单服务按数据库能承受的速度从 MQ 拉取消息,创建订单、扣减数据库库存。走到这一步的请求数量已经和库存数相当,数据库压力很小。

消费时要注意:

  • 幂等:以“用户 ID + 商品 ID”作为唯一键,防止重复消费生成多张订单。
  • 消息不丢失:开启生产者确认和消费者手动 ACK。

五、数据库层:最终兜底 ​

  • 扣库存时使用乐观条件,保证不会扣成负数:
sql
UPDATE seckill_stock SET stock = stock - 1
WHERE goods_id = 2001 AND stock > 0;
  • 订单表对(用户 ID,商品 ID)加唯一索引,从数据库层面防止重复下单。
  • 读写分离,查询走从库。

六、一致性与兜底 ​

Redis 和数据库的库存之间可能出现短暂不一致,可以:

  • 定时对账,发现差异时触发补偿。
  • 订单超时未支付则关闭订单,并把库存回补到 Redis。
  • 做好熔断降级,秒杀服务独立部署,不影响主站其他业务。

总结 ​

秒杀系统的整体链路是:CDN 和验证码拦截第一波,网关限流拦截第二波,Redis + Lua 原子预扣库存拦截绝大多数请求,剩余少量请求进入 MQ 排队,最后由数据库以乐观锁和唯一索引兜底落地。核心原则是让流量逐层递减,并确保库存扣减的原子性。


📮 联系方式

微信:BuZhiCoder | QQ:1776418445 扫码添加,备注「求职」优先通过

微信二维码

基于 VitePress 搭建

🌟训练营🤖AI面试📄做简历💻刷题