# 秒杀系统设计

在这里插入图片描述

# 整体流程

设计一个秒杀系统(如电商抢购、大促活动)是典型的高并发、大流量、高可用架构设计问题。秒杀的核心痛点在于:瞬时并发极高、防止超卖/恶意刷单。

# 秒杀系统的核心设计原则

尽量把请求拦截在下游之前:不要让所有流量都冲到数据库。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 成功生成了订单,前端引导用户进入支付页面。