一、秒杀系统核心挑战
挑战类型 | 具体问题 |
---|---|
瞬时高并发 | 数万~百万级QPS集中在秒杀开始瞬间 |
超卖问题 | 库存扣减的原子性,避免卖超 |
恶意请求 | 黄牛脚本、DDoS攻击 |
系统稳定性 | 高并发下数据库、缓存、服务链路的雪崩风险 |
数据一致性 | 库存、订单、支付状态的一致性 |
二、分层架构设计
1. 整体架构(分层削峰)
plaintext
用户层
│
▼
CDN静态资源缓存(HTML/JS/CSS)
│
▼
接入层:Nginx集群(限流+动静分离)
│
▼
应用层:秒杀微服务(无状态集群)
│ │
▼ ▼
缓存层 消息队列
Redis集群 Kafka/RocketMQ
│ │
▼ ▼
数据层 订单服务
MySQL集群(分库分表)
2. 关键组件职责
- CDN:缓存静态页面(如秒杀倒计时页面),减少服务器压力。
- Nginx :
- 限流:IP/用户级请求限制(如令牌桶算法)。
- 动静分离:将动态请求(如抢购)和静态请求(如商品图片)分流。
- 秒杀服务 :
- 预校验:用户资格、秒杀状态、库存缓存。
- 异步化:请求快速进入队列,避免同步阻塞。
- Redis :
- 库存预热:提前加载秒杀库存到Redis。
- 原子扣减:通过
DECR
或Lua脚本保证原子性。
- 消息队列 :
- 削峰填谷:将瞬时请求转为异步处理。
- 顺序消费:保证先到先得的公平性(可选)。
- MySQL :
- 最终库存一致性:通过消息队列异步同步。
- 订单分库分表:按用户ID哈希分片。
三、详细设计要点
1. 秒杀流程设计
plaintext
1. 用户进入秒杀页(静态页,CDN缓存)
2. 点击"立即抢购"时:
a. 前端JS限制频繁点击(如5秒内只能提交1次)
b. 请求携带Token(由服务端预生成,防CSRF)
3. 服务端处理:
a. Nginx层限流(如单IP 10次/秒)
b. 预校验:
- 是否黑名单用户
- Redis检查活动是否开始/结束
- 本地缓存检查用户是否已参与过
c. Redis原子扣减库存(DECR或Lua脚本)
d. 扣减成功则生成订单ID,写入消息队列
e. 返回"抢购中"状态,轮询查询结果
4. 消费者服务:
a. 从队列获取订单请求
b. 创建订单(MySQL事务)
c. 更新Redis中的订单状态
2. 库存扣减方案
方案一:Redis原子操作 + 异步落库
lua
-- Lua脚本保证原子性
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 成功
else
return 0 -- 失败
end
方案二:令牌桶算法
- 提前将库存拆分为令牌放入Redis,抢到令牌才有资格下单。
3. 防刷与安全
-
Token机制 :
- 秒杀开始前,服务端生成加密Token,前端提交时校验。
-
分级限流 :
plaintext1. Nginx层:IP限流(滑动窗口算法) 2. 网关层:用户ID限流(如每秒5次) 3. 服务层:商品ID限流(根据库存比例)
-
人机验证 :
- 活动开始前要求完成滑块验证或短信验证码。
4. 数据一致性保障
- 库存一致性 :
- Redis库存扣减成功后,通过消息队列异步更新MySQL。
- 定时任务对比Redis与MySQL库存差异,自动修正。
- 订单状态一致性 :
- 使用本地消息表或事务消息(如RocketMQ)保证创建订单与扣库存的最终一致。
四、性能优化手段
1. 读优化
-
多级缓存 :
plaintext1. 本地缓存(Caffeine):活动配置、黑名单用户 2. Redis集群:库存数量、秒杀结果 3. MySQL:最终数据持久化
-
缓存预热 :
- 活动开始前5分钟,将商品信息、库存加载到Redis。
2. 写优化
- 异步化设计 :
- 关键路径(如库存扣减)同步处理,非关键路径(如订单生成)异步化。
- 批量操作 :
- 日志、监控数据先内存聚合,再批量写入。
3. MySQL优化
-
分库分表 :
- 订单表按用户ID哈希分16个库,每个库分32张表。
-
特殊字段设计 :
sqlCREATE TABLE seckill_orders ( order_id BIGINT PRIMARY KEY, user_id BIGINT, sku_id BIGINT, status TINYINT COMMENT '0-待支付 1-已支付', INDEX idx_user (user_id), INDEX idx_sku (sku_id) ENGINE=InnoDB;
五、容灾与降级方案
1. 服务降级
- 降级策略 :
- 库存不足时直接返回"已售罄",不走后续流程。
- 促销服务不可用时,跳过优惠计算。
- 开关配置 :
- 通过配置中心动态关闭非核心功能(如风控校验)。
2. 熔断机制
- 监控Redis、MySQL、消息队列的响应时间,超阈值时:
- 熔断非核心服务(如用户积分抵扣)。
- 返回友好提示("系统繁忙,请重试")。
3. 数据恢复
- Redis故障 :
- 降级到MySQL库存检查,通过分布式锁保证一致性。
- MySQL故障 :
- 允许超卖,事后通过人工补偿(如退款)。
六、监控与告警
- 核心指标监控 :
- Redis库存剩余量、MySQL订单创建TPS。
- 消息队列积压情况。
- 报警规则 :
- 库存消耗速率异常(如1秒内降为0,可能被刷)。
- 订单创建失败率>0.1%。
- 日志追踪 :
- 全链路TraceID,快速定位问题节点。
七、典型问题解决方案
1. 如何防止超卖?
-
Redis原子操作 :
DECR
+WATCH
或Lua脚本。 -
数据库乐观锁 :
sqlUPDATE sku_stock SET stock = stock - 1 WHERE sku_id = 1001 AND stock >= 1;
2. 如何解决重复下单?
- 唯一索引:用户ID+活动ID建立唯一索引。
- Redis标记 :
SET user_activity:{userId}:{actId} 1 EX 3600 NX
3. 如何处理热点Key?
- Redis分片:将库存KEY按商品ID哈希到不同节点。
- 本地缓存:热点商品库存缓存在服务本地,定期同步。
八、示例代码片段
1. Redis库存扣减(Lua)
lua
-- KEYS[1]: 库存KEY, ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
2. 消息队列消费者(Java)
java
@KafkaListener(topics = "seckill-orders")
public void handleOrder(OrderMessage message) {
// 1. 分布式锁防重处理
String lockKey = "order:" + message.getUserId() + ":" + message.getSkuId();
if (redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
try {
// 2. 创建订单(数据库事务)
orderService.createOrder(message);
} finally {
redisLock.unlock(lockKey);
}
}
}
总结:以上只是理论设计。实际落地时需根据业务特点具体调整,并通过全链路压测验证。