SpringBoot 接口限流 + 幂等企业级组合实战,彻底防重、防刷、防打爆

一、为什么必须「限流 + 幂等」组合使用?

在上一篇《SpringBoot AOP+Redis 实现接口幂等》中,我们解决了 重复提交、请求重试、多点重试 导致的数据重复问题。

幂等只能解决 “重复请求结果一致”,无法解决高频恶意请求、瞬间打爆接口、DDOS 刷接口问题。

真实生产痛点

  1. 只做幂等不做限流

    黑客 / 脚本一秒钟刷几十次接口,虽然不会产生脏数据,但会耗尽服务线程、打满 Redis、拖垮整个系统

  2. 只做限流不做幂等

    突发网络重试、网关重试、用户连点,依然会产生重复订单、重复扣款

企业级标准架构

限流在前、幂等在后

  • 限流:保护系统(防崩、防刷、防高并发)
  • 幂等:保护数据(防重、防脏、防错乱)

两大组件组合,才是生产环境真正的接口安全护盾。

二、技术方案选型

限流算法选型:令牌桶算法

相比固定窗口、滑动窗口:

  • 令牌桶 允许突发流量,适合秒杀、活动场景
  • 限流平滑、不会出现临界值突刺
  • 企业级微服务最主流方案

整体架构

  • 基于 Redis + AOP + 自定义双注解
  • 单机、分布式集群 全部支持
  • 接口粒度精细化控制:不同接口不同限流阈值
  • 与之前幂等框架完全兼容、无缝叠加

三、实现思路(企业级流程)

  1. 请求进入接口,先经过限流拦截
  2. 超出 QPS 直接拦截,返回「系统繁忙」
  3. 限流通过后,进入 幂等 Token 校验
  4. 重复请求直接拦截
  5. 全部校验通过,执行业务逻辑

顺序不能乱:先限流、后幂等!

四、完整代码实战(可直接上线)

4.1 新增限流自定义注解

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /**
     * 限流key前缀
     */
    String prefix() default "rate_limit:";

    /**
     * 限流时间窗口 单位秒
     */
    int time() default 1;

    /**
     * 最大请求次数
     */
    int count() default 10;

    /**
     * 提示语
     */
    String msg() default "请求过于频繁,请稍后再试";
}

4.2 Redis 限流工具类(令牌桶思想 + 计数器实现)

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;

@Component
public class RateLimitUtil {

    @Resource
   <String, Object> redisTemplate;

    /**
     * Lua脚本:原子限流
     * key=限流key
     * 1.不存在则初始化=1
     * 2.超过阈值返回0
     * 3.未超过则自增+1
     */
    private static final String LIMIT_SCRIPT =
            "local key = KEYS[1] " +
            "local limit = tonumber(ARGV[1]) " +
            "local now = tonumber(redis.call('incr', key)) " +
            "if now == 1 then " +
            "   redis.call('expire', key, ARGV[2]) " +
            "end " +
            "<= limit";

    public boolean tryAcquire(String key, int limitCount, int limitTime) {
        DefaultRedisScript<Boolean> script = new Default<>();
        script.setScriptText(LIMIT_SCRIPT);
        script.setResultType(Boolean.class<String> keys = Collections.singletonList(key);
        // Lua参数:最大次数、过期时间
        Boolean result = redisTemplate.execute(script, keys, limitCount, limitTime);
        return Boolean.TRUE.equals(result);
    }
}

使用 Lua 脚本保证原子性,杜绝并发限流穿透问题,生产标准写法。

4.3 新增限流 AOP 切面

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class RateLimitAspect {

    @Resource
    private RateLimitUtil rateLimitUtil;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes()).getRequest();

        // 拼接Key:IP+接口路径,精准限流
        String ip = getIp(request);
        String uri = request.getRequestURI();
        String limitKey = rateLimit.prefix() + ip + ":" + uri;

        // 校验限流
        boolean acquire = rateLimitUtil.tryAcquire(limitKey, rateLimit.count(), rateLimit.time());
        if (!acquire) {
            return Result.fail(rateLimit.msg());
        }
        return point.proceed();
    }

    /**
     * 获取真实IP
     */
    private String getIp(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && xForwardedFor.length() > 0) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}

五、组合使用:限流 + 幂等双防护

5.1 接口直接双注解加持(企业级终极写法)

@PostMapping("/submit/order")
@RateLimit(time = 1, count = 5, msg = "操作过于频繁,每秒最多5次")
@Idempotent(message = "订单提交中,请勿重复提交")<String> submitOrder() {
    // 下单业务
    return Result.success("订单创建成功");
}

执行顺序

  1. 先限流拦截:防止刷接口、高频攻击
  2. 后幂等拦截:防止重复提交、重试问题

双层防护,固若金汤

六、进阶策略(生产优化)

6.1 差异化限流

  • 普通接口:1 秒 10 次
  • 下单 / 支付:1 秒 3~5 次
  • 秒杀接口:1 秒 1 次

6.2 支持全局限流 + 用户级限流扩展

可改造 key 规则:

  • 未登录:IP 限流
  • 已登录:UserId + 接口限流

精准限制单个用户薅接口

6.3 解决幂等误拦截

部分业务异常需要允许重试

可扩展注解参数:autoClean = true

业务异常自动删除 Token,支持用户正常重试。

6.4 黑名单联动(高阶)

限流频繁的 IP 自动加入短时黑名单,防护恶意攻击。

七、常见面试 & 生产问题

Q1:为什么限流必须用 Lua?

Redis 多条命令非原子,高并发下会超限流,Lua 脚本保证一次性执行,绝对原子安全。

Q2:限流和幂等顺序能不能反过来?

绝对不能!

如果先幂等、后限流:恶意流量依然会打满 AOP、占用线程,失去系统保护意义。

Q3:集群环境是否兼容?

完全兼容,基于 Redis 全局计数,分布式统一限流、统一防重

八、终极总结(企业级架构)

  1. 幂等保证数据安全,解决业务重复问题
  2. 限流保证系统安全,解决流量打爆问题
  3. 生产写接口必须双注解同时开启
  4. 一套 AOP 注解框架,零侵入、开箱即用、适配所有项目

欢迎点赞、收藏、关注!持续更新 SpringBoot、微服务、分布式实战干货,带你吃透生产级技术!

更多推荐