Java Web开发实战:基于Spring Boot与Redis实现高并发接口的限流、幂等与性能优化
在做 Java Web 接口时,真正把系统压垮的,往往不是“业务逻辑复杂”,而是高并发下的失控访问:
- 某个热点接口被瞬时打爆
- 用户重复点击导致重复下单
- 重试机制把服务越压越慢
- 数据库顶不住,线程池被耗尽,接口雪崩
这篇文章我不打算只讲概念,而是从一个比较实用的角度,把 Spring Boot + Redis 在高并发接口治理中的三个核心问题串起来讲清楚:
- 限流:拦住超过阈值的请求,保护系统
- 幂等:避免同一请求被重复执行
- 性能优化:减少 Redis 和应用层的额外开销,让治理本身不要变成瓶颈
文章会包含可运行代码,并补充架构取舍、常见坑和排查思路。
背景与问题
在典型的 Java Web 系统里,请求路径一般是:
Nginx / 网关 -> Spring Boot 应用 -> Redis / MySQL / 下游服务
问题往往出在下面几类场景:
1. 突发流量导致接口被打爆
比如秒杀、抢券、热点查询,某个接口在几秒内涌入数万请求。
如果没有限流,应用线程、数据库连接池、缓存连接池都会被迅速耗尽。
2. 客户端重试和用户重复点击
前端网络抖动、用户手速快、移动端自动重发,都可能让同一业务请求被执行多次。
最常见的后果就是:
- 重复下单
- 重复扣款
- 重复发券
- 重复提交表单
3. 治理逻辑本身成为新瓶颈
很多项目一开始会在 Java 内存里做限流,但一上多实例部署就不准了。
后来上 Redis,又因为:
- 键设计不合理
- 过期时间设置错误
- Lua 脚本没用
- 非原子操作导致误判
最终出现“明明做了治理,但线上还是不稳”的情况。
所以,这篇文章的重点不是“能不能做”,而是:怎么做得稳定、清晰、能上线。
方案全景与取舍分析
先给出一个推荐的架构思路:
flowchart LR
A[Client] --> B[Spring Boot API]
B --> C[限流拦截器]
C --> D[幂等拦截器]
D --> E[业务服务]
E --> F[(Redis)]
E --> G[(MySQL)]
这个顺序很重要:
- 先限流:挡掉无意义或超载请求
- 再幂等:避免重复业务执行
- 最后落库/调用下游:把昂贵资源留给有效请求
几种常见方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 单机内存限流 | 实现简单、性能高 | 多实例不一致,重启丢状态 | 本地开发、单实例服务 |
| 网关限流 | 靠前拦截,保护后端 | 难做细粒度业务幂等 | 通用流量防护 |
| Redis 分布式限流 | 跨实例一致,可动态扩展 | 依赖 Redis 可用性 | 多实例生产环境 |
| DB 唯一索引做幂等 | 最终一致性强 | 对数据库压力大 | 关键业务兜底 |
| Redis 幂等键 | 响应快,适合高并发 | 需设计状态机和过期策略 | 接口层幂等控制 |
我的建议是:
- 接口限流:优先 Redis
- 业务幂等:Redis + 数据库唯一约束双保险
- 最终保护:网关限流 + 应用限流 + 数据层约束叠加
核心原理
1. 限流原理:固定窗口、滑动窗口、令牌桶
高并发接口里最常用的是这三类。
固定窗口
例如“1 秒最多 100 次请求”。
实现简单,但窗口切换时会出现突刺:上一秒末尾 100 次 + 下一秒开头 100 次。
滑动窗口
把一个大窗口拆成多个小片段统计,更平滑。
准确性更高,但实现复杂度和 Redis 操作成本更高。
令牌桶
系统按固定速率发放令牌,请求必须拿到令牌才可通过。
适合做平滑限流和削峰。
在大多数 Spring Boot + Redis 项目里,固定窗口 + Lua 脚本就已经能解决 80% 问题。
如果业务是支付、秒杀、核心交易,再考虑滑动窗口或令牌桶。
2. 幂等原理:请求唯一标识 + 状态控制
幂等的核心不是“接口调用一次”,而是:
同一个业务请求,无论被调用多少次,结果都应该一致,且只被处理一次。
常见做法是客户端传一个唯一请求号,例如 Idempotency-Key。
服务端收到后,在 Redis 中维护状态:
PROCESSING:正在处理SUCCESS:已成功FAILED:失败,可按策略决定是否允许重试
这个过程必须原子化,否则两个并发请求可能同时认为“自己是第一个”。
3. 为什么 Lua 脚本很关键
如果你用下面这种伪代码:
if (!redis.exists(key)) {
redis.set(key, "1", 10s);
}
这不是原子操作。两个线程并发时,都可能通过 exists 检查。
所以在 Redis 里,多步判断 + 写入 这类逻辑,应该尽量放进 Lua 脚本一次执行。
架构设计:限流与幂等如何组合
sequenceDiagram
participant C as Client
participant A as Spring Boot
participant R as Redis
participant D as DB
C->>A: 请求(携带用户ID/接口路径/幂等键)
A->>R: 执行限流Lua
alt 超过阈值
R-->>A: reject
A-->>C: 429 Too Many Requests
else 通过
A->>R: 幂等检查并设置PROCESSING
alt 已存在SUCCESS/PROCESSING
R-->>A: duplicate
A-->>C: 返回重复提交结果/提示
else 首次请求
A->>D: 执行业务
D-->>A: success
A->>R: 设置SUCCESS并缓存结果
A-->>C: 返回成功
end
end
这个组合有三个明显好处:
- 先保护系统,再保证结果唯一
- 把重复请求挡在业务逻辑之前
- 减少数据库和下游服务无效压力
实战代码(可运行)
下面给一个可落地的 Spring Boot 示例。为了控制篇幅,我会保留核心部分:
- Redis 配置
- 限流注解 + 拦截器
- 幂等注解 + 拦截器
- 控制器示例
示例基于:
- Spring Boot 2.x
- Spring Web
- Spring Data Redis
- Lettuce
1. Maven 依赖
<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-aop</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2. application.yml
spring:
redis:
host: 127.0.0.1
port: 6379
timeout: 3000ms
server:
port: 8080
3. Redis 配置
这里统一使用 StringRedisTemplate,简单直接,调试也方便。
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
}
这个配置类可以为空,Spring Boot 默认就能注入 StringRedisTemplate。
4. 定义限流注解
package com.example.demo.limit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
int maxRequests();
int windowSeconds();
String keyPrefix() default "rate_limit";
}
5. 限流 Lua 脚本执行器
这里用固定窗口模型:
- 以用户 + 接口路径作为 key
- 每次请求计数加 1
- 首次创建时设置过期时间
- 超限直接拒绝
package com.example.demo.limit;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
@Component
public class RateLimitService {
private final StringRedisTemplate stringRedisTemplate;
private final DefaultRedisScript<Long> redisScript;
public RateLimitService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
this.redisScript = new DefaultRedisScript<>();
this.redisScript.setLocation(new ClassPathResource("lua/rate_limit.lua"));
this.redisScript.setResultType(Long.class);
}
public boolean tryAcquire(String key, int maxRequests, int windowSeconds) {
Long result = stringRedisTemplate.execute(
redisScript,
Collections.singletonList(key),
String.valueOf(maxRequests),
String.valueOf(windowSeconds)
);
return result != null && result == 1L;
}
}
6. rate_limit.lua
将脚本放到 src/main/resources/lua/rate_limit.lua
local key = KEYS[1]
local max_requests = tonumber(ARGV[1])
local window_seconds = tonumber(ARGV[2])
local current = redis.call("INCR", key)
if current == 1 then
redis.call("EXPIRE", key, window_seconds)
end
if current > max_requests then
return 0
else
return 1
end
7. 限流拦截器
package com.example.demo.limit;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimitService rateLimitService;
public RateLimitInterceptor(RateLimitService rateLimitService) {
this.rateLimitService = rateLimitService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
if (rateLimit == null) {
return true;
}
String userId = request.getHeader("X-User-Id");
if (userId == null || userId.trim().isEmpty()) {
userId = request.getRemoteAddr();
}
String key = String.format("%s:%s:%s",
rateLimit.keyPrefix(),
userId,
request.getRequestURI());
boolean allowed = rateLimitService.tryAcquire(
key,
rateLimit.maxRequests(),
rateLimit.windowSeconds()
);
if (!allowed) {
response.setStatus(429);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
return false;
}
return true;
}
}
8. 定义幂等注解
package com.example.demo.idempotent;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
String keyPrefix() default "idempotent";
int expireSeconds() default 300;
}
9. 幂等服务
这里实现一个简化但实用的版本:
- 请求开始前尝试
SETNX - 成功则说明首次请求,状态置为
PROCESSING - 业务成功后置为
SUCCESS - 重复请求直接拒绝
package com.example.demo.idempotent;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class IdempotentService {
private final StringRedisTemplate stringRedisTemplate;
public IdempotentService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean start(String key, int expireSeconds) {
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "PROCESSING", expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void success(String key, int expireSeconds) {
stringRedisTemplate.opsForValue()
.set(key, "SUCCESS", expireSeconds, TimeUnit.SECONDS);
}
public String getStatus(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public void delete(String key) {
stringRedisTemplate.delete(key);
}
}
10. 幂等拦截器
package com.example.demo.idempotent;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
private final IdempotentService idempotentService;
public IdempotentInterceptor(IdempotentService idempotentService) {
this.idempotentService = idempotentService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Idempotent idempotent = handlerMethod.getMethodAnnotation(Idempotent.class);
if (idempotent == null) {
return true;
}
String idempotencyKey = request.getHeader("Idempotency-Key");
if (idempotencyKey == null || idempotencyKey.trim().isEmpty()) {
response.setStatus(400);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":400,\"message\":\"缺少 Idempotency-Key\"}");
return false;
}
String redisKey = idempotent.keyPrefix() + ":" + idempotencyKey;
boolean firstRequest = idempotentService.start(redisKey, idempotent.expireSeconds());
if (!firstRequest) {
String status = idempotentService.getStatus(redisKey);
response.setStatus(409);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":409,\"message\":\"重复请求\",\"status\":\"" + status + "\"}");
return false;
}
request.setAttribute("IDEMPOTENT_KEY", redisKey);
request.setAttribute("IDEMPOTENT_EXPIRE", idempotent.expireSeconds());
return true;
}
}
11. 注册拦截器
注意顺序:限流在前,幂等在后。
package com.example.demo.config;
import com.example.demo.idempotent.IdempotentInterceptor;
import com.example.demo.limit.RateLimitInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final RateLimitInterceptor rateLimitInterceptor;
private final IdempotentInterceptor idempotentInterceptor;
public WebConfig(RateLimitInterceptor rateLimitInterceptor,
IdempotentInterceptor idempotentInterceptor) {
this.rateLimitInterceptor = rateLimitInterceptor;
this.idempotentInterceptor = idempotentInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**").order(1);
registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**").order(2);
}
}
12. 业务控制器示例
这里模拟一个下单接口。
成功后将幂等状态更新为 SUCCESS;失败则删除 key,允许重试。
这是我比较推荐的策略:失败是否允许重试,要按业务语义决定。
package com.example.demo.controller;
import com.example.demo.idempotent.Idempotent;
import com.example.demo.idempotent.IdempotentService;
import com.example.demo.limit.RateLimit;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class OrderController {
private final IdempotentService idempotentService;
public OrderController(IdempotentService idempotentService) {
this.idempotentService = idempotentService;
}
@PostMapping("/orders")
@RateLimit(maxRequests = 5, windowSeconds = 10)
@Idempotent(expireSeconds = 60)
public Map<String, Object> createOrder(HttpServletRequest request,
@RequestParam String productId,
@RequestParam Integer count) {
String key = (String) request.getAttribute("IDEMPOTENT_KEY");
Integer expire = (Integer) request.getAttribute("IDEMPOTENT_EXPIRE");
try {
// 模拟业务处理
Thread.sleep(200);
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("message", "下单成功");
result.put("productId", productId);
result.put("count", count);
idempotentService.success(key, expire);
return result;
} catch (Exception e) {
idempotentService.delete(key);
throw new RuntimeException("下单失败", e);
}
}
}
13. 启动类
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);
}
}
14. 测试方式
第一次请求:
curl -X POST "http://localhost:8080/api/orders?productId=sku-1001&count=1" \
-H "X-User-Id: 10001" \
-H "Idempotency-Key: order-req-0001"
重复发送同一个 Idempotency-Key:
curl -X POST "http://localhost:8080/api/orders?productId=sku-1001&count=1" \
-H "X-User-Id: 10001" \
-H "Idempotency-Key: order-req-0001"
高频压测同一用户同一接口时,第 6 次开始会触发限流。
状态设计建议
仅仅有 PROCESSING 和 SUCCESS,对很多业务其实还不够。更完整的状态设计如下:
stateDiagram-v2
[*] --> INIT
INIT --> PROCESSING
PROCESSING --> SUCCESS
PROCESSING --> FAILED
FAILED --> PROCESSING: 允许重试
SUCCESS --> [*]
如果你做的是支付、库存扣减、优惠券发放,我建议:
PROCESSING:短过期,防止业务卡死SUCCESS:较长过期,拦截重复请求FAILED:根据错误类型决定是否允许重试- 参数错误:不允许重试
- 超时/网络失败:允许重试
- 下游未知状态:需要人工或补偿任务处理
容量估算与 Redis Key 设计
架构类文章里,落地时最容易被忽略的就是容量估算。
如果只会写代码,不估算 key 数量和过期策略,Redis 很容易从“高性能组件”变成“内存炸弹”。
1. 限流 Key 设计
推荐格式:
rate_limit:{userId}:{uri}
如果接口很多、用户很多,key 数量可能非常大。
估算公式可以简单记为:
每秒活跃限流键数 ≈ 活跃用户数 × 热点接口数
例如:
- 活跃用户:2 万
- 热点接口:20 个
- 窗口:10 秒
理论上短时间内可能出现几十万级 key。
不过这些 key 生命周期短,只要 TTL 合理,Redis 通常可以承受。
2. 幂等 Key 设计
推荐格式:
idempotent:{bizType}:{idempotencyKey}
注意一定要带业务前缀,避免不同接口、不同业务线的 key 冲突。
3. 过期时间怎么定
这是个很典型的线上问题,我总结一个经验值:
- 限流 key:和窗口时间一致即可
- 幂等处理中 key:略大于接口最大处理耗时
- 幂等成功 key:按业务重复提交风险决定,通常 1 分钟到 24 小时不等
比如下单接口通常会在几秒内完成,但用户可能在一分钟内不断点提交,那么成功状态保留 5~10 分钟就比较合理。
常见坑与排查
这一部分我会写得更“接地气”一点,因为很多坑真的是线上踩出来的。
1. 限流误伤:把 NAT 出口 IP 当用户标识
很多人一开始图省事,用 request.getRemoteAddr() 做限流 key。
问题是公司网络、运营商网络、网关代理后,很多用户可能共用一个出口 IP,结果就是:
- 一个用户刷接口,其他人也被限流
- 误伤比例非常高
建议:
- 优先使用登录用户 ID
- 未登录场景可使用设备号、token、手机号、clientId
- IP 只能作为兜底维度
2. 幂等键缺失或前端乱传
如果 Idempotency-Key 完全由前端随便生成,但没有约束,很容易出现:
- 每次点击都生成新 key,根本起不到幂等作用
- 多个业务请求错误复用同一个 key,导致误判重复
建议:
- 幂等键要跟业务动作绑定
- 最好由服务端下发或定义生成规则
- 关键业务中把
用户ID + 业务类型 + 请求号一起纳入校验
3. 业务异常后没有清理 PROCESSING
这是最常见的坑之一。
如果接口执行中抛异常,而 Redis 里还保留 PROCESSING 状态,后续请求就会一直被当作重复请求挡掉。
排查方式:
- 看应用日志是否有异常堆栈
- 查 Redis 中对应幂等 key 的值和 TTL
- 确认异常分支是否调用了删除或失败状态更新逻辑
建议:
- 失败分支显式删除 key 或设置
FAILED PROCESSING必须有过期时间,不能永久存在
4. 非原子操作导致并发穿透
如果你先 GET 再 SET,在高并发下几乎肯定会出问题。
现象通常是:
- 明明做了幂等,还是重复下单
- 明明加了限流,瞬时突刺还是穿过去了
建议:
- 限流逻辑用 Lua
- 幂等初始化用
SET key value NX EX seconds - 涉及多步状态切换时优先 Lua 或事务脚本
5. Redis 时间窗口与应用认知不一致
有些同学会在应用代码里记录时间,在 Redis 里做计数,结果时间边界计算不一致。
表现为:
- 以为 10 秒窗口,实际变成 9 秒或 11 秒
- 压测数据和预期不符
建议:
- 时间窗口尽量完全由 Redis TTL 控制
- 同一逻辑不要同时在 Java 和 Redis 两边算时间
安全/性能最佳实践
1. 限流维度不要只看“用户”
真正上线时,限流最好分层做:
- 全局限流:保护整个服务
- 接口级限流:保护热点 API
- 用户级限流:防刷、防误操作
- 租户级限流:SaaS 场景很常见
你可以理解为“漏斗式防护”,不要把所有压力都丢给单一规则。
2. 对幂等结果做结果缓存
如果重复请求来了,除了提示“重复提交”,更友好的做法是直接返回上一次成功结果。
这在支付、下单、表单提交里体验会更好。
可以把 Redis value 设计成 JSON:
{
"status": "SUCCESS",
"response": {
"code": 0,
"message": "下单成功",
"orderId": "202410260001"
}
}
这样重复请求时就不只是“拒绝”,而是“复用结果”。
3. 不要把 Redis 当唯一真相源
Redis 适合做高性能治理,但不应该独自承担业务唯一性保证。
例如下单场景,我强烈建议再加一层数据库唯一约束,例如:
ALTER TABLE t_order ADD CONSTRAINT uk_order_req UNIQUE (user_id, request_no);
这样即使 Redis 短暂异常,数据库仍然能兜底。
4. 给 Redis 设置合理超时与连接池
线上很多“限流逻辑失效”不是代码错,而是 Redis 连接耗尽、超时严重。
建议重点关注:
- 连接池大小
- 命令超时
- 慢查询
- Redis CPU 和内存使用率
如果 Redis 本身已经接近瓶颈,再把所有接口治理逻辑都压上去,风险会很高。
5. 限流失败响应要标准化
不要简单返回字符串,最好统一结构,并区分:
- 429:请求过多
- 409:重复提交
- 400:幂等参数缺失
- 503:治理组件不可用时的降级提示
这会让前端、网关、监控系统更容易联动。
6. 监控指标必须补齐
上线前至少补这几类指标:
- 限流命中次数
- 幂等拦截次数
- Redis 脚本执行耗时
- 幂等状态分布(PROCESSING / SUCCESS / FAILED)
- 429/409 接口比例
- 热点 key 分布
很多团队不是不会做治理,而是“做了但看不见效果”。
没有指标,就很难判断规则是不是过严、是否误伤用户、是否出现热点倾斜。
可进一步演进的方向
如果你的业务量继续上涨,可以考虑下面几个方向:
1. 从固定窗口升级到滑动窗口
适合对流量平滑性要求更高的接口,例如支付确认、库存扣减。
代价是实现和维护复杂度更高。
2. 把通用能力沉淀成 Starter
如果团队里有多个 Spring Boot 服务,建议把:
- 注解
- 拦截器
- Lua 脚本
- 错误码
- 监控埋点
统一封装成内部组件,避免每个项目各写一套。
3. 网关与应用双层限流
- 网关层:挡住明显恶意流量
- 应用层:做业务细粒度控制
这比单独依赖某一层稳定得多。
flowchart TD
A[客户端请求] --> B[API网关限流]
B -->|通过| C[Spring Boot应用限流]
C -->|通过| D[幂等控制]
D --> E[业务处理]
E --> F[MySQL/下游服务]
B -->|拦截| G[返回429]
C -->|拦截| G
D -->|重复请求| H[返回409或复用结果]
总结
在高并发 Java Web 接口治理里,限流、幂等、性能优化 不是三件互相独立的事,而是一套组合拳:
- 限流解决的是“系统能不能扛住”
- 幂等解决的是“业务会不会被重复执行”
- 性能优化解决的是“治理能力本身会不会拖垮系统”
如果你要快速落地,我建议按这个顺序推进:
- 先做 Redis 固定窗口限流
- 再做基于 Idempotency-Key 的幂等控制
- 关键业务增加数据库唯一约束兜底
- 补齐监控、日志、异常清理和 TTL 策略
- 压力上来后,再考虑滑动窗口、网关联动和结果复用
最后给一个很实用的边界建议:
- 如果只是普通查询接口,别把幂等做得过重
- 如果是下单、支付、发券、扣库存,幂等必须做到业务层
- 如果 Redis 不稳定,不要盲目把所有治理责任都压给它
- 如果你还没有监控,先别急着上复杂算法,先把可观测性补起来
很多时候,真正好用的架构方案,不是“最炫的方案”,而是能在你当前团队、当前业务量、当前运维能力下稳定运行的方案。这才是实战。