# scaffold 项目之请求限流

# 简介

请求限流是一种通过控制单位时间内系统处理的请求数量,防止服务过载的技术手段。其核心目标包括:

  • 保护系统稳定性:避免突发流量导致资源耗尽(如 CPU、内存、数据库连接)。
  • 防止恶意攻击:如 DDoS 攻击或 API 滥用。
  • 公平分配资源:确保所有用户或服务平等访问,避免少数用户占用过多资源。

# 常见的限流算法

算法原理特点
固定窗口在固定时间窗口(如 1 分钟)内统计请求数,超过阈值则拒绝请求。实现简单,但窗口切换时可能产生突发流量(如两窗口交界处请求翻倍)。
滑动窗口将时间划分为更细粒度的子窗口(如每秒),动态统计最近 N 个子窗口的请求。缓解固定窗口的突发问题,但计算复杂度稍高。
漏桶算法请求以任意速率进入漏桶,以恒定速率流出(如每秒 10 次),桶满则拒绝请求。输出速率恒定,适合需严格平滑流量的场景(如视频流处理)。
令牌桶算法系统以固定速率生成令牌,请求需获取令牌才能处理,桶空则拒绝。允许突发流量(如一次性消耗桶内所有令牌),适合秒杀等高并发场景。

# 典型应用场景

  • API 限流:防止用户频繁调用接口(如第三方 API 调用次数限制)。
  • 微服务保护:避免下游服务故障引发雪崩(如结合熔断机制)。
  • 分布式系统协调:全局限制跨节点的总请求量(如使用 Redis 实现分布式限流)。
  • 防止爬虫滥用:限制同一 IP 的访问频率。

# 开始使用

# 说明

本项目封装了 scaffold-spring-boot-starter-protection 组件, 由它的 ratelimiter 包来做幂等性,它提供了 声明式 的限流特性,可以防止请求过多。列如说,用户恶意疯狂点击某个按钮,导致发送了大量的请求。

声明式注解代码如下:

package com.tz.scaffold.framework.ratelimiter.core.annotation;
import com.tz.scaffold.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.tz.scaffold.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
 * <p> Project: scaffold - RateLimiter </p>
 *
 * 限流注解
 * @author Tz
 * @version 1.0.0
 * @date 2025/04/08 20:53
 * @since 1.0.0
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
    /**
     * 限流的时间,默认为 1 秒
     */
    int time() default 1;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 限流次数
     */
    int count() default 100;
    /**
     * 提示信息,请求过快的提示
     *
     * 为空时,使用 TOO_MANY_REQUESTS 错误提示
     * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
     */
    String message() default "";
    /**
     * 使用的 Key 解析器
     *
     * @see DefaultRateLimiterKeyResolver 全局级别
     * @see UserRateLimiterKeyResolver 用户 ID 级别
     * @see ClientIpRateLimiterKeyResolver 用户 IP 级别
     * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
     * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg ()} 计算
     */
    Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
    /**
     * 使用的 Key 参数
     */
    String keyArg() default "";
}

实际使用

@RateLimiter(timeout = 10, timeUnit = TimeUnit.SECONDS, count = 2)
@Post("/user/add")
public String createUser(User user) {
    userService.add(user);
    return "添加成功";
}

上面注释的解释,10 秒钟内,所有用户只能操作两次

问题:如何指定用户或者 IP 在指定时间内限制请求呢?

自定义规则:
可设置该注解的 keyResolver 属性,可选择的有:

  • DefaultRateLimiterKeyResolver: 全局级别
  • UserRateLimiterKeyResolver: 用户 ID 级别
  • ClientlpRateLimiterKeyResolver: 用户 IP 级别
  • ServerNodeRateLimiterKeyResolver: 服务器 Node 级别
  • ExpressionldempotentKeyResolver: 自定义级别,通过 keyArg 属性指定 Spring EL 表达式

# 实现原理

设计限流关键的几个点

  • 限流粒度:按用户、IP、接口或全局维度控制。
  • 超时策略:直接拒绝(返回 429 状态码)、排队等待或降级处理。
  • 动态调整:根据系统负载自动调整阈值(如 CPU 使用率 >80% 时触发限流)。
  • 监控与告警:记录限流事件,实时报警(如 Prometheus + Grafana)。

实现原理是针对相同的参数,一段时间内,有且仅能执行一次,这里能想到的就算用 redis,执行流程如下:

在执行方法前,判断参数对应的 key 是否超过限制:

  • 如果 超过 ,则进行报错。
  • 如果 未超过 ,则使用 Redis 计数 + 1

默认参数的 Redis Key 的计算规则由 DefaultRateLimiter 实现,使用 MD5 (方法名 + 方法参数),避免 RediskevresolverKey 过长。

# @RateLimiter 注解

package com.tz.scaffold.framework.ratelimiter.core.annotation;
import com.tz.scaffold.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.impl.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
 * <p> Project: scaffold - RateLimiter </p>
 *
 * 限流注解
 * @author Tz
 * @version 1.0.0
 * @date 2025/04/08 20:53
 * @since 1.0.0
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
    /**
     * 限流的时间,默认为 1 秒
     */
    int time() default 1;
    /**
     * 时间单位,默认为 SECONDS 秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 限流次数
     */
    int count() default 100;
    /**
     * 提示信息,请求过快的提示
     *
     * 为空时,使用 TOO_MANY_REQUESTS 错误提示
     * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
     */
    String message() default "";
    /**
     * 使用的 Key 解析器
     *
     * @see DefaultRateLimiterKeyResolver 全局级别
     * @see UserRateLimiterKeyResolver 用户 ID 级别
     * @see ClientIpRateLimiterKeyResolver 用户 IP 级别
     * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
     * @see ExpressionRateLimiterKeyResolver 自定义表达式,通过 {@link #keyArg ()} 计算
     */
    Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
    /**
     * 使用的 Key 参数
     */
    String keyArg() default "";
}

对应的切面处理:

package com.tz.scaffold.framework.ratelimiter.core.aop;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.tz.scaffold.framework.common.exception.ServiceException;
import com.tz.scaffold.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.tz.scaffold.framework.common.util.collection.CollectionUtils;
import com.tz.scaffold.framework.ratelimiter.core.annotation.RateLimiter;
import com.tz.scaffold.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import com.tz.scaffold.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.List;
import java.util.Map;
/**
 * <p> Project: scaffold - RateLimiterAspect </p>
 *
 * 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作
 * @author Tz
 * @version 1.0.0
 * @date 2025/04/08 21:04
 * @since 1.0.0
 */
@Aspect
@Slf4j
public class RateLimiterAspect {
    /**
     * RateLimiterKeyResolver 集合
     */
    private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;
    private final RateLimiterRedisDAO rateLimiterRedisDAO;
    public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
        this.rateLimiterRedisDAO = rateLimiterRedisDAO;
    }
    @Before("@annotation(rateLimiter)")
    public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 获得 IdempotentKeyResolver 对象
        RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
        Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
        // 解析 Key
        String key = keyResolver.resolver(joinPoint, rateLimiter);
        // 获取 1 次限流
        boolean success = rateLimiterRedisDAO.tryAcquire(key,
                rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit());
        if (!success) {
            log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs());
            String message = StrUtil.blankToDefault(rateLimiter.message(),
                    GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
            throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message);
        }
    }
}

生成 key 和对应的写入 redis 操作:

package com.tz.scaffold.framework.ratelimiter.core.redis;
import lombok.AllArgsConstructor;
import org.redisson.api.*;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
 * <p> Project: scaffold - RateLimiterRedisDAO </p>
 *
 * 限流 Redis DAO
 * @author Tz
 * @version 1.0.0
 * @date 2025/04/09 9:22
 * @since 1.0.0
 */
@AllArgsConstructor
public class RateLimiterRedisDAO {
    /**
     * 限流操作
     *
     * KEY 格式:rate_limiter:% s // 参数为 uuid
     * VALUE 格式:String
     * 过期时间:不固定
     */
    private static final String RATE_LIMITER = "rate_limiter:%s";
    private final RedissonClient redissonClient;
    public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
        // 1. 获得 RRateLimiter,并设置 rate 速率
        RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
        // 2. 尝试获取 1 个
        return rateLimiter.tryAcquire();
    }
    private static String formatKey(String key) {
        return String.format(RATE_LIMITER, key);
    }
    private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
        long rateInterval = timeUnit.toSeconds(time);
        // 1. 如果不存在,设置 rate 速率
        RateLimiterConfig config = rateLimiter.getConfig();
        if (config == null) {
            rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
            rateLimiter.expire(rateInterval, TimeUnit.SECONDS);
            return rateLimiter;
        }
        // 2. 如果存在,并且配置相同,则直接返回
        if (config.getRateType() == RateType.OVERALL
                && Objects.equals(config.getRate(), count)
                && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
            return rateLimiter;
        }
        // 3. 如果存在,并且配置不同,则进行新建
        rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
        rateLimiter.expire(rateInterval, TimeUnit.SECONDS);
        return rateLimiter;
    }
}

# 使用示例

在需要使用的地方引入组件

<dependency>
    <groupId>com.tz.boot</groupId>
    <artifactId>scaffold-spring-boot-starter-protection</artifactId>
</dependency>

声明式注解

@RateLimiter(timeout = 10, timeUnit = TimeUnit.SECONDS, count = 2)

# 总结

请求限流是保障系统高可用的关键措施,需结合业务场景选择合适的算法和工具。平衡系统保护与用户体验,通过监控和动态调整实现智能化流量控制。