111 lines
3.3 KiB
Markdown
111 lines
3.3 KiB
Markdown
---
|
||
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工具进行压测。 |