副标题:让1亿用户抢100件商品不崩溃的秘密 🎯
🎬 开场:秒杀的魔力与挑战
真实案例
小米手机秒杀:
活动时间:2023年12月12日 10:00:00
商品库存:1000台
参与人数:500万
实际TPS:200万+
技术挑战:
├── 瞬时流量:平时100倍
├── 库存有限:超卖风险
├── 恶意刷单:黄牛党
└── 用户体验:响应超时
结果:
✅ 系统稳定
✅ 0.3秒售罄
✅ 无超卖
✅ 无宕机
秒杀的核心挑战
挑战1:瞬时高并发 🔥
平时:1000 QPS
秒杀:1000000 QPS
增长:1000倍!
挑战2:库存有限 📦
商品:100件
请求:1000000次
比例:1:10000
挑战3:读多写少 📊
读操作:查询商品详情、库存
写操作:扣减库存、创建订单
比例:10000:1
挑战4:恶意攻击 🤖
黄牛党:
├── 脚本刷单
├── 分布式攻击
└── 恶意占用资源
🏗️ 系统架构设计
整体架构图
┌──────────────────────────────────────────┐
│ CDN + 静态资源 │
└────────────┬─────────────────────────────┘
│
┌────────────▼─────────────────────────────┐
│ Nginx(限流 + 负载均衡) │
└────────────┬─────────────────────────────┘
│
┌────────┴────────┐
│ │
┌───▼────┐ ┌────▼───┐
│ 网关层 │ │ 网关层 │
│ (限流) │ │ (限流) │
└───┬────┘ └────┬───┘
│ │
└────────┬────────┘
│
┌────────────▼─────────────────────────────┐
│ 秒杀服务集群(无状态) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │实例1│ │实例2│ │实例3│ │实例4│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │
└─────┼──────┼──────┼──────┼─────────────┘
│ │ │ │
└──────┴──────┴──────┘
│
┌────────┴────────┐
│ │
┌───▼────┐ ┌────▼────┐
│ Redis │ │ MySQL │
│ 集群 │ │ 集群 │
└────────┘ └─────────┘
│ │
└────────┬────────┘
│
┌────────▼────────┐
│ 消息队列MQ │
│ (异步处理) │
└─────────────────┘
📝 详细设计方案
阶段1:前端优化
1. 静态资源CDN
<!-- 秒杀页面 -->
<!DOCTYPE html>
<html>
<head>
<title>小米手机秒杀</title>
<!-- CSS和JS使用CDN -->
<link rel="stylesheet"
href="https://cdn.example.com/seckill.css?v=20231212">
<script src="https://cdn.example.com/seckill.js?v=20231212"></script>
</head>
<body>
<!-- 商品图片使用CDN -->
<img src="https://cdn.example.com/product/xiaomi14.jpg"
alt="小米14">
<!-- 商品详情(静态数据) -->
<div class="product-info">
<h1>小米14 手机</h1>
<p class="price">¥3999</p>
<p class="desc">骁龙8 Gen3 | 徕卡影像 | 120W快充</p>
</div>
<!-- 秒杀按钮(只有这个是动态的) -->
<div class="seckill-action">
<button id="seckillBtn" onclick="doSeckill()">
立即抢购
</button>
<span id="countdown">距离开始:00:05:30</span>
</div>
</body>
</html>
2. 按钮控制
/**
* 前端防重复提交
*/
class SeckillController {
constructor() {
this.clicked = false;
this.token = null;
}
/**
* 秒杀按钮点击
*/
async doSeckill() {
// 1. 防止重复点击
if (this.clicked) {
alert('请勿重复提交!');
return;
}
// 2. 标记已点击
this.clicked = true;
const btn = document.getElementById('seckillBtn');
btn.disabled = true;
btn.innerText = '抢购中...';
// 3. 获取秒杀令牌
if (!this.token) {
this.token = await this.getSeckillToken();
if (!this.token) {
this.reset();
alert('获取令牌失败,请重试');
return;
}
}
// 4. 发起秒杀请求
try {
const result = await this.requestSeckill(this.token);
if (result.success) {
alert('抢购成功!请尽快支付');
window.location.href = '/order/pay?orderId=' + result.orderId;
} else {
alert(result.message || '抢购失败');
this.reset();
}
} catch (error) {
console.error('秒杀失败', error);
alert('网络异常,请重试');
this.reset();
}
}
/**
* 获取秒杀令牌(限流)
*/
async getSeckillToken() {
try {
const response = await fetch('/api/seckill/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
activityId: 12345,
userId: this.getUserId()
})
});
const data = await response.json();
return data.token;
} catch (error) {
console.error('获取令牌失败', error);
return null;
}
}
/**
* 发起秒杀请求
*/
async requestSeckill(token) {
const response = await fetch('/api/seckill/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Seckill-Token': token
},
body: JSON.stringify({
activityId: 12345,
userId: this.getUserId()
})
});
return await response.json();
}
/**
* 重置状态
*/
reset() {
this.clicked = false;
const btn = document.getElementById('seckillBtn');
btn.disabled = false;
btn.innerText = '立即抢购';
}
getUserId() {
return localStorage.getItem('userId');
}
}
3. 倒计时和预加载
/**
* 倒计时控制
*/
class CountdownTimer {
constructor(startTime) {
this.startTime = new Date(startTime).getTime();
this.timer = null;
}
/**
* 开始倒计时
*/
start() {
this.timer = setInterval(() => {
const now = Date.now();
const diff = this.startTime - now;
if (diff <= 0) {
// 秒杀开始
this.onStart();
clearInterval(this.timer);
} else {
// 更新倒计时显示
this.updateDisplay(diff);
// 提前1秒预加载
if (diff <= 1000 && !this.preloaded) {
this.preload();
this.preloaded = true;
}
}
}, 100);
}
/**
* 更新倒计时显示
*/
updateDisplay(diff) {
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
document.getElementById('countdown').innerText =
`距离开始:${this.pad(hours)}:${this.pad(minutes)}:${this.pad(seconds)}`;
}
/**
* 预加载(提前建立连接)
*/
async preload() {
console.log('预加载秒杀接口...');
// 预热接口(HEAD请求)
fetch('/api/seckill/warmup', { method: 'HEAD' });
// 预获取令牌
const controller = new SeckillController();
await controller.getSeckillToken();
}
/**
* 秒杀开始
*/
onStart() {
document.getElementById('countdown').innerText = '秒杀进行中';
document.getElementById('seckillBtn').disabled = false;
document.getElementById('seckillBtn').classList.add('active');
}
pad(num) {
return num < 10 ? '0' + num : num;
}
}
// 初始化
const timer = new CountdownTimer('2023-12-12 10:00:00');
timer.start();
阶段2:网关层限流
1. Nginx限流
# nginx.conf
http {
# 限流配置
# 每个IP每秒最多10个请求
limit_req_zone $binary_remote_addr zone=seckill_limit:10m rate=10r/s;
# 每个IP最多保持50个连接
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
listen 80;
server_name seckill.example.com;
location /api/seckill/ {
# 应用限流规则
limit_req zone=seckill_limit burst=20 nodelay;
limit_conn conn_limit 50;
# 返回友好的限流提示
limit_req_status 429;
limit_conn_status 429;
# 代理到后端
proxy_pass http://seckill_backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 超时设置
proxy_connect_timeout 3s;
proxy_send_timeout 3s;
proxy_read_timeout 3s;
}
}
# 后端服务器集群
upstream seckill_backend {
server 192.168.1.101:8080 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.1.102:8080 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.1.103:8080 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.1.104:8080 weight=1 max_fails=2 fail_timeout=30s;
keepalive 1000;
}
}
2. 网关层令牌桶限流
/**
* API网关限流
*/
@Component
public class SeckillGatewayFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 总限流:每秒100万请求
private static final long TOTAL_PERMITS_PER_SECOND = 1000000;
// 单用户限流:每秒10个请求
private static final long USER_PERMITS_PER_SECOND = 10;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 只对秒杀接口限流
if (!path.startsWith("/api/seckill/")) {
return chain.filter(exchange);
}
// 1. 全局限流
if (!tryAcquireGlobal()) {
return responseLimit(exchange, "系统繁忙,请稍后重试");
}
// 2. 用户级限流
String userId = getUserId(exchange);
if (userId != null && !tryAcquireUser(userId)) {
return responseLimit(exchange, "您的操作太频繁,请稍后重试");
}
return chain.filter(exchange);
}
/**
* 全局限流(令牌桶)
*/
private boolean tryAcquireGlobal() {
String key = "seckill:limit:global";
// 使用Redis + Lua实现令牌桶
String luaScript =
"local key = KEYS[1]\n" +
"local capacity = tonumber(ARGV[1])\n" +
"local rate = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"\n" +
"local tokens = redis.call('hget', key, 'tokens')\n" +
"local last_time = redis.call('hget', key, 'last_time')\n" +
"\n" +
"if tokens == false then\n" +
" tokens = capacity\n" +
" last_time = now\n" +
"else\n" +
" tokens = tonumber(tokens)\n" +
" last_time = tonumber(last_time)\n" +
"end\n" +
"\n" +
"local elapsed = math.max(0, now - last_time)\n" +
"local new_tokens = math.min(capacity, tokens + elapsed * rate)\n" +
"\n" +
"if new_tokens >= 1 then\n" +
" new_tokens = new_tokens - 1\n" +
" redis.call('hset', key, 'tokens', new_tokens)\n" +
" redis.call('hset', key, 'last_time', now)\n" +
" redis.call('expire', key, 60)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(TOTAL_PERMITS_PER_SECOND), // capacity
String.valueOf(TOTAL_PERMITS_PER_SECOND), // rate (每秒补充)
String.valueOf(System.currentTimeMillis() / 1000) // now
);
return result != null && result == 1;
}
/**
* 用户级限流
*/
private boolean tryAcquireUser(String userId) {
String key = "seckill:limit:user:" + userId;
// 使用滑动窗口限流
long now = System.currentTimeMillis();
long windowStart = now - 1000; // 1秒窗口
// 移除过期的记录
redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
// 统计当前窗口内的请求数
Long count = redisTemplate.opsForZSet().zCard(key);
if (count != null && count >= USER_PERMITS_PER_SECOND) {
return false;
}
// 添加当前请求
redisTemplate.opsForZSet().add(key, String.valueOf(now), now);
redisTemplate.expire(key, 2, TimeUnit.SECONDS);
return true;
}
/**
* 返回限流响应
*/
private Mono<Void> responseLimit(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"code\":429,\"message\":\"" + message + "\"}";
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return exchange.getResponse().writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 优先级最高
}
}
阶段3:库存预热
/**
* 库存预热到Redis
*/
@Service
public class StockWarmUpService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SeckillActivityMapper activityMapper;
/**
* 秒杀开始前预热库存
*/
public void warmUpStock(Long activityId) {
// 1. 从数据库加载活动信息
SeckillActivity activity = activityMapper.selectById(activityId);
if (activity == null) {
throw new RuntimeException("活动不存在");
}
// 2. 预热库存到Redis
String stockKey = "seckill:stock:" + activityId;
redisTemplate.opsForValue().set(stockKey, activity.getStock());
// 3. 设置过期时间(活动结束后1小时)
long ttl = activity.getEndTime().getTime() - System.currentTimeMillis() + 3600000;
redisTemplate.expire(stockKey, ttl, TimeUnit.MILLISECONDS);
log.info("库存预热完成: activityId={}, stock={}", activityId, activity.getStock());
// 4. 预热商品信息
String productKey = "seckill:product:" + activityId;
redisTemplate.opsForValue().set(productKey, activity.getProduct());
redisTemplate.expire(productKey, ttl, TimeUnit.MILLISECONDS);
log.info("商品信息预热完成: activityId={}", activityId);
}
/**
* 批量预热多个活动
*/
@Scheduled(cron = "0 0 9 * * ?") // 每天9点执行
public void batchWarmUp() {
// 查询今天的秒杀活动
List<SeckillActivity> activities = activityMapper.selectTodayActivities();
log.info("开始预热今日秒杀活动,共{}个", activities.size());
for (SeckillActivity activity : activities) {
try {
warmUpStock(activity.getId());
} catch (Exception e) {
log.error("预热失败: activityId={}", activity.getId(), e);
}
}
log.info("预热完成");
}
}
阶段4:核心秒杀逻辑
1. 秒杀令牌
/**
* 秒杀令牌服务
*/
@Service
public class SeckillTokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 生成秒杀令牌(限流)
*/
public String generateToken(Long activityId, Long userId) {
// 1. 检查用户是否已经有令牌
String userTokenKey = String.format("seckill:token:user:%d:%d",
activityId, userId);
String existingToken = redisTemplate.opsForValue().get(userTokenKey);
if (existingToken != null) {
return existingToken;
}
// 2. 检查令牌总数是否超限
String tokenCountKey = "seckill:token:count:" + activityId;
Long count = redisTemplate.opsForValue().increment(tokenCountKey);
// 令牌数量 = 库存 * 5 (超卖保护)
long maxTokens = getStockCount(activityId) * 5;
if (count > maxTokens) {
throw new SeckillException("活动太火爆,请稍后再试");
}
// 3. 生成令牌
String token = UUID.randomUUID().toString().replace("-", "");
// 4. 存储令牌
String tokenKey = "seckill:token:" + token;
Map<String, Object> tokenData = new HashMap<>();
tokenData.put("activityId", activityId);
tokenData.put("userId", userId);
tokenData.put("createTime", System.currentTimeMillis());
redisTemplate.opsForHash().putAll(tokenKey, tokenData);
redisTemplate.expire(tokenKey, 5, TimeUnit.MINUTES); // 5分钟有效
// 5. 关联用户
redisTemplate.opsForValue().set(userTokenKey, token, 5, TimeUnit.MINUTES);
log.info("生成秒杀令牌: token={}, userId={}", token, userId);
return token;
}
/**
* 验证令牌
*/
public boolean validateToken(String token, Long activityId, Long userId) {
String tokenKey = "seckill:token:" + token;
Map<Object, Object> tokenData = redisTemplate.opsForHash().entries(tokenKey);
if (tokenData.isEmpty()) {
return false;
}
Long tokenActivityId = Long.valueOf(tokenData.get("activityId").toString());
Long tokenUserId = Long.valueOf(tokenData.get("userId").toString());
return tokenActivityId.equals(activityId) && tokenUserId.equals(userId);
}
/**
* 消费令牌(一次性)
*/
public void consumeToken(String token) {
String tokenKey = "seckill:token:" + token;
redisTemplate.delete(tokenKey);
}
private Long getStockCount(Long activityId) {
String stockKey = "seckill:stock:" + activityId;
Object stock = redisTemplate.opsForValue().get(stockKey);
return stock != null ? Long.valueOf(stock.toString()) : 0L;
}
}
2. 库存扣减(Lua脚本)
/**
* 库存扣减服务
*/
@Service
public class StockDeductionService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 扣减库存(原子操作)
*/
public boolean deductStock(Long activityId, Long userId) {
String stockKey = "seckill:stock:" + activityId;
String userKey = "seckill:user:" + activityId + ":" + userId;
// Lua脚本保证原子性
String luaScript =
"-- 检查用户是否已经购买\n" +
"if redis.call('exists', KEYS[2]) == 1 then\n" +
" return -2 -- 已经购买过\n" +
"end\n" +
"\n" +
"-- 获取当前库存\n" +
"local stock = redis.call('get', KEYS[1])\n" +
"if not stock or tonumber(stock) <= 0 then\n" +
" return -1 -- 库存不足\n" +
"end\n" +
"\n" +
"-- 扣减库存\n" +
"redis.call('decr', KEYS[1])\n" +
"\n" +
"-- 标记用户已购买\n" +
"redis.call('setex', KEYS[2], 3600, '1')\n" +
"\n" +
"return 1 -- 成功";
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Arrays.asList(stockKey, userKey)
);
if (result == null) {
return false;
}
switch (result.intValue()) {
case 1:
log.info("扣减库存成功: activityId={}, userId={}", activityId, userId);
return true;
case -1:
log.warn("库存不足: activityId={}", activityId);
throw new SeckillException("商品已售罄");
case -2:
log.warn("重复购买: activityId={}, userId={}", activityId, userId);
throw new SeckillException("您已经购买过了");
default:
return false;
}
}
}
3. 秒杀主流程
/**
* 秒杀服务
*/
@Service
public class SeckillService {
@Autowired
private SeckillTokenService tokenService;
@Autowired
private StockDeductionService stockService;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 执行秒杀
*/
@Transactional(rollbackFor = Exception.class)
public SeckillResult executeSeckill(SeckillRequest request) {
Long activityId = request.getActivityId();
Long userId = request.getUserId();
String token = request.getToken();
// 1. 验证令牌
if (!tokenService.validateToken(token, activityId, userId)) {
throw new SeckillException("令牌无效");
}
// 2. 检查活动状态
if (!isActivityValid(activityId)) {
throw new SeckillException("活动已结束");
}
// 3. 扣减库存(Redis原子操作)
boolean success = stockService.deductStock(activityId, userId);
if (!success) {
return SeckillResult.fail("抢购失败");
}
// 4. 消费令牌
tokenService.consumeToken(token);
// 5. 发送MQ消息(异步创建订单)
SeckillMessage message = new SeckillMessage();
message.setActivityId(activityId);
message.setUserId(userId);
message.setTimestamp(System.currentTimeMillis());
rabbitTemplate.convertAndSend(
"seckill.order.exchange",
"seckill.order.create",
message
);
log.info("秒杀成功,发送MQ消息: userId={}, activityId={}",
userId, activityId);
// 6. 返回结果
return SeckillResult.success("抢购成功,订单生成中");
}
/**
* 检查活动是否有效
*/
private boolean isActivityValid(Long activityId) {
String key = "seckill:activity:" + activityId;
// 从缓存获取活动信息
Object cached = redisTemplate.opsForValue().get(key);
if (cached == null) {
// 缓存未命中,查数据库
// ...
}
// 检查活动时间
// ...
return true;
}
}
/**
* 秒杀Controller
*/
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private SeckillTokenService tokenService;
/**
* 获取秒杀令牌
*/
@PostMapping("/token")
public Result<String> getToken(@RequestBody TokenRequest request) {
Long userId = request.getUserId();
Long activityId = request.getActivityId();
// 参数校验
if (userId == null || activityId == null) {
return Result.fail("参数错误");
}
try {
String token = tokenService.generateToken(activityId, userId);
return Result.success(token);
} catch (SeckillException e) {
return Result.fail(e.getMessage());
}
}
/**
* 执行秒杀
*/
@PostMapping("/execute")
public Result<SeckillResult> execute(
@RequestBody SeckillRequest request,
@RequestHeader("X-Seckill-Token") String token) {
request.setToken(token);
try {
SeckillResult result = seckillService.executeSeckill(request);
return Result.success(result);
} catch (SeckillException e) {
return Result.fail(e.getMessage());
}
}
}
阶段5:异步订单处理
/**
* 订单消息消费者
*/
@Component
public class OrderMessageConsumer {
@Autowired
private OrderService orderService;
@Autowired
private StockMapper stockMapper;
/**
* 消费秒杀订单消息
*/
@RabbitListener(queues = "seckill.order.queue")
public void handleOrderMessage(SeckillMessage message) {
Long activityId = message.getActivityId();
Long userId = message.getUserId();
log.info("收到秒杀订单消息: userId={}, activityId={}", userId, activityId);
try {
// 1. 创建订单
Order order = orderService.createSeckillOrder(activityId, userId);
// 2. 扣减数据库库存
int rows = stockMapper.deductStock(activityId, 1);
if (rows == 0) {
// 库存扣减失败,回滚订单
log.error("数据库库存扣减失败: activityId={}", activityId);
orderService.cancelOrder(order.getId());
return;
}
// 3. 发送订单创建通知
notifyOrderCreated(order);
log.info("订单创建成功: orderId={}, userId={}", order.getId(), userId);
} catch (Exception e) {
log.error("处理秒杀订单失败: userId={}, activityId={}",
userId, activityId, e);
// 回滚Redis库存
rollbackRedisStock(activityId);
}
}
/**
* 回滚Redis库存
*/
private void rollbackRedisStock(Long activityId) {
String stockKey = "seckill:stock:" + activityId;
redisTemplate.opsForValue().increment(stockKey);
log.info("回滚Redis库存: activityId={}", activityId);
}
/**
* 发送订单通知
*/
private void notifyOrderCreated(Order order) {
// 发送短信/推送通知
// ...
}
}
🎯 防刷和安全
1. 验证码
/**
* 验证码服务
*/
@Service
public class CaptchaService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 生成图形验证码
*/
public CaptchaVO generateCaptcha(String sessionId) {
// 生成随机验证码
String code = RandomStringUtils.randomNumeric(4);
// 生成图片
BufferedImage image = createImage(code);
// 保存到Redis
String key = "captcha:" + sessionId;
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
// 转换为Base64
String imageBase64 = imageToBase64(image);
CaptchaVO vo = new CaptchaVO();
vo.setSessionId(sessionId);
vo.setImage(imageBase64);
return vo;
}
/**
* 验证验证码
*/
public boolean validateCaptcha(String sessionId, String userCode) {
String key = "captcha:" + sessionId;
String correctCode = redisTemplate.opsForValue().get(key);
if (correctCode == null) {
return false;
}
// 验证后删除
redisTemplate.delete(key);
return correctCode.equalsIgnoreCase(userCode);
}
}
2. 黑名单
/**
* 黑名单服务
*/
@Service
public class BlacklistService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 检查用户是否在黑名单
*/
public boolean isBlacklisted(Long userId) {
String key = "blacklist:user:" + userId;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 加入黑名单
*/
public void addToBlacklist(Long userId, long hours) {
String key = "blacklist:user:" + userId;
redisTemplate.opsForValue().set(key, "1", hours, TimeUnit.HOURS);
log.warn("用户加入黑名单: userId={}, hours={}", userId, hours);
}
/**
* 检测异常行为
*/
@Async
public void detectAbnormalBehavior(Long userId) {
// 检查用户最近的请求频率
String countKey = "behavior:count:" + userId;
Long count = redisTemplate.opsForValue().increment(countKey);
if (count == 1) {
redisTemplate.expire(countKey, 60, TimeUnit.SECONDS);
}
// 1分钟内超过100次请求,加入黑名单
if (count > 100) {
addToBlacklist(userId, 24); // 封禁24小时
// 告警
alertService.send("检测到异常行为",
"用户 " + userId + " 1分钟内请求" + count + "次");
}
}
}
📊 监控和统计
/**
* 秒杀监控服务
*/
@Service
public class SeckillMonitorService {
@Autowired
private MeterRegistry meterRegistry;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 记录秒杀请求
*/
public void recordRequest(Long activityId, boolean success) {
Counter.builder("seckill.request")
.tag("activityId", String.valueOf(activityId))
.tag("success", String.valueOf(success))
.register(meterRegistry)
.increment();
}
/**
* 实时统计
*/
@Scheduled(fixedDelay = 1000) // 每秒
public void realTimeStats() {
List<Long> activityIds = getActiveActivityIds();
for (Long activityId : activityIds) {
// 剩余库存
Long stock = getRemainStock(activityId);
// 成功订单数
Long successCount = getSuccessCount(activityId);
// 请求总数
Long totalRequest = getTotalRequest(activityId);
// 成功率
double successRate = totalRequest > 0 ?
(double) successCount / totalRequest * 100 : 0;
log.info("秒杀实时数据: activityId={}, stock={}, success={}, total={}, rate={}%",
activityId, stock, successCount, totalRequest,
String.format("%.2f", successRate));
// 发送到监控系统
Gauge.builder("seckill.stock", stock::doubleValue)
.tag("activityId", String.valueOf(activityId))
.register(meterRegistry);
}
}
private Long getRemainStock(Long activityId) {
String key = "seckill:stock:" + activityId;
Object value = redisTemplate.opsForValue().get(key);
return value != null ? Long.valueOf(value.toString()) : 0L;
}
}
🎉 总结
架构核心要点
1. 前端优化 🌐
├── CDN加速
├── 静态资源
├── 按钮防重
└── 倒计时预热
2. 限流层 🚦
├── Nginx限流
├── 网关限流
├── 令牌桶
└── 滑动窗口
3. 缓存层 ⚡
├── 库存预热
├── Redis扣减
├── Lua脚本
└── 原子操作
4. 异步层 📮
├── MQ削峰
├── 异步订单
├── 批量处理
└── 最终一致性
5. 安全层 🛡️
├── 令牌验证
├── 验证码
├── 黑名单
└── 行为检测
性能指标
目标:
├── 支持TPS:100万+
├── 响应时间:< 500ms
├── 成功率:> 99.9%
└── 0超卖
实现:
├── Nginx:过滤90%无效请求
├── 网关:再过滤80%
├── Redis:承载100万QPS
├── MQ:削峰填谷
└── DB:只承载成功订单
记忆口诀
秒杀系统要设计,
分层架构是关键。
前端优化CDN快,
静态资源不变化。
按钮防重加令牌,
倒计时预热连接。
限流层次有三级,
Nginx网关加应用。
令牌桶和滑动窗,
黑名单来防刷单。
库存预热到Redis,
Lua脚本保原子。
扣减成功发MQ,
异步处理创订单。
监控告警要实时,
随时准备应急案。
百万TPS不是梦,
架构合理就能行!
愿你的秒杀系统永不宕机,订单创建秒级完成! ⚡✨