sequenceDiagram
participant User as 用户
participant Frontend as 前端/App
participant Redis as Redis缓存
participant MQ as 消息队列
participant OrderService as 订单服务
participant DB as 数据库
User->>Frontend: 点击"立即抢购"
Frontend->>Frontend: 校验(未开始/验证码)
Frontend->>Redis: 请求扣减库存 (Lua脚本)
alt 库存不足 或 用户已买
Redis-->>Frontend: 返回失败 (秒杀结束)
Frontend-->>User: 提示"手慢了"
else 库存充足
Redis->>Redis: 预扣库存 (decr)
Redis-->>Frontend: 返回成功 (排队中)
Frontend->>MQ: 发送"创建订单"消息
Frontend-->>User: 提示"正在为您排队..."
loop 轮询结果
Frontend->>Backend: 查询订单结果
end
end
MQ->>OrderService: 消费消息
OrderService->>DB: 1. 插入订单 (唯一索引防重)
OrderService->>DB: 2. 扣减真实库存
alt 数据库事务成功
OrderService-->>Redis: 标记订单创建成功
else 异常/重复
OrderService-->>Redis: 回滚Redis库存
end
秒杀系统的核心挑战
秒杀系统的特点是时间短、并发高、资源少,核心要解决的问题是:
- 高并发流量:防止流量直接击垮数据库
- 超卖/少卖:保证库存的严格一致
- 防刷/安全:防止机器人等恶意脚本抢单
前端进行削峰
前端是流量的第一大关口,可以把绝大部分无效请求拦截在最外层,让少量有效请求进入后端。
- 页面静态化(CDN): 静态资源部署到CDN,减少业务服务器的压力
- 按钮控制:未开始的时候置灰、点击后立即禁用
- 答题/验证码:点击抢购前弹出验证码或简单数学题
- 接口动态校验:先请求一个获取Token的接口,拿到随机的path_id,再拼装成/seckill/{path_id}/buy,后台校验该 URL 的合法性。
redis锁库存设计
- 库存预热:秒杀开始前,把库存加载到redis中,比如 set stock:1001 100
- 原子扣减
- 不要使用简单的
get和set,因为在并发环境下二者操作的时间差会导致超卖 - 不要单纯依赖
decr+代码判断ret>0,因为要防止减成负数- 因为如果用户退款,需要回滚库存,如果扣成负数,则乱套了
- 必须做成Lua脚本,将
判断库存>0和库存扣减做成原子操作1 2 3 4 5 6 7 8local key = KEYS[1] local stock = tonumber(redis.call('get', key)) if stock <= 0 then return -1 else redis.call('decr', key) return 1 end
- 不要使用简单的
- 一人一单:SETNX、布隆过滤器。这一步需要放到上述Lua里面
MQ削峰填谷
redis扣减库存成功后,不是直接去写库。而是把这一条“创建订单”的消息发送到MQ,完成这个步骤后就可以给前端返回“抢购成功”
下游服务按照自己的处理能力慢慢排队去执行下单逻辑
限流
用限流算法,将过量请求直接拦截掉。
todo::限流的策略
因为目的是促成订单交易成功,把库存的东西卖出去,而不是追求大而无用的请求
数据库设计
数据库是最后一道防线,主要用于数据持久化和最终一致性保证。
- 表和字段设计
- 订单表内,uid和商品id要创建唯一索引,这样即使上层出错,也会利用数据库的
Duplicate Key Error保证只有一个订单生成
- 订单表内,uid和商品id要创建唯一索引,这样即使上层出错,也会利用数据库的
- 扣库存SQL: 建议落库的时候再做一次乐观锁校验
1UPDATE seckill_goods SET stock_count = stock_count - 1 WHERE goods_id = ? AND stock_count > 0
付款超时处理
下单后加入到延时队列,到时间内没付款则关闭订单、回滚库存