系统常用的限流方案实践

目录

1、限流

1.1、设计背景

1.2、限流算法

1.3、使用建议

2、解决方案

[2.1、API 接口级限流](#2.1、API 接口级限流)

2.2、网关层限流

[2.3、AOP 切面限流](#2.3、AOP 切面限流)

2.4、拦截器限流


前沿

在高并发场景下,限流是保护系统不被压垮的"安全阀"。

如下所示:

更多详细介绍,可参考:分布式系统设计的容错机制https://dyclt.blog.csdn.net/article/details/150345015?spm=1011.2415.3001.5331

本文将带你深入理解 网关限流、API 接口限流、AOP 限流、拦截器限流 四种主流方案.


1、限流

1.1、设计背景

如下所示:

  • 防止恶意刷接口(如爬虫、DDoS)
  • 保护下游服务(数据库、第三方 API)
  • 保证核心业务可用性(如支付 > 查询)
  • 实现公平资源分配(如免费用户 vs VIP)

⚠️ 限流 ≠ 熔断/降级,它是入口控制,而非故障处理。

1.2、限流算法

如下所示:

算法 原理 特点
计数器(固定窗口) 每秒最多 N 次请求 实现简单,但存在"临界突刺"问题
滑动窗口 将时间窗口细分,更平滑 精度高,内存占用略大
漏桶(Leaky Bucket) 请求以恒定速率流出 平滑流量,但无法应对突发
令牌桶(Token Bucket) 以固定速率生成令牌,请求消耗令牌 允许突发流量,最常用

Spring Cloud Gateway / Redis + Lua 通常基于令牌桶或滑动窗口

1.3、使用建议

1.第一道防线:网关限流

→ 全局兜底,防 DDoS,按 IP 限流

2.第二道防线:AOP 限流

→ 核心业务方法(如下单、发短信),按用户 ID 限流

3.补充:拦截器限流

→ 传统项目无网关时的替代方案

4.避免:纯 API 限流

→ 仅用于临时调试或单机场景

⚠️注意:限流策略应与 监控告警(如 Prometheus + Grafana) 结合,实时观察限流效果!


2、解决方案

2.1、API 接口级限流

(最细粒度)

1、原理

在 Controller 方法上直接加限流注解或代码,针对单个接口进行限流。

如下所示:

2、实现方式(基于 Guava RateLimiter)

java 复制代码
@RestController
@Slf4j
public class SmsController {

    // 每秒生成 1 个令牌,桶容量 = 2(允许突发 2 次)
    private final RateLimiter smsRateLimiter = RateLimiter.create(1.0, 2, TimeUnit.SECONDS);

    @PostMapping("/api/sms/send")
    public ResponseEntity<?> sendSms(@RequestBody SmsRequest request) {
        // 尝试获取令牌,最多等待 500ms
        if (!smsRateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS)) {
            log.warn("短信接口限流,userId={}", request.getUserId());
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                .body(ApiResponse.error("发送太频繁,请1秒后再试"));
        }

        try {
            smsService.send(request.getPhone(), request.getContent());
            return ResponseEntity.ok(ApiResponse.success());
        } catch (Exception e) {
            log.error("发送短信失败", e);
            return ResponseEntity.status(500).body(ApiResponse.error("发送失败"));
        }
    }
}

⚠️ 关键细节

  • tryAcquire(timeout):避免线程阻塞
  • 单机有效:多实例部署时每个节点独立计数
  • 无持久化:服务重启后计数清零

📊 效果

  • QPS ≤ 1
  • 突发请求:前 2 次立即通过,第 3 次需等待

生产慎用:仅适合单体应用或临时调试

2.2、网关层限流

1、原理

Spring Cloud GatewayNginx 等网关层统一限流,所有请求先经过网关 ,天然支持分布式。推荐!全局入口。

如下所示:

2.Spring Cloud Gateway + Redis 实现(令牌桶)

  1. 添加依赖
XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- 支持 SpEL 表达式 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 配置限流规则
java 复制代码
spring:
  cloud:
    gateway:
      routes:
        # 订单服务
        - id: order_route
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10    # 令牌生成速率(每秒)
                redis-rate-limiter.burstCapacity: 20    # 令牌桶容量
                key-resolver: "#{@userKeyResolver}"     # 按用户ID限流

        # 短信服务(更严格)
        - id: sms_route
          uri: lb://sms-service
          predicates:
            - Path=/api/sms/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 2
                key-resolver: "#{@ipKeyResolver}"       # 按IP限流
  1. 定义限流 Key(按 IP 限流)
java 复制代码
@Configuration
public class KeyResolverConfig {

    // 按用户ID限流(从 Header 获取)
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("X-User-ID");
            return Mono.justOrEmpty(userId).defaultIfEmpty("anonymous");
        };
    }

    // 按 IP 限流
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
        );
    }

    // 按 API 路径限流
    @Bean
    public KeyResolver pathKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
}
  1. 自定义限流响应
java 复制代码
@Component
@Primary
public class JsonBlockHandler implements BlockRequestHandler {
    @Override
    public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
        Map<String, Object> body = Map.of(
            "code", 429,
            "message", "请求过于频繁,请稍后再试",
            "retryAfter", "1" // 建议重试时间(秒)
        );
        
        return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
            .contentType(MediaType.APPLICATION_JSON)
            .header("Retry-After", "1") // HTTP 标准头
            .bodyValue(body);
    }
}
  1. Redis 配置优化(连接池)
java 复制代码
spring:
  redis:
    host: redis-cluster
    port: 6379
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 2

⚠️ 生产避坑指南

问题 解决方案
Redis 宕机导致限流失效 配置降级策略(如本地缓存兜底)
Key 过多导致 Redis 内存爆炸 设置 Key TTL(Gateway 自动设置)
限流不生效 检查路由顺序(先匹配的路由先生效)
高并发下 Lua 脚本性能瓶颈 使用 Redis Cluster 分片

⚠️ 优缺点

强烈推荐:所有微服务架构的首选方案!

2.3、AOP 切面限流

1.原理

通过自定义注解 + AOP,在任意 Service/Controller 方法上添加限流逻辑。(方法级,灵活).

如下所示:

2.实现步骤

  1. 自定义限流注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 限流策略
    int limit() default 10;           // 最大请求数
    int timeout() default 60;         // 时间窗口(秒)
    String key() default "";          // 自定义 key(SpEL 表达式)
    String prefix() default "rate";   // Redis key 前缀
    boolean throwException() default true; // 是否抛异常
}

2.滑动窗口

java 复制代码
@Component
public class RateLimitLuaScript {
    // KEYS[1] = 限流 key
    // ARGV[1] = limit
    // ARGV[2] = timeout(秒)
    public static final String SCRIPT =
        "local current = redis.call('GET', KEYS[1])\n" +
        "if current then\n" +
        "  if tonumber(current) >= tonumber(ARGV[1]) then\n" +
        "    return 0\n" +
        "  else\n" +
        "    redis.call('INCR', KEYS[1])\n" +
        "    return 1\n" +
        "  end\n" +
        "else\n" +
        "  redis.call('SET', KEYS[1], 1, 'EX', ARGV[2])\n" +
        "  return 1\n" +
        "end";
}
  1. AOP 切面实现(基于 Redis + Lua)
java 复制代码
@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Around("@annotation(rateLimit)")
    public Object intercept(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        // 1. 生成限流 key
        String key = resolveKey(joinPoint, rateLimit);
        String redisKey = rateLimit.prefix() + ":" + key;

        // 2. 执行 Lua 脚本
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(RateLimitLuaScript.SCRIPT, Long.class);
        Long result = redisTemplate.execute(script,
            Collections.singletonList(redisKey),
            String.valueOf(rateLimit.limit()),
            String.valueOf(rateLimit.timeout())
        );

        // 3. 判断结果
        if (result != null && result == 1) {
            return joinPoint.proceed();
        } else {
            String errorMsg = String.format("接口限流,key=%s, limit=%d/%ds", 
                redisKey, rateLimit.limit(), rateLimit.timeout());
            log.warn(errorMsg);
            
            if (rateLimit.throwException()) {
                throw new BusinessException("请求过于频繁,请稍后再试");
            }
            return null; // 或返回默认值
        }
    }

    private String resolveKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
        if (!StringUtils.hasText(rateLimit.key())) {
            // 默认 key:类名 + 方法名
            return joinPoint.getSignature().toLongString();
        }

        // 支持 SpEL 表达式,如 "#userId" 或 "#request.phone"
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();
        
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        
        try {
            return parser.parseExpression(rateLimit.key()).getValue(context, String.class);
        } catch (Exception e) {
            log.error("解析 SpEL 表达式失败: {}", rateLimit.key(), e);
            return joinPoint.getSignature().getName(); // 降级
        }
    }
}
  1. 使用示例
java 复制代码
@Service
public class OrderService {

    // 每个用户每分钟最多创建 5 个订单
    @RateLimit(limit = 5, timeout = 60, key = "#userId")
    public Order createOrder(String userId, CreateOrderDTO dto) {
        // ...
    }

    // 每个手机号每天最多发送 3 条短信
    @RateLimit(limit = 3, timeout = 86400, key = "#request.phone", prefix = "sms")
    public void sendSms(SmsRequest request) {
        // ...
    }

    // 全局限流:整个方法每秒最多 10 次
    @RateLimit(limit = 10, timeout = 1)
    public List<Product> listProducts() {
        // ...
    }
}

⚠️ 关键优势

  • 动态 Key:支持方法参数(如用户ID、手机号)
  • 分布式安全:Redis 保证全局一致性
  • 灵活降级:可选择抛异常或返回 null

适用场景:核心业务方法的精细化限流

⚠️ 优缺点

适用场景:需要对特定业务方法限流(如"下单"、"发送短信")

2.4、拦截器限流

(Controller 层统一控制)

1.原理

通过 Spring MVC 的 HandlerInterceptor,在请求进入 Controller 前进行限流。

如下所示:

2.实现步骤

自定义拦截器(带防刷 + 日志)

代码如下所示:

java 复制代码
@Component
@Slf4j
public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 白名单(内部 IP 不限流)
    private static final Set<String> WHITE_LIST = Set.of("127.0.0.1", "192.168.1.100");

    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        String clientIp = getClientIp(request);
        if (WHITE_LIST.contains(clientIp)) {
            return true; // 白名单放行
        }

        String uri = request.getRequestURI();
        String key = "interceptor:limit:" + clientIp + ":" + uri;

        // 使用 INCR + EXPIRE 原子操作(Redis 2.6+)
        Long count = redisTemplate.execute(
            (RedisCallback<Long>) connection -> {
                byte[] redisKey = key.getBytes();
                Long current = connection.incr(redisKey);
                if (current == 1) {
                    connection.expire(redisKey, 60); // 1分钟窗口
                }
                return current;
            }
        );

        if (count != null && count > 20) { // 每分钟最多20次
            log.warn("拦截器限流触发,ip={}, uri={}", clientIp, uri);
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":429,\"msg\":\"请求过于频繁\"}");
            return false;
        }

        return true;
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Real-IP");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 处理代理情况(取第一个 IP)
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}
  1. 注册拦截器
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor())
            .addPathPatterns("/api/**"); // 仅拦截 API 路径
    }
}

⚠️ 注意事项

  • 路径匹配:确保排除健康检查、监控端点
  • IP 获取:正确处理 Nginx 代理(X-Forwarded-For)
  • 性能:每个请求都访问 Redis,需监控延迟

适用场景:传统 Spring MVC 单体应用,无网关时的替代方案

优缺点

适用场景:传统单体应用,需要对所有 API 接口统一限流


总结:


参考文章:

1、分布式系统限流

相关推荐
聆风吟º14 小时前
【数据结构手札】空间复杂度详解:概念 | 习题
java·数据结构·算法
计算机程序设计小李同学14 小时前
基于SpringBoot的个性化穿搭推荐及交流平台
java·spring boot·后端
是一个Bug14 小时前
50道核心JVM面试题
java·开发语言·面试
朱朱没烦恼yeye14 小时前
java基础学习
java·python·学习
她和夏天一样热14 小时前
【观后感】Java线程池实现原理及其在美团业务中的实践
java·开发语言·jvm
郑州光合科技余经理15 小时前
技术架构:上门服务APP海外版源码部署
java·大数据·开发语言·前端·架构·uni-app·php
篱笆院的狗15 小时前
Java 中的 DelayQueue 和 ScheduledThreadPool 有什么区别?
java·开发语言
2501_9418091415 小时前
面向多活架构与数据地域隔离的互联网系统设计思考与多语言工程实现实践分享记录
java·开发语言·python
qualifying16 小时前
JavaEE——多线程(4)
java·开发语言·java-ee
better_liang16 小时前
每日Java面试场景题知识点之-DDD领域驱动设计
java·ddd·实体·领域驱动设计·架构设计·聚合根·企业级开发