• 【前后端的那些事】SpringBoot 基于内存的ip访问频率限制切面(RateLimiter)


    1. 什么是限流

    限流就是在用户访问次数庞大时,对系统资源的一种保护手段。高峰期,用户可能对某个接口的访问频率急剧升高,后端接口通常需要进行DB操作,接口访问频率升高,DB的IO次数就显著增高,从而极大的影响整个系统的性能。如果不对用户访问频率进行限制,高频的访问容易打跨整个服务

    2. 常见的限流策略

    2.1 漏斗算法

    我们想象一个漏斗,大口用于接收客户端的请求,小口用于流出用户的请求。漏斗能够保证流出请求数量的稳定。

    在这里插入图片描述

    2.2 令牌桶算法

    令牌桶算法,每个请求想要通过,就必须从令牌桶中取出一个令牌。否则无法通过。而令牌会内部会维护每秒钟产生的令牌的数量,使得每秒钟能够通过的请求数量得到控制

    在这里插入图片描述

    2.3 次数统计

    次数统计的方式非常直接,每一次请求都进行计数,并统计时间戳。如果下一次请求携带的时间戳在一定的频率内,进行次数的累加。如果次数达到一定阈值,则拒绝后续请求。直到下一次请求时间戳大于初始时间戳,重置接口次数与时间戳

    在这里插入图片描述

    3. 令牌桶代码编写

    令牌桶算法我们可以使用Google guava包下的封装好的RateLimiter,紧紧抱住大爹大腿

    另外,ip频率限制是一个横向逻辑,该功能应该保护所有后端接口,因此我们可以采用Spring AOP增强所有后端接口

    另外,我们需要对同一个用户,对同一个接口访问次数进行限流,这意味着我们需要限制的是——(用户,接口)这样的一对元组。用户可以通过ip进行限定,也就是说,后端是同一个ip针对同一个请求的访问进行限流

    因此我们需要为每一个这样的(ip,method)使用令牌桶限流,(ip,method)-> RateLimiter。ip + method这一对元组唯一确定一个RateLimiter

    我们可以采用Map缓存这样的一一对应的关系

    But,HashMap显然不适合,应为HashMap不防并发;另外ConcurrentHashMap也不合适,假如一个用户发出一个请求后就下线了,那么这个key就会长久的存活于内存中,这极大的增加了内存的压力

    因此我们采用Google的Cache

    Google大爹提供的Cache功能极其强大,读者可以自行阅读下面文档

    /**
     * A builder of {@link LoadingCache} and {@link Cache} instances having any combination of the
     * following features:
     *
     * 
      *
    • automatic loading of entries into the cache *
    • least-recently-used eviction when a maximum size is exceeded *
    • time-based expiration of entries, measured since last access or last write *
    • keys automatically wrapped in {@code WeakReference} *
    • values automatically wrapped in {@code WeakReference} or {@code SoftReference} *
    • notification of evicted (or otherwise removed) entries *
    • accumulation of cache access statistics *
    * /
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    IpLimiterAspect.java

    import com.fgbg.demo.utils.RequestUtils;
    import com.google.common.cache.Cache;
    import com.google.common.cache.CacheBuilder;
    import com.google.common.util.concurrent.RateLimiter;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 限制每个ip对同一个接口的访问频率
     */
    @Component
    @Aspect
    @Slf4j
    @RestController
    public class IpLimiterAspect {
        @Autowired
        private RequestUtils requestUtils;
    
        // 每秒生成1个令牌, 同个ip访问同个接口的QPS为1
        private final double PERMIT_PER_SECOND = 1;
    
        // 创建本地缓存
        private final Cache<String, RateLimiter> limiterCache = CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build();
    
        @Around("execution(* com.fgbg.demo.controller..*.*(..))")
        public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            // 构造key
            Signature signature = proceedingJoinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            String methodName = proceedingJoinPoint.getTarget().getClass().getName() + "." + methodSignature.getName();
            String key = requestUtils.getCurrentIp() + "->" + methodName;
    
            // 获取key对应的RateLimiter
            RateLimiter rateLimiter = limiterCache.get(key, () -> RateLimiter.create(PERMIT_PER_SECOND));
    
            if (! rateLimiter.tryAcquire()) {
                // 如果不能立刻获取令牌, 说明访问速度大于1 次/s, 触发限流
                log.warn("访问过快, 触发限流");
                throw new RuntimeException("访问过快, 触发限流");
            }
            log.info("接口放行...");
            return proceedingJoinPoint.proceed();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    RequestUtils.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    
    @Component
    public class RequestUtils {
    
        @Autowired
        private HttpServletRequest httpServletRequest;
    
        public String getCurrentIp() {
            return httpServletRequest.getHeader("X-Real-IP");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4. 接口测试

    接口测试这块就比较随意了,笔者这里采用apifox进行接口测试。因为AOP逻辑是增强所有接口,因此这里选择了项目曾经暴露出的一个查询接口。点击运行,即可开始测试
    在这里插入图片描述

    5. 测试结果

    在这里插入图片描述
    2.6s,分别在0,1,2s开始时,允许接口访问。10个请求中通过3个,失败7个,QPS = 1,限流成功

    在这里插入图片描述
    测试量达到40,QPS维持1,说明代码逻辑基本没有问题,Google yyds

  • 相关阅读:
    原生Javascript(数组操作方法总结)-更新
    【Java】泛型 之 使用泛型
    9月10日OpenCV学习笔记——Mask、彩色直方图、人脸检测
    .Net平台
    浅谈选择示波器时的“5倍法则”
    Layui + Flask | 基础使用(样式篇)(02)
    实际电容的频率特性
    java大学校园失物招领管理系统
    fastapi实现websocket
    荐书丨《实验心理学指导手册》:如何快速掌握一篇文献中的实验范式
  • 原文地址:https://blog.csdn.net/qq_62835094/article/details/137838740