秒杀系统架构与库存扣减设计
秒杀系统核心挑战分析
秒杀活动是电商系统中极具技术挑战性的业务场景,它将大量用户请求集中在极短的时间窗口内释放,对系统的承载能力形成巨大冲击。要构建一个健壮的秒杀系统,首先需要明确它面临的核心技术问题:
graph TD
A[秒杀系统核心挑战] --> B[瞬时高并发]
A --> C[热点数据竞争]
A --> D[库存准确性]
A --> E[恶意流量攻击]
A --> F[重复提交]
A --> G[业务隔离]
B --> B1[流量削峰]
B --> B2[多级过滤]
C --> C1[数据拆分]
C --> C2[缓存预热]
D --> D1[防止超卖]
D --> D2[防止少卖]
E --> E1[风控识别]
E --> E2[请求限流]
F --> F1[幂等控制]
G --> G1[资源隔离]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#E74C3C,color:#fff,rx:10,ry:10
style C fill:#E67E22,color:#fff,rx:10,ry:10
style D fill:#9B59B6,color:#fff,rx:10,ry:10
style E fill:#1ABC9C,color:#fff,rx:10,ry:10
style F fill:#34495E,color:#fff,rx:10,ry:10
style G fill:#27AE60,color:#fff,rx:10,ry:10流量层层过滤架构
整体架构设计思路
秒杀系统架构的核心理念是逐层削减流量,让绝大多数请求在到达核心服务之前就被过滤掉。一次用户请求从发起到最终处理,需要经过多个节点,每一层都承担着流量筛选的职责。
graph LR
subgraph 用户层
A[用户请求]
end
subgraph 接入层
B[CDN静态资源]
C[Nginx负载均衡]
end
subgraph 应用层
D[网关限流]
E[业务校验]
end
subgraph 缓存层
F[本地缓存]
G[Redis集群]
end
subgraph 数据层
H[(数据库)]
end
A -->|100万QPS| B
B -->|50万QPS| C
C -->|10万QPS| D
D -->|1万QPS| E
E -->|5000QPS| F
F -->|2000QPS| G
G -->|500QPS| H
style A fill:#E74C3C,color:#fff,rx:10,ry:10
style B fill:#3498DB,color:#fff,rx:10,ry:10
style C fill:#3498DB,color:#fff,rx:10,ry:10
style D fill:#9B59B6,color:#fff,rx:10,ry:10
style E fill:#9B59B6,color:#fff,rx:10,ry:10
style F fill:#E67E22,color:#fff,rx:10,ry:10
style G fill:#E67E22,color:#fff,rx:10,ry:10
style H fill:#27AE60,color:#fff,rx:10,ry:10各层过滤策略详解
客户端层过滤:在前端页面中加入随机丢弃逻辑,当检测到服务端压力过大时,部分请求直接在客户端返回"系统繁忙"提示,引导用户稍后重试。
CDN与静态资源:秒杀页面的静态资源(HTML、CSS、JS、图片等)提前推送至CDN节点,用户访问时直接从就近的CDN节点获取,大幅降低源站压力。
Nginx接入层:配置IP限流规则、黑白名单、请求频率控制等策略。例如限制单个IP每秒最多发起5次请求,超出则直接拒绝。
应用网关层:基于Sentinel等限流组件实现动态限流,可根据系统负载实时调整限流阈值。同时进行业务层面的校验,如用户登录状态、活动时间窗口等。
缓存层:将商品信息、库存数据等热点数据预加载到本地缓存和Redis中。本地缓存(如Caffeine)响应速度更快,适合存储读多写少的数据。
热点数据处理策略
秒杀商品天然就是热点数据,所有用户都在抢购同一件商品,这会导致该商品的读写请求高度集中。
数据拆分策略
将单个热点商品的库存拆分成多个子库存,分散到不同的缓存节点上。例如1000件商品可以拆分为10个子库存,每个子库存100件,用户请求被均匀路由到不同的子库存上进行扣减。
graph TD
A[商品总库存 1000件] --> B[子库存1<br/>100件]
A --> C[子库存2<br/>100件]
A --> D[子库存3<br/>100件]
A --> E[...]
A --> F[子库存10<br/>100件]
G[用户请求] --> H{负载均衡}
H --> B
H --> C
H --> D
H --> F
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#27AE60,color:#fff,rx:10,ry:10
style C fill:#27AE60,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10
style F fill:#27AE60,color:#fff,rx:10,ry:10
style G fill:#E74C3C,color:#fff,rx:10,ry:10
style H fill:#9B59B6,color:#fff,rx:10,ry:10多级缓存预热
秒杀活动开始前,需要将热点数据提前加载到各级缓存中:
- Redis预热:将商品详情、库存数量等数据写入Redis集群
- 本地缓存预热:应用启动时或定时任务将热点数据加载到本地缓存
- 多副本部署:对于Redis热Key,可以在不同的Redis节点上创建多个副本
库存扣减核心设计
库存扣减是秒杀系统中最关键的环节,必须同时满足原子性和有序性两个要求,否则会导致超卖或少卖问题。
超卖问题根源分析
超卖是指商品实际销售数量超过了可售库存。以下场景展示了超卖是如何发生的:
sequenceDiagram
participant T1 as 线程1
participant T2 as 线程2
participant DB as 数据库(库存=1)
T1->>DB: 查询库存
DB-->>T1: 返回库存=1
T2->>DB: 查询库存
DB-->>T2: 返回库存=1
T1->>T1: 判断库存>=1,通过
T2->>T2: 判断库存>=1,通过
T1->>DB: 扣减库存 stock=stock-1
DB-->>T1: 库存变为0
T2->>DB: 扣减库存 stock=stock-1
DB-->>T2: 库存变为-1(超卖!)两个并发线程同时查询到库存为1,都通过了库存校验,最终导致库存被扣减为负数。
数据库层面解决方案
最直接的解决思路是在数据库扣减时增加库存校验条件:
-- 会员权益扣减示例
UPDATE member_quota
SET remaining_times = remaining_times - #{deductCount}
WHERE member_id = '#{memberId}'
AND quota_type = '#{quotaType}'
AND remaining_times >= #{deductCount}这条SQL利用数据库的行级锁和原子操作特性,只有当剩余次数足够时才会执行扣减。如果扣减失败(影响行数为0),说明库存不足。
但这种方案在高并发下存在严重性能瓶颈:
- 多线程同时更新同一行数据时会产生锁竞争
- MySQL单行热点更新QPS通常不超过300
- 大量请求排队等待锁释放,可能导致数据库连接池耗尽
Redis + Lua脚本解决方案
更优的方案是将库存扣减操作放到Redis中执行,利用Redis单线程特性和Lua脚本的原子性保证:
-- 会员积分扣减脚本
local memberKey = KEYS[1]
local deductPoints = tonumber(ARGV[1])
-- 获取当前积分余额
local currentPoints = tonumber(redis.call('GET', memberKey))
if currentPoints == nil then
return "MEMBER_NOT_FOUND"
end
-- 判断积分是否充足
if currentPoints >= deductPoints then
-- 执行积分扣减
redis.call('DECRBY', memberKey, deductPoints)
return redis.call('GET', memberKey)
else
return "INSUFFICIENT_POINTS"
endLua脚本在Redis中以原子方式执行,中间不会被其他命令打断,完美解决了并发扣减问题。
数据一致性保障
实际生产环境中,通常采用Redis扣减+数据库持久化的组合方案:
graph TD
A[用户请求] --> B[Redis Lua脚本扣减]
B --> C{扣减成功?}
C -->|是| D[发送MQ消息]
C -->|否| E[返回库存不足]
D --> F[消费者处理]
F --> G[数据库扣减库存]
G --> H[记录扣减流水]
I[对账系统] --> J[定时比对Redis与DB]
J --> K{数据一致?}
K -->|否| L[补偿处理]
K -->|是| M[正常]
style A fill:#3498DB,color:#fff,rx:10,ry:10
style B fill:#E67E22,color:#fff,rx:10,ry:10
style D fill:#9B59B6,color:#fff,rx:10,ry:10
style G fill:#27AE60,color:#fff,rx:10,ry:10
style I fill:#E74C3C,color:#fff,rx:10,ry:10
style L fill:#E74C3C,color:#fff,rx:10,ry:10整个流程分为三个阶段:
- Redis快速扣减:利用Redis高性能特性承接高并发流量
- 异步持久化:通过MQ异步将扣减操作同步到数据库
- 对账补偿:定时任务比对Redis和数据库数据,发现不一致则进行补偿
少卖问题及对策
少卖是指因系统异常导致实际销售数量少于应售数量。典型场景是Redis扣减成功,但后续MQ消息丢失或消费失败,导致数据库库存未扣减。
解决方案:
- 流水记录:每次Redis扣减时同步记录到ZSet中,包含业务单号和时间戳
- 定时对账:扫描一段时间内的扣减流水,与数据库实际扣减记录比对
- 补偿机制:发现不一致时,根据业务规则进行库存回补或告警
库存增加设计
秒杀过程中可能需要临时追加库存,其设计思路与库存扣减类似:
-- 库存追加脚本
local itemKey = KEYS[1]
local addQuantity = tonumber(ARGV[1])
-- 直接增加库存
redis.call('INCRBY', itemKey, addQuantity)
return redis.call('GET', itemKey)库存增加相对简单,因为不存在"透支"风险。但需要注意:
- 增加操作同样需要异步同步到数据库
- 需要做好表单防重,避免重复提交导致库存多加
- 如果业务需要"设置为某个值"而非"增加多少",需要先计算差值
恶意流量防护
黄牛识别与拦截
秒杀商品往往存在价差,吸引黄牛使用脚本批量抢购。防护策略包括:
graph LR
A[用户请求] --> B{风控引擎}
B --> C[IP频率分析]
B --> D[设备指纹识别]
B --> E[行为轨迹分析]
B --> F[账号画像评估]
C --> G{风险判定}
D --> G
E --> G
F --> G
G -->|高风险| H[加入黑名单]
G -->|中风险| I[触发验证码]
G -->|低风险| J[放行请求]
style A fill:#3498DB,color:#fff,rx:10,ry:10
style B fill:#9B59B6,color:#fff,rx:10,ry:10
style G fill:#E67E22,color:#fff,rx:10,ry:10
style H fill:#E74C3C,color:#fff,rx:10,ry:10
style I fill:#F39C12,color:#fff,rx:10,ry:10
style J fill:#27AE60,color:#fff,rx:10,ry:10黑名单机制:将风控引擎识别出的高风险用户ID、IP地址、设备标识加入黑名单,在Nginx层和应用层进行双重过滤。
多维限流:基于用户ID、IP、设备等多个维度进行限流。例如限制单个用户每分钟最多下单1次,单个IP每分钟最多发起10次请求。
Token防刷机制
为防止脚本绕过前端直接调用接口,可以引入Token校验:
- 用户访问秒杀页面时,服务端生成一个一次性Token返回给前端
- 用户提交秒杀请求时必须携带该Token
- 服务端校验Token有效性,校验通过后立即使Token失效
- Token不合法或已失效则直接拒绝请求
重复下单防护
用户在秒杀时可能因网络延迟、页面卡顿等原因多次点击提交按钮,需要防止重复下单。
多层幂等控制
graph TD
A[用户提交订单] --> B{Token校验}
B -->|Token无效| C[拒绝请求]
B -->|Token有效| D{限购校验}
D -->|已达上限| E[返回已购买]
D -->|未达上限| F{分布式锁}
F -->|获取失败| G[返回处理中]
F -->|获取成功| H[创建订单]
H --> I[释放锁]
I --> J[Token失效]
style A fill:#3498DB,color:#fff,rx:10,ry:10
style B fill:#9B59B6,color:#fff,rx:10,ry:10
style D fill:#E67E22,color:#fff,rx:10,ry:10
style F fill:#E74C3C,color:#fff,rx:10,ry:10
style H fill:#27AE60,color:#fff,rx:10,ry:10- Token一次性校验:同一个Token只能使用一次
- 限购规则校验:检查用户是否已有在途订单或已达到购买上限
- 分布式锁控制:使用Redis分布式锁,同一用户同一商品同时只能有一个请求在处理
业务隔离策略
秒杀流量可能冲击到其他正常业务,需要进行隔离保护:
物理隔离
- 秒杀服务独立部署,使用独立的服务器集群
- 秒杀数据库独立部署,与主业务库分离
- Redis集群独立部署,避免热Key影响其他业务缓存
逻辑隔离
- 秒杀订单打标,便于后续统计分析和问题排查
- 独立的订单处理流程,可采用不同的风控和校验策略
- 单独的监控告警体系
业务层面优化手段
技术方案并非解决所有问题的唯一途径,合理的业务设计同样能大幅降低技术复杂度:
- 预约机制:用户需要提前预约才能参与秒杀,有效控制参与人数
- 分批开抢:将商品分多个时段开放抢购,分散瞬时流量
- 验证码/答题:秒杀前增加验证环节,既能防脚本又能错峰
- 预售模式:先付定金后付尾款,将流量分散到多个时间点
- 限购策略:每人限购N件,减少单用户的请求次数
这些业务手段在不影响用户体验的前提下,能够显著降低系统压力,使技术方案更加简洁可靠。
更新: 2025-12-04 17:42:31
原文: https://www.yuque.com/u22210564/zoxfmt/doc-30-01