iPhone秒杀场景如何设计?
业务场景
公司年会抽奖活动,100台iPhone秒杀,预计10万人参与,秒杀开始瞬间QPS达10万+。
核心挑战:
- 瞬时流量极高
- 库存有限(100台)
- 绝对不能超卖
- 用户体验要好
秒杀系统架构
┌─────────────────────────────────────────────────────────┐
│ 秒杀架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ 用户 → CDN静态化 → 网关限流 → 秒杀服务 → Redis预扣 │
│ ↓ │
│ MQ → 订单服务 → 数据库 │
│ │
└─────────────────────────────────────────────────────────┘
核心策略
- 流量过滤:层层削减,只放行有效请求
- Redis预扣:库存操作不落库
- 异步下单:MQ削峰
策略一:层层限流
前端限流
// 按钮置灰,禁止重复点击
let clicked = false;
function seckill() {
if (clicked) return;
clicked = true;
setTimeout(() => clicked = false, 3000);
// 发起请求
}
网关限流
# Sentinel网关限流
spring:
cloud:
sentinel:
filter:
url-patterns: /seckill/**
datasource:
flow:
nacos:
rule-type: gw-flow
data-id: seckill-flow-rules
[{
"resource": "/seckill/buy",
"count": 1000,
"intervalSec": 1,
"burst": 0,
"grade": 1
}]
应用层限流
// 用户级别限流:每人每秒最多1次
@SentinelResource(
value = "seckillBuy",
blockHandler = "seckillBlockHandler"
)
public Result buy(Long userId, Long itemId) {
// ...
}
策略二:Redis库存预扣
为什么不直接操作数据库?
数据库扛不住10万QPS的库存扣减,且存在超卖风险。
Redis预扣方案
@Service
public class SeckillService {
private static final String STOCK_KEY = "seckill:stock:";
private static final String BOUGHT_KEY = "seckill:bought:";
public Result seckill(Long userId, Long itemId) {
String stockKey = STOCK_KEY + itemId;
String boughtKey = BOUGHT_KEY + itemId;
// 1. 检查是否已购买(防止重复购买)
Boolean bought = redis.opsForSet().isMember(boughtKey, userId.toString());
if (Boolean.TRUE.equals(bought)) {
return Result.fail("您已参与过秒杀");
}
// 2. Lua脚本原子扣减库存
String script = """
local stock = redis.call('GET', KEYS[1])
if stock == false or tonumber(stock) <= 0 then
return -1
end
local bought = redis.call('SISMEMBER', KEYS[2], ARGV[1])
if bought == 1 then
return -2
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1
""";
Long result = redis.execute(new DefaultRedisScript<>(script, Long.class),
List.of(stockKey, boughtKey), userId.toString());
if (result == -1) {
return Result.fail("商品已售罄");
}
if (result == -2) {
return Result.fail("您已参与过秒杀");
}
// 3. 发送MQ,异步创建订单
kafkaTemplate.send("seckill-order", JSON.toJSONString(
new SeckillOrder(userId, itemId)));
return Result.success("秒杀成功,正在创建订单");
}
}
库存预热
// 秒杀开始前预热库存到Redis
@Scheduled(cron = "0 55 11 * * ?") // 12点秒杀,11:55预热
public void warmUpStock() {
List<SeckillItem> items = seckillItemMapper.findToday();
for (SeckillItem item : items) {
redis.opsForValue().set(
STOCK_KEY + item.getId(),
String.valueOf(item.getStock())
);
}
}
策略三:异步创建订单
@KafkaListener(topics = "seckill-order")
public void createOrder(String message) {
SeckillOrder order = JSON.parseObject(message, SeckillOrder.class);
try {
// 1. 创建订单
Order orderEntity = orderService.create(order);
// 2. 扣减数据库库存(兜底)
int affected = stockMapper.deduct(order.getItemId(), 1);
if (affected == 0) {
// 库存不足,回滚Redis
compensate(order);
return;
}
// 3. 通知用户
messageService.sendSeckillSuccess(order.getUserId(), orderEntity);
} catch (Exception e) {
// 异常回滚
compensate(order);
}
}
private void compensate(SeckillOrder order) {
redis.opsForValue().increment(STOCK_KEY + order.getItemId());
redis.opsForSet().remove(BOUGHT_KEY + order.getItemId(),
order.getUserId().toString());
messageService.sendSeckillFailed(order.getUserId(), "系统异常");
}
策略四:防刷防作弊
隐藏秒杀入口
// 秒杀开始前,接口不暴露
@GetMapping("/seckill/url")
public Result getSeckillUrl(Long itemId) {
SeckillItem item = seckillItemMapper.findById(itemId);
if (LocalDateTime.now().isBefore(item.getStartTime())) {
return Result.fail("秒杀尚未开始");
}
// 返回带Token的秒杀链接
String token = generateToken(itemId);
return Result.success("/seckill/buy?token=" + token);
}
验证码
// 增加验证码,分散请求
@PostMapping("/seckill/buy")
public Result buy(@RequestParam String captcha,
@RequestParam Long itemId,
@RequestParam Long userId) {
if (!captchaService.verify(userId, captcha)) {
return Result.fail("验证码错误");
}
// ...
}
黑名单
// 风控拦截
if (riskService.isBlacklist(userId)) {
return Result.fail("账号异常");
}
策略五:降级兜底
// 熔断降级
@SentinelResource(
value = "seckillBuy",
fallback = "seckillFallback"
)
public Result seckill(Long userId, Long itemId) {
// ...
}
public Result seckillFallback(Long userId, Long itemId, Throwable t) {
return Result.fail("系统繁忙,请稍后重试");
}
完整流程图
用户点击秒杀
│
├── 前端:按钮防重复
│
├── 网关:限流1000 QPS
│
├── 应用:用户级限流
│
├── 风控:黑名单检查
│
├── Redis:Lua脚本原子扣库存
│ ├── 库存不足 → 返回"已售罄"
│ ├── 已购买 → 返回"已参与"
│ └── 成功 → 继续
│
├── MQ:异步下单
│
└── 返回"秒杀成功,创建订单中"
面试答题框架
核心挑战:瞬时10万QPS、100库存、不能超卖
解决方案:
1. 层层限流:前端→网关→应用
2. Redis预扣:Lua脚本原子操作
3. 异步下单:MQ削峰
4. 防刷措施:隐藏入口、验证码、黑名单
技术细节:
- Lua保证原子性
- Set记录已购用户
- 库存预热到Redis
- 失败补偿机制
总结
| 层级 | 策略 | 作用 |
|---|---|---|
| 前端 | 按钮防重 | 减少无效请求 |
| 网关 | 限流 | 保护后端 |
| 应用 | 风控+限流 | 过滤恶意请求 |
| Redis | Lua原子扣减 | 防超卖 |
| MQ | 异步下单 | 削峰 |