java后端学习(从零实现单机限流,Java 后端必备技能)

java后端学习(从零实现单机限流,Java 后端必备技能)
从零实现单机限流,Java 后端必备技能

一、为什么要限流

  1. 防止雪崩(单机限流能避免大量请求瞬间打满线程、数据库连接与 IO 资源,拖垮自身及下游服务,通过舍弃少量请求,防止系统整体雪崩,保住核心业务可用。)
  2. 防止资源耗尽(单机限流可以把请求量控制在服务器承受范围内,避免因 CPU 爆满、内存溢出 OOM、线程池耗尽而导致系统直接崩溃。)
  3. 防御天降流量(单机限流能自动拦截前端 BUG、爬虫误抓、测试忘关脚本、用户狂点等各类意外突发流量,避免服务被非恶意流量打崩。)
  4. 保证公平(单机限流可限制单用户 / 单 IP 的访问频率,避免个别用户耗尽全部资源,保障其他用户正常使用,实现公平访问。)
  5. 流量削峰(单机限流可通过令牌桶等方式削峰填谷,把瞬间突增的流量毛刺拉平,让系统以稳定速度处理请求,避免响应剧烈抖动。)

  6. 二、具体实现

这里我们将采用AOP(面向切面编程)+ 自定义注解,我们可以将限流逻辑从业务代码中剥离,实现声明式限流

1、pom.xml引入jar包

    com.google.guava    guava    33.0.0-jre <!-- 请使用最新稳定版 -->

2、定义自定义注解 @RateLimit

import java.lang.annotation.*;@Target(ElementType.METHOD) // 只能用在方法上@Retention(RetentionPolicy.RUNTIME) // 运行时可见@Documentedpublic @interface RateLimit {    /**     * 每秒允许的请求数 (QPS)     * 默认值为 5     */    double qps() default 5.0;    /**     * 超时等待时间 (毫秒),如果为 0 则立即拒绝,大于 0 则尝试等待     * 默认 0 (立即拒绝)     */    long timeout() default 0;}

3、创建 AOP 切面 RateLimitAspect

import com.google.common.util.concurrent.RateLimiter;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.web.server.ResponseStatusException;import java.lang.reflect.Method;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;@Aspect@Componentpublic class RateLimitAspect {    // 存储每个方法对应的 RateLimiter 实例    // Key: 方法签名 (类名 + 方法名), Value: RateLimiter    private final Map limiterMap = new ConcurrentHashMap<>();    @Around("@annotation(com.example.demo.RateLimit)") // 替换为你的包路径    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {        MethodSignature signature = (MethodSignature) joinPoint.getSignature();        Method method = signature.getMethod();        // 获取注解信息        RateLimit rateLimit = method.getAnnotation(RateLimit.class);        double qps = rateLimit.qps();        long timeout = rateLimit.timeout();        // 生成唯一的 Key (类名 + 方法名)        // 注意:如果同一个方法需要不同的 QPS,这里逻辑需调整(通常注解值固定,所以没问题)        String key = signature.getDeclaringTypeName() + "." + signature.getName();        // 获取或创建 RateLimiter (双重检查锁或直接 computeIfAbsent)        RateLimiter limiter = limiterMap.computeIfAbsent(key, k -> RateLimiter.create(qps));        // 【重要】动态调整:如果注解的 QPS 变了,更新现有的 RateLimiter        // Guava 的 setRate 是线程安全的,可以动态调整        limiter.setRate(qps);        boolean acquired;        if (timeout > 0) {            // 尝试等待获取令牌            acquired = limiter.tryAcquire(timeout, TimeUnit.MILLISECONDS);        } else {            // 立即尝试获取,拿不到就失败            acquired = limiter.tryAcquire();        }        if (!acquired) {            // 限流触发,抛出异常或返回特定结果            throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS,                 "请求过于频繁,当前限制为 " + qps + " QPS");        }        // 获取到令牌,执行目标方法        return joinPoint.proceed();    }}

温馨提示:要记得引入aop的jar包哦!

    org.springframework.boot    spring-boot-starter-aop

4、自定义注解使用

import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class FlexibleController {    /**     * 场景 1:严格限流,每秒最多 2 个请求,超出的直接拒绝     */    @GetMapping("/strict-api")    @RateLimit(qps = 2.0, timeout = 0)     public String strictApi() {        return "严格模式:每秒仅限 2 次访问";    }    /**     * 场景 2:宽松限流,每秒 10 个请求,如果满了最多等待 500ms     * 适用于用户体验优先,稍微慢点也能接受的场景     */    @GetMapping("/smooth-api")    @RateLimit(qps = 10.0, timeout = 500)    public String smoothApi() throws InterruptedException {        // 模拟业务耗时        Thread.sleep(100);         return "平滑模式:每秒 10 次,支持排队等待";    }    /**     * 场景 3:使用默认值 (默认 qps=5, timeout=0)     */    @GetMapping("/default-api")    @RateLimit     public String defaultApi() {        return "默认模式:每秒 5 次访问";    }}

三、扩展

@RateLimit 通常是针对“整个接口”的全局限流(即所有请求共享一个令牌桶)。

1、扩展1:实现“单 IP 限流”

java后端学习(从零实现单机限流,Java 后端必备技能)

@Around("@annotation(rateLimit)")public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {    // 1. 获取当前请求对象 (需要注入 RequestAttributes 或 HttpServletRequest)    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();    // 2. 获取客户端 IP (处理代理情况)    String ip = getClientIp(request);     // 3. 【关键修改】构造唯一的 Key:方法名 + IP    // 这样每个 IP 都会生成一个独立的 RateLimiter 实例    String uniqueKey = joinPoint.getSignature().toShortString() + ":" + ip;    // 4. 获取或创建该 IP 专属的限流器    // 注意:这里需要设置缓存过期策略,否则内存会爆(见下文注意事项)    RateLimiter limiter = limiterMap.computeIfAbsent(uniqueKey, k -> RateLimiter.create(rateLimit.qps()));    if (!limiter.tryAcquire()) {        throw new RuntimeException("您的访问过于频繁,请稍后再试 (IP: " + ip + ")");    }    return joinPoint.proceed();}// 辅助方法:获取真实 IPprivate String getClientIp(HttpServletRequest request) {    String ip = request.getHeader("X-Forwarded-For");    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {        ip = request.getHeader("Proxy-Client-IP");    }    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {        ip = request.getHeader("WL-Proxy-Client-IP");    }    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {        ip = request.getRemoteAddr(); // 最终兜底    }    // 如果是多级代理,取第一个 IP    if (ip != null && ip.contains(",")) {        ip = ip.split(",")[0];    }    return ip;}

⚠️ 重要隐患:内存泄漏风险

优化方案:使用带过期时间的缓存,不要直接用ConcurrentHashMap,改用 Guava Cache或 Caffeine,它们支持自动过期。

// 定义一个带过期时间的缓存:key 存在 60 秒后自动删除// 如果某个 IP 60 秒内没再访问,它的限流器就被回收了,节省内存LoadingCache limiterCache = CacheBuilder.newBuilder()    .expireAfterAccess(60, TimeUnit.SECONDS) // 关键:60 秒无访问自动移除    .build(new CacheLoader() {        @Override        public RateLimiter load(String key) {            // 这里从 key 中解析出配置的 qps,或者使用默认值            return RateLimiter.create(10.0);         }    });// 在切面中使用RateLimiter limiter = limiterCache.get(uniqueKey); // 自动处理创建和过期

2、扩展2:实现“限制 用户 ID限流”

// 伪代码:AOP 切面逻辑public Object around(ProceedingJoinPoint joinPoint) {    // 1. 尝试获取用户 ID (从 Token 解析)    String userId = UserContext.getCurrentUserId();     // 2. 如果没登录,降级为 IP 限流    if (userId == null) {        userId = getClientIp(request);     }    // 3. 构造 Key:方法名 + 用户标识    String key = joinPoint.getSignature().toShortString() + ":" + userId;    // 4. 执行限流 (配合 Caffeine/Guava Cache 做过期处理)    RateLimiter limiter = cache.get(key);    if (!limiter.tryAcquire()) {        throw new TooManyRequestsException("操作太频繁了,请休息一下");    }    return joinPoint.proceed();}




今天,你学废了吗

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章