feat:增加秒杀系统设计文档
This commit is contained in:
111
docs/Web/Others/Seckill.md
Normal file
111
docs/Web/Others/Seckill.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
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("已售罄");
|
||||
}
|
||||
```
|
||||
  要先减再判断,如果小于0,则表示已售罄,再回滚数据。
|
||||
|
||||
## 2.3 异步
|
||||
```java
|
||||
rocketMQTemplate.convertAndSend("seckill-order", Map.of("userId", userId, "goodsId", 1));
|
||||
return ResponseEntity.status(200).body("秒杀成功");
|
||||
```
|
||||
  Redis成功后则通过发消息通知消费者操作数据库。
|
||||
  为了防止重复创建订单,还需要进行一次幂等校验,即根据订单号的唯一性判断是否有重复的。
|
||||
|
||||
# 三、压测
|
||||
  采用ApiPost工具进行压测。
|
||||
Reference in New Issue
Block a user