Files
blog-press/docs/Web/Others/Seckill.md
2026-06-22 15:51:10 +08:00

130 lines
3.8 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;采用JMeter工具进行压测。
## 3.1 创建测试数据
&emsp;&emsp;新建users.csv输入以下内容
```csv
1
2
3
...
```
&emsp;&emsp;然后在`线程组->添加->配置元件->CSV Data Set Config`设置变量名称为userId。
::: tip
不需要输入表头。
:::
## 3.2 HTTP请求
&emsp;&emsp;设置请求路径为`/seckill/${userId}`设置线程组参数例如线程数1000Ramp-Up时间5秒循环次数为1000创建聚合报告执行线程查看结果。
![JMeter](../images/jmeter.png)