对外提供一个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 扫码添加,备注「求职」优先通过
