秒杀系统设计:高并发下的技术与艺术
前言
秒杀,是互联网领域最具挑战性的场景之一。双11的抢购、春运的火车票、热门商品的限量发售——这些场景的共同特点是:极短时间内,海量用户涌入,争夺有限的资源。
秒杀系统的本质是一个高并发、大流量、资源有限的分布式系统问题。本文将系统性地剖析秒杀系统的设计难点、核心架构、优化策略以及完整的实现方案。
一、秒杀系统的核心挑战
1.1 秒杀场景特征
| 特征 | 说明 | 量化指标 |
|---|---|---|
| 瞬时高并发 | 短时间内请求量暴增 | QPS 从 1000 飙升至 100万+ |
| 热点商品 | 所有请求争抢同一个资源 | 单个 SKU 被百万用户抢购 |
| 资源有限 | 商品库存极少 | 1000 件商品 vs 100万用户 |
| 流量洪峰 | 请求时间高度集中 | 前几秒涌入 80% 的流量 |
| 业务简单 | 核心逻辑相对简单 | 库存扣减 + 订单创建 |
1.2 核心难点
┌─────────────────────────────────────────────────────────────┐
│ 秒杀系统的三大难题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 超卖问题 │ │ 高并发读 │ │ 高并发写 │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ 库存扣减 │ │ 商品详情页 │ │ 订单创建 │ │
│ │ 数据一致性 │ │ 倒计时 │ │ 支付处理 │ │
│ │ 防重下单 │ │ 按钮状态 │ │ 库存扣减 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.3 目标与约束
| 目标 | 约束 |
|---|---|
| 不超卖 | 库存扣减必须准确 |
| 不误杀 | 合法用户能正常参与 |
| 系统不崩 | 超出承载能力的流量要拒绝 |
| 响应快速 | 用户体验友好 |
| 公平性 | 先到先得(或适当随机) |
二、秒杀系统整体架构
2.1 架构全景图
┌─────────────────────────────────────────────────────────────────────────┐
│ 客户端 │
│ App / H5 / 小程序 / PC Web │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────▼───────────────────────────────────────┐
│ CDN (静态资源加速) │
│ HTML / CSS / JS / 图片 │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────▼───────────────────────────────────────┐
│ 负载均衡 (SLB / LVS) │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────▼───────────────────────────────────────┐
│ 接入层 (Nginx / OpenResty) │
│ - 限流(IP/用户) - 黑白名单 - 静态化 │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────▼───────────────────────────────────────┐
│ 秒杀网关 (Seckill Gateway) │
│ - 请求校验 - 流量分发 - 削峰填谷 │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────▼───────────────────────────────────────┐
│ 秒杀服务集群 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 商品服务 │ │ 库存服务 │ │ 订单服务 │ │ 用户服务 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌───────▼───────┐ ┌────────▼────────┐ ┌───────▼───────┐
│ Redis │ │ 消息队列 │ │ MySQL │
│ - 库存缓存 │ │ - 削峰填谷 │ │ - 订单持久化 │
│ - 用户标记 │ │ - 异步下单 │ │ - 库存记录 │
│ - 限流计数 │ │ - 失败重试 │ │ - 用户数据 │
└───────────────┘ └─────────────────┘ └───────────────┘
2.2 流量漏斗模型
秒杀系统的核心思想是层层过滤,让最终打到数据库的请求量控制在系统可承受范围内:
100万 请求
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第1层: 前端/客户端限流 │
│ - 按钮置灰、防抖节流 │
│ 剩余: 100万 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第2层: CDN/接入层限流 │
│ - IP限流、用户限流、静态页面缓存 │
│ 剩余: 50万 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第3层: Nginx 限流 │
│ - 连接数限制、请求速率限制 │
│ 剩余: 10万 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第4层: 网关限流(令牌桶/漏桶) │
│ - 集群限流、用户维度限流 │
│ 剩余: 1万 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第5层: 业务逻辑校验 │
│ - 活动时间校验、重复下单校验、库存预检 │
│ 剩余: 5000 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第6层: 库存扣减(Redis原子操作) │
│ - LUA脚本保证原子性 │
│ 成功: 1000(库存数) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 第7层: 异步下单(MQ) │
│ - 消息队列削峰 │
│ 最终入库: 1000 │
└──────────────────────────────────────────────────────────┘
三、核心问题解决方案
3.1 库存扣减:不超卖的核心
方案选择:Redis 原子操作 + LUA 脚本
-- 秒杀库存扣减 LUA 脚本
-- KEYS[1]: 库存key
-- KEYS[2]: 用户限购key
-- ARGV[1]: 商品ID
-- ARGV[2]: 用户ID
-- ARGV[3]: 扣减数量
-- ARGV[4]: 限购数量
local stock_key = KEYS[1]
local user_limit_key = KEYS[2]
local product_id = ARGV[1]
local user_id = ARGV[2]
local quantity = tonumber(ARGV[3])
local limit_qty = tonumber(ARGV[4])
-- 1. 检查用户是否已抢购(防重复下单)
local user_bought = redis.call('GET', user_limit_key)
if user_bought and tonumber(user_bought) >= limit_qty then
return {0, "您已达到购买上限"}
end
-- 2. 检查库存
local stock = redis.call('GET', stock_key)
if not stock or tonumber(stock) < quantity then
return {0, "库存不足"}
end
-- 3. 扣减库存(原子操作)
local new_stock = redis.call('DECRBY', stock_key, quantity)
-- 4. 记录用户购买记录
redis.call('INCRBY', user_limit_key, quantity)
redis.call('EXPIRE', user_limit_key, 3600) -- 1小时过期
-- 5. 返回成功
return {1, new_stock}
Java 调用示例:
@Component
public class SeckillStockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private DefaultRedisScript<Long> stockLuaScript;
@PostConstruct
public void init() {
stockLuaScript = new DefaultRedisScript<>();
stockLuaScript.setScriptText(SECKILL_LUA_SCRIPT);
stockLuaScript.setResultType(Long.class);
}
public SeckillResult deductStock(String productId, String userId, int quantity, int limitQty) {
String stockKey = "seckill:stock:" + productId;
String userKey = "seckill:user:" + productId + ":" + userId;
List<String> keys = Arrays.asList(stockKey, userKey);
// 执行LUA脚本
Long result = redisTemplate.execute(
stockLuaScript,
keys,
productId, userId, String.valueOf(quantity), String.valueOf(limitQty)
);
if (result == 1L) {
// 扣减成功,发送MQ消息异步创建订单
sendOrderCreateMessage(productId, userId, quantity);
return SeckillResult.success();
} else {
return SeckillResult.fail("抢购失败,库存不足或已达上限");
}
}
}
3.2 流量削峰:消息队列
@Component
public class SeckillOrderService {
@Autowired
private RocketMQTemplate mqTemplate;
@Autowired
private OrderService orderService;
// 接收抢购请求,异步处理
public void handleSeckill(SeckillRequest request) {
// 1. 先进行库存扣减(Redis)
SeckillResult result = stockService.deductStock(
request.getProductId(),
request.getUserId(),
1,
1
);
if (!result.isSuccess()) {
return;
}
// 2. 扣减成功后,发送MQ消息
SeckillOrderMessage message = SeckillOrderMessage.builder()
.productId(request.getProductId())
.userId(request.getUserId())
.seckillTime(System.currentTimeMillis())
.build();
mqTemplate.sendAsync("seckill_order_topic", message);
// 3. 立即返回"排队中",告知用户稍后查询结果
}
// MQ消费者:异步创建订单
@RocketMQMessageListener(topic = "seckill_order_topic", consumerGroup = "order_consumer")
public class OrderConsumer implements RocketMQListener<SeckillOrderMessage> {
@Override
public void onMessage(SeckillOrderMessage message) {
try {
// 创建订单(写入数据库)
Order order = orderService.createSeckillOrder(
message.getProductId(),
message.getUserId()
);
// 发送通知给用户(WebSocket/推送)
notifyService.notifyUser(message.getUserId(), order);
} catch (Exception e) {
log.error("创建订单失败", e);
// 失败补偿:回滚Redis库存
stockService.rollbackStock(message.getProductId(), message.getUserId());
}
}
}
}
3.3 限流策略:多层防护
3.3.1 Nginx 限流
# nginx.conf
# 限制单个IP的请求速率(10r/s)
limit_req_zone $binary_remote_addr zone=seckill_ip:10m rate=10r/s;
# 限制单个IP的连接数(20个)
limit_conn_zone $binary_remote_addr zone=conn_ip:10m;
server {
listen 80;
server_name seckill.example.com;
location /seckill {
# 应用限流
limit_req zone=seckill_ip burst=20 nodelay;
limit_conn conn_ip 20;
# 只允许内网访问秒杀接口(通过网关转发)
allow 10.0.0.0/8;
deny all;
proxy_pass http://seckill_gateway;
}
}
3.3.2 网关层限流(Redis + 令牌桶)
@Component
public class TokenBucketLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 令牌桶限流
* @param key 限流key(如用户ID、IP)
* @param capacity 桶容量
* @param rate 令牌生成速率(个/秒)
*/
public boolean tryAcquire(String key, int capacity, int rate) {
String tokenKey = "rate_limiter:" + key;
String lastRefillKey = "rate_limiter_last:" + key;
long now = System.currentTimeMillis();
// 获取当前令牌数
String tokenStr = redisTemplate.opsForValue().get(tokenKey);
long tokens = tokenStr == null ? capacity : Long.parseLong(tokenStr);
// 获取上次填充时间
String lastStr = redisTemplate.opsForValue().get(lastRefillKey);
long lastRefill = lastStr == null ? now : Long.parseLong(lastStr);
// 计算需要补充的令牌数
long interval = now - lastRefill;
long newTokens = Math.min(capacity, tokens + interval * rate / 1000);
if (newTokens < 1) {
return false;
}
// 消耗一个令牌
newTokens--;
// 更新Redis
redisTemplate.opsForValue().set(tokenKey, String.valueOf(newTokens));
redisTemplate.opsForValue().set(lastRefillKey, String.valueOf(now));
return true;
}
}
3.3.3 用户级限流
@Component
public class UserRateLimiter {
// 每秒最多请求数
private static final int MAX_REQUESTS_PER_SECOND = 5;
// 每分钟最多请求数
private static final int MAX_REQUESTS_PER_MINUTE = 50;
public boolean checkUserRateLimit(String userId) {
String secondKey = "rate:user:second:" + userId;
String minuteKey = "rate:user:minute:" + userId;
// 秒级限流
Long secondCount = redisTemplate.opsForValue().increment(secondKey);
if (secondCount == 1) {
redisTemplate.expire(secondKey, 1, TimeUnit.SECONDS);
}
if (secondCount > MAX_REQUESTS_PER_SECOND) {
log.warn("用户 {} 触发秒级限流", userId);
return false;
}
// 分钟级限流
Long minuteCount = redisTemplate.opsForValue().increment(minuteKey);
if (minuteCount == 1) {
redisTemplate.expire(minuteKey, 60, TimeUnit.SECONDS);
}
if (minuteCount > MAX_REQUESTS_PER_MINUTE) {
log.warn("用户 {} 触发分钟级限流", userId);
return false;
}
return true;
}
}
3.4 页面静态化与动静分离
┌─────────────────────────────────────────────────────────────┐
│ 动静分离架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 静态资源(CDN) 动态数据(API) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ HTML/CSS/JS │ │ 库存数量 │ │
│ │ 商品图片 │ │ 倒计时 │ │
│ │ 页面框架 │ │ 秒杀按钮状态 │ │
│ │ 样式文件 │ │ 用户资格 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ 直接返回给浏览器 异步请求刷新 │
│ │
└─────────────────────────────────────────────────────────────┘
实现方式:
<!-- 秒杀页面:静态HTML框架 -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="//cdn.example.com/seckill/main.css">
<script src="//cdn.example.com/seckill/main.js"></script>
</head>
<body>
<div class="product-info">
<img src="//cdn.example.com/images/product_123.jpg">
<h1>秒杀商品:XXX手机</h1>
<div class="price">秒杀价: ¥999</div>
</div>
<div class="seckill-info">
<div id="countdown">距离开始: 00:00:00</div>
<button id="seckill-btn" disabled>即将开始</button>
</div>
<script>
// 轮询获取动态数据
setInterval(function() {
$.get('/api/seckill/status?productId=123', function(data) {
updateCountdown(data.timeLeft);
updateButtonStatus(data.status);
});
}, 1000);
</script>
</body>
</html>
四、完整业务流程实现
4.1 秒杀活动流程图
┌─────────────────────────────────────────────────────────────────────┐
│ 秒杀完整流程 │
└─────────────────────────────────────────────────────────────────────┘
用户端 服务端
│ │
│ 1. 进入秒杀页面 │
├──────────────────────────────────▶│
│ │
│ 2. 获取活动状态 │
│◀──────────────────────────────────┤ ← 返回倒计时、商品信息
│ │
│ 3. 等待倒计时结束 │
│ │
│ 4. 点击秒杀按钮 │
├──────────────────────────────────▶│
│ │
│ │ 5. 请求校验
│ │ - 活动是否进行中
│ │ - 用户是否登录
│ │ - 用户IP是否正常
│ │
│ │ 6. 限流检查
│ │ - 令牌桶限流
│ │ - 用户级限流
│ │
│ │ 7. 库存预检
│ │ - Redis检查库存
│ │
│ │ 8. 库存扣减
│ │ - LUA原子操作
│ │
│ │ 9. 发送MQ消息
│ │
│ 10. 返回"排队中" │
│◀──────────────────────────────────┤
│ │
│ 11. 轮询查询结果 │
├──────────────────────────────────▶│
│ │
│ │ 12. 查询订单状态
│ │ - 从Redis/DB获取
│ │
│ 13. 返回结果 │
│ - 成功:跳转支付 │
│ - 失败:提示信息 │
│◀──────────────────────────────────┤
│ │
│ 后台异步处理 │
│ │ 14. MQ消费
│ │ - 创建订单
│ │ - 扣减真实库存
│ │ - 发送通知
│ │
4.2 核心代码实现
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private UserRateLimiter rateLimiter;
@Autowired
private SeckillStockService stockService;
@Autowired
private RocketMQTemplate mqTemplate;
/**
* 秒杀接口
*/
@PostMapping("/do")
public Result doSeckill(@RequestBody SeckillRequest request) {
String userId = request.getUserId();
String productId = request.getProductId();
// 1. 基础校验
// 1.1 活动时间校验
if (!seckillService.isInSeckillPeriod(productId)) {
return Result.error("秒杀活动未开始或已结束");
}
// 1.2 用户登录校验(从Token获取)
if (!isLogin(userId)) {
return Result.error("请先登录");
}
// 1.3 重复提交校验(防重令牌)
String token = request.getToken();
if (!validateToken(userId, token)) {
return Result.error("请勿重复提交");
}
// 2. 限流检查
if (!rateLimiter.checkUserRateLimit(userId)) {
return Result.error("请求过于频繁,请稍后再试");
}
// 3. 库存预检(快速失败)
Long stock = stockService.getStock(productId);
if (stock == null || stock <= 0) {
return Result.error("商品已抢光");
}
// 4. 核心:库存扣减(Redis LUA)
SeckillResult result = stockService.deductStock(productId, userId, 1, 1);
if (!result.isSuccess()) {
return Result.error(result.getMessage());
}
// 5. 发送MQ消息,异步创建订单
SeckillOrderMessage message = SeckillOrderMessage.builder()
.productId(productId)
.userId(userId)
.seckillTime(System.currentTimeMillis())
.build();
String orderId = generateOrderId();
message.setOrderId(orderId);
// 将订单状态存入Redis(用于轮询)
redisTemplate.opsForValue().set(
"seckill:order_status:" + orderId,
"pending",
10, TimeUnit.MINUTES
);
mqTemplate.sendAsync("seckill_order_topic", message);
// 6. 返回排队中,给用户orderId用于轮询
return Result.success(Map.of(
"orderId", orderId,
"status", "pending",
"message", "正在排队处理中,请稍后查询结果"
));
}
/**
* 轮询查询秒杀结果
*/
@GetMapping("/result")
public Result queryResult(@RequestParam String orderId) {
// 1. 先查Redis中的状态
String status = redisTemplate.opsForValue().get("seckill:order_status:" + orderId);
if ("pending".equals(status)) {
return Result.success(Map.of("status", "pending"));
}
if ("success".equals(status)) {
// 获取订单详情
Order order = orderService.getOrder(orderId);
return Result.success(Map.of("status", "success", "order", order));
}
if ("failed".equals(status)) {
return Result.success(Map.of("status", "failed"));
}
// 2. Redis中没有,查数据库
Order order = orderService.getOrder(orderId);
if (order != null) {
return Result.success(Map.of("status", "success", "order", order));
}
return Result.success(Map.of("status", "pending"));
}
}
4.3 订单异步处理器
@Component
public class SeckillOrderConsumer {
@Autowired
private OrderService orderService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private NotifyService notifyService;
@RocketMQMessageListener(
topic = "seckill_order_topic",
consumerGroup = "seckill_order_consumer",
consumeThreadMax = 64,
maxReconsumeTimes = 3
)
public void onMessage(SeckillOrderMessage message) {
String orderId = message.getOrderId();
String userId = message.getUserId();
String productId = message.getProductId();
try {
// 1. 幂等性检查(防止重复消费)
Boolean consumed = redisTemplate.opsForValue()
.setIfAbsent("seckill:consumed:" + orderId, "1", 1, TimeUnit.DAYS);
if (Boolean.FALSE.equals(consumed)) {
log.warn("订单已处理过,跳过: {}", orderId);
return;
}
// 2. 创建订单(写入数据库)
Order order = orderService.createSeckillOrder(
orderId, productId, userId
);
// 3. 更新订单状态
redisTemplate.opsForValue().set(
"seckill:order_status:" + orderId,
"success",
10, TimeUnit.MINUTES
);
// 4. 发送通知(WebSocket/消息推送)
notifyService.notifyUser(userId, "恭喜您抢购成功!订单号:" + orderId);
// 5. 更新统计数据
metricsCollector.recordSuccess();
} catch (Exception e) {
log.error("创建订单失败: orderId={}", orderId, e);
// 失败:更新状态
redisTemplate.opsForValue().set(
"seckill:order_status:" + orderId,
"failed",
10, TimeUnit.MINUTES
);
// 回滚库存
rollbackStock(productId, userId);
// 发送失败通知
notifyService.notifyUser(userId, "抢购失败,库存不足");
metricsCollector.recordFailure();
// 抛出异常触发MQ重试
throw new RuntimeException("订单创建失败", e);
}
}
private void rollbackStock(String productId, String userId) {
String stockKey = "seckill:stock:" + productId;
String userKey = "seckill:user:" + productId + ":" + userId;
redisTemplate.opsForValue().increment(stockKey, 1);
redisTemplate.delete(userKey);
}
}
五、进阶优化策略
5.1 缓存预热
@Component
public class SeckillCacheWarmer {
@EventListener(ApplicationReadyEvent.class)
public void warmUp() {
// 获取即将开始的秒杀活动
List<SeckillActivity> activities = activityService.getUpcomingActivities();
for (SeckillActivity activity : activities) {
// 预热商品信息
warmProductCache(activity.getProductId());
// 预热库存到Redis
String stockKey = "seckill:stock:" + activity.getProductId();
redisTemplate.opsForValue().set(
stockKey,
String.valueOf(activity.getStock()),
24, TimeUnit.HOURS
);
// 预热活动信息
String activityKey = "seckill:activity:" + activity.getProductId();
redisTemplate.opsForHash().putAll(activityKey, Map.of(
"startTime", String.valueOf(activity.getStartTime()),
"endTime", String.valueOf(activity.getEndTime()),
"price", String.valueOf(activity.getPrice()),
"status", "1"
));
}
}
private void warmProductCache(String productId) {
// 将商品详情从DB加载到Redis
Product product = productService.getProduct(productId);
String productKey = "seckill:product:" + productId;
redisTemplate.opsForHash().putAll(productKey, Map.of(
"name", product.getName(),
"image", product.getImage(),
"seckillPrice", String.valueOf(product.getSeckillPrice())
));
}
}
5.2 热点检测与动态隔离
@Component
public class HotspotDetector {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 每秒统计窗口
private final LoadingCache<String, AtomicLong> qpsCounter = Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(key -> new AtomicLong(0));
// 热点阈值
private static final int HOTSPOT_THRESHOLD = 10000;
public void recordRequest(String productId) {
AtomicLong counter = qpsCounter.get(productId);
long qps = counter.incrementAndGet();
if (qps > HOTSPOT_THRESHOLD) {
// 触发热点保护
String hotspotKey = "seckill:hotspot:" + productId;
Boolean isHot = redisTemplate.opsForValue().setIfAbsent(hotspotKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(isHot)) {
log.warn("商品 {} 成为热点,触发保护机制", productId);
// 动态扩容该商品的处理节点
scaleUpForHotspot(productId);
}
}
}
private void scaleUpForHotspot(String productId) {
// 方案1: 增加该商品的缓存副本
String stockKey = "seckill:stock:" + productId;
for (int i = 1; i <= 5; i++) {
String replicaKey = stockKey + ":replica:" + i;
redisTemplate.opsForValue().set(replicaKey,
redisTemplate.opsForValue().get(stockKey));
}
// 方案2: 通知K8s增加Pod副本
k8sClient.scaleDeployment("seckill-service", 10);
}
}
5.3 降级与熔断
@Component
public class SeckillCircuitBreaker {
@Autowired
private HealthIndicator healthIndicator;
// 降级开关
private volatile boolean degradeEnabled = false;
// 熔断器状态
private enum CircuitState { CLOSED, OPEN, HALF_OPEN }
private volatile CircuitState state = CircuitState.CLOSED;
private AtomicInteger failureCount = new AtomicInteger(0);
private AtomicInteger successCount = new AtomicInteger(0);
private static final int FAILURE_THRESHOLD = 50;
private static final int SUCCESS_THRESHOLD = 10;
private static final long OPEN_TIMEOUT = 30000; // 30秒
private volatile long openTime = 0;
@Scheduled(fixedDelay = 1000)
public void checkHealth() {
switch (state) {
case CLOSED:
if (failureCount.get() > FAILURE_THRESHOLD) {
state = CircuitState.OPEN;
openTime = System.currentTimeMillis();
log.warn("熔断器开启,秒杀服务降级");
degradeEnabled = true;
}
break;
case OPEN:
if (System.currentTimeMillis() - openTime > OPEN_TIMEOUT) {
state = CircuitState.HALF_OPEN;
successCount.set(0);
log.info("熔断器半开,尝试恢复");
}
break;
case HALF_OPEN:
if (successCount.get() > SUCCESS_THRESHOLD) {
state = CircuitState.CLOSED;
failureCount.set(0);
degradeEnabled = false;
log.info("熔断器关闭,服务恢复");
}
break;
}
}
public void recordSuccess() {
if (state == CircuitState.HALF_OPEN) {
successCount.incrementAndGet();
} else if (state == CircuitState.CLOSED) {
failureCount.set(0);
}
}
public void recordFailure() {
if (state == CircuitState.CLOSED) {
failureCount.incrementAndGet();
} else if (state == CircuitState.HALF_OPEN) {
state = CircuitState.OPEN;
openTime = System.currentTimeMillis();
}
}
public boolean isDegradeEnabled() {
return degradeEnabled;
}
}
5.4 数据库优化
-- 秒杀订单表(分库分表)
CREATE TABLE `seckill_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_id` varchar(32) NOT NULL COMMENT '订单号',
`product_id` varchar(32) NOT NULL COMMENT '商品ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`price` decimal(10,2) NOT NULL COMMENT '秒杀价',
`status` tinyint DEFAULT '0' COMMENT '0-待支付 1-已支付 2-已取消',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`pay_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';
-- 建议:
-- 1. 按 user_id 分片,方便查询用户订单
-- 2. 订单号使用雪花算法,保证全局唯一
-- 3. 历史订单定期归档到冷存储
六、监控与运维
6.1 核心监控指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 秒杀QPS | 当前请求量 | > 预期值200% |
| 库存扣减成功率 | 成功扣减/总请求 | < 50% |
| 订单创建延迟 | P99延迟 | > 3秒 |
| Redis命中率 | 缓存命中率 | < 90% |
| MQ积压数 | 未消费消息数 | > 10000 |
| 限流触发次数 | 被限流请求数 | > 总请求50% |
6.2 压测方案
压测工具: JMeter / wrk / Locust
压测场景:
- 正常流量: 模拟真实用户行为
- 突发流量: 瞬间大量请求
- 超卖测试: 并发请求超过库存数
- 异常测试: 网络延迟、服务超时
压测指标:
- 吞吐量: 最大 QPS
- 响应时间: P50/P99/P999
- 错误率: 4xx/5xx 比例
- 资源占用: CPU/内存/网络
压测数据准备:
- 100万用户账号
- 10个秒杀商品
- 库存设置: 1000/商品
七、总结
7.1 设计要点回顾
| 问题 | 解决方案 | 关键点 |
|---|---|---|
| 超卖 | Redis LUA原子扣减 | 保证原子性 |
| 高 |