# 秒杀系统设计

# 整体流程
设计一个秒杀系统(如电商抢购、大促活动)是典型的高并发、大流量、高可用架构设计问题。秒杀的核心痛点在于:瞬时并发极高、防止超卖/恶意刷单。
# 秒杀系统的核心设计原则
尽量把请求拦截在下游之前:不要让所有流量都冲到数据库。100万个请求,最后只有100个商品,其实99.9%的请求都是无意义的。
读写分离/缓存为主:活动开始前,库存数据提前预热到缓存中,所有的查询和扣减库存操作尽量在缓存中完成。
异步处理:抢到资格后,后续的下单、支付等操作通过消息队列异步处理,削峰填谷。
防止超卖:这是底线。无论并发多高,卖出的商品数量绝对不能超过库存。
# 秒杀系统的漏斗架构设计
我们可以把整个秒杀系统分为以下几层来进行“层层拦截”:

# 客户端(前端优化)
CDN 加速:秒杀页面上的静态资源(HTML、CSS、JS、商品图片)全部推送到 CDN,绝对不能让这些请求打到源站服务器。
按钮防刷:用户点击“抢购”后,按钮立刻变灰并置入倒计时(如 3 秒内只能点击一次),防止用户疯狂连击。
动态 URL:秒杀开始前,抢购接口的 URL 是隐藏的或随机生成的。只有在秒杀开始的那一瞬,由后端下发动态的 MD5 盐值,前端拼接后才能发起请求,防止黄牛使用脚本提前刷接口。
# 接入层(网关/Nginx)
限流(Rate Limiting):在 Nginx 或微服务网关(如 Spring Cloud Gateway)使用令牌桶或漏斗算法。对单个 IP、单个用户 ID 限制每秒最高访问频次,直接拦截掉异常的刷单脚本。
黑名单:通过风控系统,将频繁刷接口的恶意 IP 或用户加入黑名单,直接返回 403。
# 应用层(服务层)
单机限流与隔离:秒杀业务一定要独立部署(微服务隔离),不能因为秒杀服务挂了,导致公司其他的购物车、正常下单业务也跟着瘫痪。
队列削峰(MQ):当请求通过了限流,不要直接操作数据库,而是将下单请求封装成消息发送到消息队列(如 Kafka、RabbitMQ)。商品服务作为消费者,按照自己的处理能力,异步、匀速地从队列中拉取消息进行消费。
# 缓存层(秒杀核心)
Redis 预热库存:活动开始前,将商品库存同步到 Redis 中。
Lua 脚本原子扣减:利用 Redis 的单线程特性和 Lua 脚本,将“判断库存是否足够”和“扣减库存”这两个步骤合并为一个原子操作,防止并发超卖。
Lua 脚本伪代码思路:
local count = tonumber(redis.call('get', KEYS[1]))
if count > 0 then
redis.call('decr', KEYS[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
end
分布式锁(防止重复下单):用 Redis 的 SETNX,以 User_ID + Goods_ID 作为 Key 加上限时锁,确保一个用户在一次秒杀中只能成功提交一次。
# 数据库层(DB)
乐观锁兜底:虽然 Redis 已经过滤了绝大部分请求,但数据库层依然要做最后防线。
乐观锁方案:update t_goods set stock = stock - 1 where id = #id# and stock > 0; (利用数据库单行更新锁,只要 stock > 0 就能防止超卖)。
读写分离与分库分表:如果秒杀订单量依然巨大,可以考虑将订单表进行分库分表,提升数据库的吞吐量。
# 关键痛点解决方案
# 如何防止超卖?
第一道防线:Redis + Lua 脚本。因为 Redis 是单线程的,Lua 脚本执行期间不会被其他命令干扰,能保证判断和扣减的原子性。
第二道防线:数据库 SQL 必须带上 where stock > 0 的条件,利用数据库自带的行锁做最终兜底。
# 扣了库存,用户不支付怎么办(预扣库存 vs 实际扣库存)?
下订单时预扣缓存库存,并给订单设置一个较短的超时时间(如 5-10 分钟)。
如果用户超时未支付,利用延迟队列触发订单取消,并在 Redis 中将库存加回去(回滚库存)。这样既不会导致超卖,也能防止“恶意占位不支付”的情况。
# 怎么知道秒杀结果?(异步排队体验)
用户在界面点击抢购后,由于采用了 MQ 削峰,后端不会立刻返回“成功”或“失败”,而是返回一个“排队中...”的状态。
前端随后开启轮询(或者通过 WebSocket),每隔 1-2 秒向后端查询一次该用户的订单生成状态。一旦后端消费 MQ 成功生成了订单,前端引导用户进入支付页面。
← MongoDB架构详解 红包系统设计 →