跳转到内容
123xiao | 无名键客

《Java Web开发实战:基于Spring Boot与Redis实现高并发接口的限流、幂等与性能优化》

字数: 0 阅读时长: 1 分钟

Java Web开发实战:基于Spring Boot与Redis实现高并发接口的限流、幂等与性能优化

在做 Java Web 接口时,真正把系统压垮的,往往不是“业务逻辑复杂”,而是高并发下的失控访问

  • 某个热点接口被瞬时打爆
  • 用户重复点击导致重复下单
  • 重试机制把服务越压越慢
  • 数据库顶不住,线程池被耗尽,接口雪崩

这篇文章我不打算只讲概念,而是从一个比较实用的角度,把 Spring Boot + Redis 在高并发接口治理中的三个核心问题串起来讲清楚:

  1. 限流:拦住超过阈值的请求,保护系统
  2. 幂等:避免同一请求被重复执行
  3. 性能优化:减少 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)]

这个顺序很重要:

  1. 先限流:挡掉无意义或超载请求
  2. 再幂等:避免重复业务执行
  3. 最后落库/调用下游:把昂贵资源留给有效请求

几种常见方案对比

方案优点缺点适用场景
单机内存限流实现简单、性能高多实例不一致,重启丢状态本地开发、单实例服务
网关限流靠前拦截,保护后端难做细粒度业务幂等通用流量防护
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

这个组合有三个明显好处:

  1. 先保护系统,再保证结果唯一
  2. 把重复请求挡在业务逻辑之前
  3. 减少数据库和下游服务无效压力

实战代码(可运行)

下面给一个可落地的 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 次开始会触发限流。


状态设计建议

仅仅有 PROCESSINGSUCCESS,对很多业务其实还不够。更完整的状态设计如下:

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 状态,后续请求就会一直被当作重复请求挡掉。

排查方式:

  1. 看应用日志是否有异常堆栈
  2. 查 Redis 中对应幂等 key 的值和 TTL
  3. 确认异常分支是否调用了删除或失败状态更新逻辑

建议:

  • 失败分支显式删除 key 或设置 FAILED
  • PROCESSING 必须有过期时间,不能永久存在

4. 非原子操作导致并发穿透

如果你先 GETSET,在高并发下几乎肯定会出问题。
现象通常是:

  • 明明做了幂等,还是重复下单
  • 明明加了限流,瞬时突刺还是穿过去了

建议:

  • 限流逻辑用 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 接口治理里,限流、幂等、性能优化 不是三件互相独立的事,而是一套组合拳:

  • 限流解决的是“系统能不能扛住”
  • 幂等解决的是“业务会不会被重复执行”
  • 性能优化解决的是“治理能力本身会不会拖垮系统”

如果你要快速落地,我建议按这个顺序推进:

  1. 先做 Redis 固定窗口限流
  2. 再做基于 Idempotency-Key 的幂等控制
  3. 关键业务增加数据库唯一约束兜底
  4. 补齐监控、日志、异常清理和 TTL 策略
  5. 压力上来后,再考虑滑动窗口、网关联动和结果复用

最后给一个很实用的边界建议:

  • 如果只是普通查询接口,别把幂等做得过重
  • 如果是下单、支付、发券、扣库存,幂等必须做到业务层
  • 如果 Redis 不稳定,不要盲目把所有治理责任都压给它
  • 如果你还没有监控,先别急着上复杂算法,先把可观测性补起来

很多时候,真正好用的架构方案,不是“最炫的方案”,而是能在你当前团队、当前业务量、当前运维能力下稳定运行的方案。这才是实战。


分享到:

上一篇
《前端性能实战:基于 Web Vitals 的指标监控、瓶颈定位与优化闭环构建》
下一篇
《微服务架构中基于服务网格的灰度发布与流量治理实战》