Spring Boot 3 中构建高并发 Java Web 接口的实战:限流、幂等与接口性能优化
做 Java Web 接口开发时,很多人一开始关注的是“功能能不能跑通”,但线上流量一上来,问题就变成了另外一类:
- 同一个请求被重复提交,订单生成两次
- 某个热点接口被突发流量打穿,数据库连接池耗尽
- 单机压测挺好,多实例部署后限流完全失效
- 接口 RT 看起来不高,但吞吐量就是上不去
这些问题,本质上都和高并发下的接口治理有关。
这篇文章我想从一个更偏工程落地的角度,带你在 Spring Boot 3 里把三件事串起来:
- 限流:保护系统不被突发流量击穿
- 幂等:防止重复请求造成数据错乱
- 性能优化:让接口在相同资源下扛住更多请求
我会给出一套可运行的示例,尽量避免“概念讲一堆,代码落不了地”的问题。
背景与问题
先看一个典型业务链路:用户调用下单接口。
在低并发下,代码可能只是这样:
- 校验参数
- 查库存
- 扣库存
- 写订单
- 返回结果
但高并发时,问题会迅速暴露:
- 突发请求:秒杀、活动场景下,单接口 QPS 飙升
- 重复提交:用户连点、网络重试、网关超时重放
- 资源争抢:线程池、数据库连接池、Redis 连接池被打满
- 慢查询拖垮整体吞吐:某个 SQL 慢一点,整体链路排队
如果接口没有保护层,业务代码写得再“优雅”也顶不住。
一个更稳妥的接口处理链
我的经验是,不要把高并发问题只看成“优化 SQL”或者“加机器”。更稳妥的做法是把请求分层处理:
flowchart LR
A[客户端请求] --> B[网关/认证]
B --> C[接口限流]
C --> D[幂等校验]
D --> E[业务处理]
E --> F[缓存/数据库]
F --> G[响应返回]
这个顺序很关键:
- 先限流,挡住不该进来的流量
- 再幂等,避免重复写
- 最后再谈性能优化,让有效请求处理得更快
核心原理
这一节不空谈定义,只讲和实战最相关的部分。
1. 限流:控制进入系统的请求速率
限流的目标不是“让所有请求都成功”,而是让系统在高压下仍然可控。
常见限流维度:
- 按接口:
/api/order/create - 按用户:某个用户每秒最多 N 次
- 按 IP:防恶意请求
- 按系统总量:整个服务总 QPS 上限
常见算法:
- 固定窗口:实现简单,但边界突刺明显
- 滑动窗口:更平滑
- 令牌桶:允许一定突发流量,适合接口场景
- 漏桶:强制匀速流出,更适合削峰
在 Web 接口里,令牌桶通常是一个兼顾效果和实现复杂度的方案。
2. 幂等:同一个请求,多次提交,结果一致
幂等最容易被误解。它不是“接口不能重复调用”,而是:
同一个业务请求重复执行,不会造成重复副作用。
例如创建订单接口:
- 第一次请求:成功创建订单
- 第二次相同请求:不再重复创建,而是返回第一次结果或提示已处理
常见实现方式:
- 前端防重复点击:有用,但不可靠
- 数据库唯一索引:最后一道防线
- Redis 幂等键:高并发下常用
- 业务请求号 / Idempotency-Key:推荐做法
我比较推荐的组合是:
- 请求头传
Idempotency-Key - Redis
SETNX做快速抢占 - 数据库唯一约束兜底
3. 性能优化:不是单点提速,而是减少系统总消耗
接口性能优化,不只是“把某段代码写快一点”,而是从系统视角减少整体资源消耗。
核心抓手通常有:
- 少一次数据库访问
- 避免不必要对象创建
- 缩短事务范围
- 热点数据缓存
- 线程池隔离
- 避免阻塞调用堆积
如果说限流是“别让洪水冲进来”,性能优化就是“把进来的水更快排走”。
方案对比与取舍分析
在架构设计上,这三类问题有多种做法。选型时要看你的流量规模、部署方式和维护成本。
限流方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地内存限流 | 实现简单,性能高 | 多实例不一致 | 单机、低复杂场景 |
| Redis 分布式限流 | 多实例统一,部署常见 | 依赖 Redis,可用性要保障 | 多实例生产环境 |
| 网关层限流 | 统一治理,接入方便 | 粒度受限,不够贴近业务 | 通用 API 保护 |
| 应用层 AOP 限流 | 业务灵活,可按方法控制 | 侵入应用层 | 需要细粒度策略 |
我的建议是:
- 网关限流 + 应用层补充限流
- 单机 demo 可以先用本地
- 生产环境尽量用 Redis 分布式限流
幂等方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 前端按钮置灰 | 成本低 | 不可靠 | 体验优化 |
| 数据库唯一索引 | 强一致 | 只能兜底,提示不友好 | 核心写操作 |
| Redis 幂等键 | 性能高 | 需要 TTL 和状态设计 | 高并发接口 |
| 防重表 | 可审计 | 成本高 | 金融、支付类 |
最稳的思路通常不是“二选一”,而是:
- Redis 防重负责快速拦截
- 数据库唯一索引负责最终一致性兜底
容量估算:别等压测时才发现配置不够
很多接口上线前没有做容量估算,最后会出现一种尴尬局面:业务逻辑没问题,但连接池和线程池先倒了。
一个简单估算思路:
假设:
- 峰值 QPS:1000
- 平均 RT:50ms
粗略并发量可估算为:
并发数 ≈ QPS × RT = 1000 × 0.05 = 50
这意味着你的线程池、连接池、下游资源至少要围绕这个量级来设计。
再考虑突发峰值,比如 3 倍瞬时流量:
保护阈值 ≈ 1000 ~ 1500 QPS
突发缓冲 ≈ 2000 QPS 内短时可接受
所以:
- 限流阈值不能瞎设成“越大越好”
- 数据库连接池也不要盲目调很大
- 线程数更不是越多越强,阻塞型接口线程开太大只会放大上下文切换
实战代码(可运行)
下面我们用一个 Spring Boot 3 示例,演示:
- 基于 Redis 的接口限流
- 基于
Idempotency-Key的幂等控制 - 一个简化的下单接口
1. 项目依赖
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>high-concurrency-api-demo</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
2. 配置文件
application.yml
server:
port: 8080
spring:
data:
redis:
host: localhost
port: 6379
timeout: 2s
3. 启动类
DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4. 统一返回与异常
ApiResponse.java
package com.example.demo.common;
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
public ApiResponse() {
}
public ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, "OK", data);
}
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(false, message, null);
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
BizException.java
package com.example.demo.common;
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
GlobalExceptionHandler.java
package com.example.demo.common;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ApiResponse<Void> handleBiz(BizException e) {
return ApiResponse.fail(e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError() != null
? e.getBindingResult().getFieldError().getDefaultMessage()
: "参数校验失败";
return ApiResponse.fail(msg);
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleOther(Exception e) {
return ApiResponse.fail("系统异常:" + e.getMessage());
}
}
5. 限流注解与切面
这里我们做一个基于 Redis 计数器的简单限流实现。它不是最强版本,但胜在容易理解和落地。
RateLimit.java
package com.example.demo.ratelimit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
String key();
long windowSeconds() default 1;
long maxRequests() default 5;
}
RateLimitAspect.java
package com.example.demo.ratelimit;
import com.example.demo.common.BizException;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.Duration;
import java.util.Objects;
@Aspect
@Component
public class RateLimitAspect {
private final StringRedisTemplate stringRedisTemplate;
public RateLimitAspect(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Before("@annotation(rateLimit)")
public void doBefore(JoinPoint joinPoint, RateLimit rateLimit) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
String ip = getClientIp(request);
String redisKey = "rate_limit:" + rateLimit.key() + ":" + ip;
Long count = stringRedisTemplate.opsForValue().increment(redisKey);
if (count != null && count == 1L) {
stringRedisTemplate.expire(redisKey, Duration.ofSeconds(rateLimit.windowSeconds()));
}
if (count != null && count > rateLimit.maxRequests()) {
throw new BizException("请求过于频繁,请稍后再试");
}
}
private String getClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) {
return xff.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
说明:这里是固定窗口计数器方案,简单易上手。生产环境要更平滑的话,可以升级为 Lua + 滑动窗口 / 令牌桶。
6. 幂等控制
我们使用请求头 Idempotency-Key。如果 Redis 中已经存在相同业务键,就拒绝重复处理。
Idempotent.java
package com.example.demo.idempotent;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
long ttlSeconds() default 30;
}
IdempotentAspect.java
package com.example.demo.idempotent;
import com.example.demo.common.BizException;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.Duration;
import java.util.Objects;
@Aspect
@Component
public class IdempotentAspect {
private final StringRedisTemplate stringRedisTemplate;
private static final ThreadLocal<String> KEY_HOLDER = new ThreadLocal<>();
public IdempotentAspect(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Before("@annotation(idempotent)")
public void before(Idempotent idempotent) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
String idempotencyKey = request.getHeader("Idempotency-Key");
if (idempotencyKey == null || idempotencyKey.isBlank()) {
throw new BizException("缺少 Idempotency-Key 请求头");
}
String redisKey = "idempotent:" + request.getRequestURI() + ":" + idempotencyKey;
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(redisKey, "PROCESSING", Duration.ofSeconds(idempotent.ttlSeconds()));
if (!Boolean.TRUE.equals(success)) {
throw new BizException("请求重复,请勿重复提交");
}
KEY_HOLDER.set(redisKey);
}
@AfterThrowing(pointcut = "@annotation(idempotent)", throwing = "e")
public void afterThrowing(Idempotent idempotent, Exception e) {
String key = KEY_HOLDER.get();
if (key != null) {
stringRedisTemplate.delete(key);
KEY_HOLDER.remove();
}
}
}
这里有一个设计点要说明:
- 业务执行失败时,删除幂等键,允许客户端重试
- 业务执行成功时,保留幂等键直到 TTL 过期,避免短时间重复提交
这是很常见的一种策略。
7. DTO 与 Controller
CreateOrderRequest.java
package com.example.demo.order;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public class CreateOrderRequest {
@NotBlank(message = "用户ID不能为空")
private String userId;
@NotBlank(message = "商品ID不能为空")
private String productId;
@Min(value = 1, message = "购买数量必须大于0")
private int amount;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.userId = userId;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
这里我特意提醒一个我自己也踩过的坑:setProductId 里很容易手滑写错字段。上面的代码就有 bug。下面给出正确版本:
CreateOrderRequest.java(修正版)
package com.example.demo.order;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public class CreateOrderRequest {
@NotBlank(message = "用户ID不能为空")
private String userId;
@NotBlank(message = "商品ID不能为空")
private String productId;
@Min(value = 1, message = "购买数量必须大于0")
private int amount;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
OrderController.java
package com.example.demo.order;
import com.example.demo.common.ApiResponse;
import com.example.demo.idempotent.Idempotent;
import com.example.demo.ratelimit.RateLimit;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@RateLimit(key = "create_order", windowSeconds = 1, maxRequests = 3)
@Idempotent(ttlSeconds = 10)
public ApiResponse<Map<String, Object>> createOrder(@Valid @RequestBody CreateOrderRequest request)
throws InterruptedException {
// 模拟业务处理耗时
Thread.sleep(100);
String orderId = UUID.randomUUID().toString();
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("userId", request.getUserId());
result.put("productId", request.getProductId());
result.put("amount", request.getAmount());
return ApiResponse.ok(result);
}
}
8. AOP 依赖补充
如果你的工程没有自动带上 AOP,还需要增加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
9. 调用示例
curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-create-1001' \
--data '{
"userId": "u1001",
"productId": "p2001",
"amount": 2
}'
如果短时间内用相同 Idempotency-Key 重复提交,会得到类似响应:
{
"success": false,
"message": "请求重复,请勿重复提交",
"data": null
}
请求执行时序
用一张时序图看得更清楚:
sequenceDiagram
participant C as Client
participant A as Spring Boot API
participant R as Redis
participant B as Business
C->>A: POST /api/orders + Idempotency-Key
A->>R: 限流计数 INCR
R-->>A: count
A->>R: SETNX 幂等键
R-->>A: success/fail
alt 幂等成功
A->>B: 执行业务逻辑
B-->>A: 返回订单结果
A-->>C: 200 OK
else 幂等失败
A-->>C: 重复请求
end
性能优化:高并发接口真正该优化什么
上面的代码把“防御层”搭起来了,但如果业务逻辑本身很慢,接口照样扛不住。下面是更贴近实战的优化点。
1. 缩短事务范围
很多同学喜欢把整个方法都包进事务里,但事务越大,锁持有时间越长,吞吐越低。
错误思路:
- 参数校验、远程调用、组装响应都放在同一事务中
更好的做法:
- 只把真正需要原子性的数据库写操作放进事务
2. 缓存热点读数据
比如商品基础信息、用户等级、活动配置,这些都适合缓存。
目标不是“全部缓存”,而是优先缓存:
- 访问频繁
- 更新相对少
- 查询代价高
3. 避免接口内阻塞等待
例如:
- 同步调用慢下游
- 不必要的
Thread.sleep - 在请求线程里做复杂报表计算
如果是非核心流程,可以异步化:
- 发 MQ
- 异步线程池
- 事件驱动
但注意:异步化不是万能药,它只是把延迟转移了,前提是你能接受最终一致性。
4. 数据库优化要落到 SQL 层
高并发接口最终常常卡在数据库。
重点检查:
- 是否走索引
- 是否出现回表过多
- 是否有
select * - 是否存在大事务
- 是否把热点更新集中在同一行
5. JSON 序列化与对象创建
这个点常被忽略,但在高 QPS 场景会很明显:
- 少创建中间对象
- 响应字段别过于冗余
- 大对象不要频繁拷贝
- 注意日志打印不要序列化整个请求体
常见坑与排查
这一部分非常重要,因为很多方案“看起来对”,但线上会翻车。
1. 多实例部署后,本地限流失效
现象
单机测试没问题,部署 3 台后,整体流量超出预期。
原因
限流状态放在本地内存,每台机器各算各的。
解决
改为 Redis 分布式限流,或者在网关层统一限流。
2. 幂等键 TTL 设置不合理
现象
- TTL 太短:请求还没处理完,幂等键过期了,重复请求又进来
- TTL 太长:业务失败后用户长时间无法重试
经验值
TTL 至少覆盖:
接口最大处理耗时 + 网络重试窗口 + 一点冗余
比如接口最慢 3 秒,客户端 5 秒内可能自动重试一次,那 TTL 设 10~30 秒会更稳一些。
3. AOP 不生效
现象
注解加了,但限流和幂等逻辑完全没执行。
排查
- 是否引入
spring-boot-starter-aop - 方法是否是
public - 是否发生了类内部自调用
- 切点表达式是否正确
我之前就踩过“同类内部调用导致切面失效”的坑,查半天 Redis 还以为是连接问题,最后发现压根没进切面。
4. X-Forwarded-For 获取 IP 不准确
现象
所有请求都显示成一个代理 IP,导致限流误伤。
原因
服务部署在 Nginx、Ingress 或网关后面时,没有正确透传真实 IP。
解决
- 网关/Nginx 正确设置
X-Forwarded-For - 应用层只信任受控代理传来的头
- 不要盲信客户端自己传的 IP 头
5. Redis 原子性理解不完整
现象
高并发下偶发限流不准、状态异常。
原因
多条 Redis 命令组合执行,不一定具备完整原子性。
解决
生产环境建议把复杂限流逻辑写成 Lua 脚本,把判断和更新放到一次原子执行里。
安全/性能最佳实践
这一部分给“能直接拿去用”的建议。
1. 限流不要只做一层
推荐分层控制:
- 网关层:拦截明显异常流量
- 应用层:按业务接口、用户、资源维度细分
- 下游保护:线程池隔离、连接池上限
flowchart TD
A[外部流量] --> B[网关限流]
B --> C[应用层限流]
C --> D[幂等校验]
D --> E[线程池/连接池隔离]
E --> F[数据库/缓存]
2. 幂等必须有“业务唯一标识”
不要只拿用户 ID 当幂等键,否则同一个用户正常下两单也会被拦。
更合理的是:
- 订单提交号
- 支付请求号
- 客户端生成的请求唯一 ID
3. 数据库唯一索引一定要保留
Redis 很快,但不是最终真相来源。
核心写操作建议保留数据库唯一索引兜底,比如:
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
product_id VARCHAR(64) NOT NULL,
amount INT NOT NULL,
UNIQUE KEY uk_order_no (order_no)
);
4. 接口超时要明确
高并发时,最怕“慢慢卡死”。
建议明确配置:
- Web 请求超时
- Redis 超时
- 数据库超时
- 下游 HTTP/RPC 超时
超时不是坏事,无边界等待才是坏事。
5. 监控指标要提前埋
至少监控这些指标:
- QPS
- RT(P95/P99)
- 限流次数
- 幂等拦截次数
- Redis 延迟
- 数据库慢查询数量
- Tomcat 线程池活跃数
没有监控时,排查高并发问题基本靠猜。
进一步优化方向
如果你的业务量继续上升,可以往这些方向演进:
1. Lua 脚本实现分布式令牌桶
比简单计数器更平滑,也更适合秒杀类流量控制。
2. 返回幂等结果而不是直接报错
更高级的实现是:
- 第一次请求成功后,把结果写入 Redis
- 重复请求直接返回第一次结果
这样客户端体验更好,特别适合支付、创建类接口。
3. 将削峰交给消息队列
对于强突发场景,接口层只做接收与校验,真正落库异步消费。
但前提是业务能接受:
- 排队
- 最终一致
- 结果延迟可见
总结
在 Spring Boot 3 里做高并发接口,不要把问题拆散看。
限流、幂等、性能优化,本质上是同一套稳定性设计的不同面。
你可以按这个落地顺序来做:
- 先加限流:至少保护热点接口
- 再补幂等:创建、支付、回调类接口必须做
- 再看性能瓶颈:优先优化数据库、缓存和阻塞调用
- 最后做分层治理:网关、应用、下游一起兜住
如果你的项目还在“功能优先”的阶段,我建议最少先做两件事:
- 给关键写接口加
Idempotency-Key - 给热点 API 加 Redis 分布式限流
这两件事投入不高,但对线上稳定性的提升非常直接。
最后也给一个边界条件:
如果你的系统还是单机、低 QPS、内部使用工具型接口,就没必要一上来把架构做得特别重。
但只要涉及:
- 支付
- 下单
- 营销活动
- 回调通知
- 高频公网 API
那限流和幂等就不是“优化项”,而是必选项。