Files
blog-press/docs/Web/Others/Seckill.md

111 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 秒杀系统设计与实战
date: 2026-06-10
---
# 一、简介
  一个典型的秒杀系统通常采用 **限流 + 缓存 + 异步** 的架构:
```plain
客户端
Nginx / 网关(限流)
秒杀接口校验、Redis 预扣库存)
RocketMQ下单消息
订单服务(创建订单、真正扣库存)
MySQL
```
  **限流挡人、缓存决策、异步解耦**。
# 二、实战设计
## 2.1 限流
### 2.1.1 Nginx限流
```conf
# 定义规则
limit_req_zone $binary_remote_addr zone=seckill:10m rate=5r/s;
server {
listen 80 default_server;
listen [::]:80 default_server;
location /seckill-api/ {
# 启用限流
limit_req zone=seckill burst=10 nodelay;
# 返回 429
limit_req_status 429;
proxy_pass http://127.0.0.1:8080/;
}
}
```
  `limit_req_zone`用来制定规则,`limit_req`表示启用规则,如果某个`location`没写,则不会启动规则。
  `zone=seckill:10m`表示开辟一块`10MB`的内存,取名叫`seckill`,专门存`IP`访问记录,`rate=5r/s`表示每个`IP`每秒最多`5`个请求。
  `limit_req zone=seckill`表示启动`seckill`规则,`burst=10`表示突发请求缓冲,`nodelay`表示超出后直接失败。
::: tip
`limit_req_zone`一定要写在`http`模块中,如果是`Linux`部署的,直接写在最外面。
:::
::: warning
Nginx限流只能防止单ip高刷无法挡住大量不同IP的分布式攻击。
:::
### 2.1.2 Semaphore限流
  `Semaphore`是针对应用层限流,保护`JVM`,防止`Tomcat`线程池被打爆。
```java
private final Semaphore semaphore = new Semaphore(100);
if (!semaphore.tryAcquire()) {
return ResponseEntity.status(429).body("系统繁忙");
}
try {
} finally {
semaphore.release();
}
```
  `new Semaphore(100)`表示最多`100`个线程同时进来,超过则直接返回失败,不会进入排队,也不会卡住线程。
::: tip
Semaphore不是全局限流不具备分布式能力。
:::
### 2.1.3 Redis限流
```java
String userKey = "seckill:user:" + userId;
Boolean first = redis.opsForValue().setIfAbsent(userKey, "1", 60, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(first)) {
return ResponseEntity.status(429).body("请勿重复尝试");
}
```
  如果是限制每个用户只能限购一次,则可以将过期时间设置为永久。
## 2.2 缓存
  先在`Redis`里面判断逻辑,然后再执行数据库。
```java
String stockKey = "seckill:stock:1";
Long remain = redis.opsForValue().decrement(stockKey);
if (remain == null || remain < 0) {
redis.opsForValue().increment(stockKey);
return ResponseEntity.status(200).body("已售罄");
}
```
&emsp;&emsp;要先减再判断如果小于0则表示已售罄再回滚数据。
## 2.3 异步
```java
rocketMQTemplate.convertAndSend("seckill-order", Map.of("userId", userId, "goodsId", 1));
return ResponseEntity.status(200).body("秒杀成功");
```
&emsp;&emsp;Redis成功后则通过发消息通知消费者操作数据库。
&emsp;&emsp;为了防止重复创建订单,还需要进行一次幂等校验,即根据订单号的唯一性判断是否有重复的。
# 三、压测
&emsp;&emsp;采用ApiPost工具进行压测。