51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

SpringBoot 如何进行接口限流?

作者:钟小嘿
原文链接: https://www.cnblogs.com/zys2019/p/17385568.html

1、序言 {#toc_h2_0}

联网系统通常都要面对大并发大流量的请求,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法对外提供服务。为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。

限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。

常见的限流算法有三种: {#toc_h3_1}

1)计数器限流 {#toc_h4_2}

计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。

如:使用 AomicInteger 来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。

2)漏桶算法 {#toc_h4_3}

漏桶算法思路很简单,我们把水比作是 请求 ,漏桶比作是 系统处理能力极限 ,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

3)令牌桶算法 {#toc_h4_4}

令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

系统会维护一个令牌( token )桶,以一个恒定的速度往桶里放入令牌( token ),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌( token ),当桶里没有令牌( token )可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

2、单机版 {#toc_h2_5}

对于单机版,直接使用Guava即可。

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效,实现步骤如下:

第一步:引入guava依赖包

                  <dependency> <groupid>com.google.guava</groupid> <artifactid>guava</artifactid> <version>30.1-jre</version> </dependency>                  

    <svg>
                      <use xlink:href="#vditor-icon-copy" />
                    

    </svg>
                  </span>



  
  
                
                  <dependency>
    <groupid>com.google.guava</groupid>
    <artifactid>guava</artifactid>
    <version>30.1-jre</version>
</dependency>

                </code>
              </pre>



 
第二步:给接口加上限流逻辑

 
      
        
                      @Slf4j @RestController @RequestMapping("/limit") public class LimitController { /** * 限流策略 : 1秒钟2个请求 */ private final RateLimiter limiter = RateLimiter.create(2.0); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @GetMapping("/test1") public String testLimiter() { //500毫秒内,没拿到令牌,就直接进入服务降级 boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf)); return "当前排队人数较多,请稍后再试!"; } log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf)); return "请求成功"; } }                  

        <svg>
                          <use xlink:href="#vditor-icon-copy" />
                        

        </svg>
                      </span>



      
        
                    
                      @Slf4j
    @RestController
    @RequestMapping("/limit")
    public class LimitController {
        /**
         * 限流策略 : 1秒钟2个请求
         */
        private final RateLimiter limiter = RateLimiter.create(2.0);

        private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        @GetMapping("/test1")
        public String testLimiter() {
            //500毫秒内,没拿到令牌,就直接进入服务降级
            boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);

            if (!tryAcquire) {
                log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf));
                return "当前排队人数较多,请稍后再试!";
            }

            log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf));
            return "请求成功";
        }




    }


                    </code>
                  </pre>



     
    第三步:体验效果

     
    通过访问测试地址: http://127.0.0.1:8080/limit/test1,反复刷新并观察后端日志

     
    ![](http://static.51tbox.com/static/2025-01-06/col/c96f1de8ea934d2b567a2db004a4b6af/331f0e7b18094577ba1075bd3ef1a08a.jpg)

     
    初体验后,就可以对其进行解耦了,对于这样的限流,全部写在controller中是冗余的,可以使用注解方式来优化代码:

     
    ①加入AOP依赖

     
          
              
                          <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-aop</artifactid> </dependency>                  

            <svg>
                              <use xlink:href="#vditor-icon-copy" />
                            

            </svg>
                          </span>



          
              
                        
                          <dependency>
          <groupid>org.springframework.boot</groupid>
          <artifactid>spring-boot-starter-aop</artifactid>
        </dependency>

                        </code>
                      </pre>



         
        ②自定义限流注解

         
              
                    
                              @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Limit { /** * 资源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的访问限制次数 */ double permitsPerSecond () ; /** * 获取令牌最大等待时间 */ long timeout(); /** * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒 */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; /** * 得不到令牌的提示语 */ String msg() default "系统繁忙,请稍后再试."; }                  

                <svg>
                                  <use xlink:href="#vditor-icon-copy" />
                                

                </svg>
                              </span>



              
                    
                            
                              @Retention(RetentionPolicy.RUNTIME)
            @Target({ElementType.METHOD})
            @Documented
            public @interface Limit {
                /**
                 * 资源的key,唯一
                 * 作用:不同的接口,不同的流量控制
                 */
                String key() default "";

                /**
                 * 最多的访问限制次数
                 */
                double permitsPerSecond () ;

                /**
                 * 获取令牌最大等待时间
                 */
                long timeout();

                /**
                 * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
                 */
                TimeUnit timeunit() default TimeUnit.MILLISECONDS;

                /**
                 * 得不到令牌的提示语
                 */
                String msg() default "系统繁忙,请稍后再试.";




            }


                            </code>
                          </pre>



             
            ③使用AOP切面拦截限流注解

             
                  
                          
                                  @Slf4j @Aspect @Component public class LimitAop { /** * 不同的接口,不同的流量控制 * map的key为 Limiter.key */ private final Map<string, ratelimiter=""> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.zxh.limit.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { //key作用:不同的接口,不同的流量控制 String key=limit.key(); RateLimiter rateLimiter = null; //验证缓存是否有命中key if (!limitMap.containsKey(key)) { // 创建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond()); } rateLimiter = limitMap.get(key); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回异常提示 if (!acquire) { log.debug("令牌桶={},获取令牌失败",key); this.responseFail(limit.msg()); return null; } } return joinPoint.proceed(); } /** * 直接向前端抛出异常 * @param msg 提示信息 */ private void responseFail(String msg) { HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter pr=response.getWriter(); pr.write(msg); } }                  

                    <svg>
                                      <use xlink:href="#vditor-icon-copy" />
                                    

                    </svg>
                                  </span>



                  
                          
                                
                                  @Slf4j
                @Aspect
                @Component
                public class LimitAop {
                    /**
                     * 不同的接口,不同的流量控制
                     * map的key为 Limiter.key
                     */
                    private final Map<string, ratelimiter=""> limitMap = Maps.newConcurrentMap();

                    @Around("@annotation(com.zxh.limit.Limit)")
                    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
                        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                        Method method = signature.getMethod();
                        //拿limit的注解
                        Limit limit = method.getAnnotation(Limit.class);
                        if (limit != null) {
                            //key作用:不同的接口,不同的流量控制
                            String key=limit.key();
                            RateLimiter rateLimiter = null;
                            //验证缓存是否有命中key
                            if (!limitMap.containsKey(key)) {
                                // 创建令牌桶
                                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                                limitMap.put(key, rateLimiter);
                                log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
                            }
                            rateLimiter = limitMap.get(key);
                            // 拿令牌
                            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
                            // 拿不到命令,直接返回异常提示
                            if (!acquire) {
                                log.debug("令牌桶={},获取令牌失败",key);
                                this.responseFail(limit.msg());
                                return null;
                            }
                        }
                        return joinPoint.proceed();
                    }

                    /**
                     * 直接向前端抛出异常
                     * @param msg 提示信息
                     */
                    private void responseFail(String msg)  {
                        HttpServletResponse response=((ServletRequestAttributes)         RequestContextHolder.getRequestAttributes()).getResponse();
                        response.setCharacterEncoding("UTF-8");
                        response.setHeader("Content-type", "text/html;charset=UTF-8");
                        PrintWriter pr=response.getWriter();
                        pr.write(msg);
                     }




                }


                                </code>
                              </pre>



                 
                ④给需要限流的接口加上注解

                 
                      
                                
                                      @Slf4j @RestController @RequestMapping("/limit") public class LimitController { @GetMapping("/test2") @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!") public String limit2() { log.info("令牌桶limit2获取令牌成功"); return "ok"; } @GetMapping("/test3") @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!") public String limit3() { log.info("令牌桶limit3获取令牌成功"); return "ok"; } }                  

                        <svg>
                                          <use xlink:href="#vditor-icon-copy" />
                                        

                        </svg>
                                      </span>



                      
                                
                                    
                                      @Slf4j
                    @RestController
                    @RequestMapping("/limit")
                    public class LimitController {

                        @GetMapping("/test2")
                        @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")
                        public String limit2() {
                            log.info("令牌桶limit2获取令牌成功");
                            return "ok";
                        }


                        @GetMapping("/test3")
                        @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!")
                        public String limit3() {
                            log.info("令牌桶limit3获取令牌成功");
                            return "ok";
                        }




                    }


                                    </code>
                                  </pre>



                     
                    ⑤体验效果

                     
                    通过访问测试地址: http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果

                     
                    正常响应是返回"ok",触发限流时显示"系统繁忙,请稍后再试!"

                     
                    3、分布式版 {#toc_h2_6}
                    ------------------


                     
                    上述单机版并不能解决分布式的问题,那么分布式限流可以采用 ` Redis + Lua ` 实现。

                     
                    第一步:引入Redis依赖包

                     
                          
                                      
                                          <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>                  

                            <svg>
                                              <use xlink:href="#vditor-icon-copy" />
                                            

                            </svg>
                                          </span>



                          
                                      
                                        
                                          <dependency>  
                            <groupId>org.springframework.boot</groupId>  
                            <artifactId>spring-boot-starter-data-redis</artifactId>  
                        </dependency>

                                        </code>
                                      </pre>



                         
                        第二步:配置Redis

                         
                              
                                            
                                              @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } }                  

                                <svg>
                                                  <use xlink:href="#vditor-icon-copy" />
                                                

                                </svg>
                                              </span>



                              
                                            
                                            
                                              @Configuration
                            public class RedisConfig {

                                @Bean
                                public RedisTemplate&lt;String, Object&gt; redisTemplate(RedisConnectionFactory factory) {
                                    RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;&gt;();
                                    template.setConnectionFactory(factory);

                                    // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
                                    Jackson2JsonRedisSerializer&lt;Object&gt; serializer = new Jackson2JsonRedisSerializer&lt;&gt;(Object.class);
                                    ObjectMapper mapper = new ObjectMapper();
                                    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
                                    mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                                            ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
                                    serializer.setObjectMapper(mapper);

                                    template.setValueSerializer(serializer);
                                    // 使用StringRedisSerializer来序列化和反序列化redis的key值
                                    template.setKeySerializer(new StringRedisSerializer());
                                    template.afterPropertiesSet();

                                    return template;
                                }




                            }


                                            </code>
                                          </pre>



                             
                            第三步:自定义限流注解

                             
                                  
                                                  
                                                  @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface RedisLimit { /** * 资源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的访问限制次数 */ long permitsPerSecond() default 2; /** * 过期时间也可以理解为单位时间,单位秒,默认60 */ long expire() default 60; /** * 得不到令牌的提示语 */ String msg() default "系统繁忙,请稍后再试."; }                  

                                    <svg>
                                                      <use xlink:href="#vditor-icon-copy" />
                                                    

                                    </svg>
                                                  </span>



                                  
                                                  
                                                
                                                  @Retention(RetentionPolicy.RUNTIME)
                                @Target({ElementType.METHOD})
                                @Documented
                                public @interface RedisLimit {
                                    /**
                                     * 资源的key,唯一
                                     * 作用:不同的接口,不同的流量控制
                                     */
                                    String key() default "";

                                    /**
                                     * 最多的访问限制次数
                                     */
                                    long permitsPerSecond() default 2;

                                    /**
                                     * 过期时间也可以理解为单位时间,单位秒,默认60
                                     */
                                    long expire() default 60;


                                    /**
                                     * 得不到令牌的提示语
                                     */
                                    String msg() default "系统繁忙,请稍后再试.";




                                }


                                                </code>
                                              </pre>



                                 
                                第四步:创建限流异常(代码中并未体现异常拦截的代码,开发时根据时间情况进行拦截并处理即可)

                                 
                                      
                                                        
                                                      public class RedisLimitException extends RuntimeException{ public RedisLimitException(String msg) { super( msg ); } }                  

                                        <svg>
                                                          <use xlink:href="#vditor-icon-copy" />
                                                        

                                        </svg>
                                                      </span>



                                      
                                                        
                                                    
                                                      public class RedisLimitException extends RuntimeException{
                                        public RedisLimitException(String msg) {
                                            super( msg );
                                        }
                                    }

                                                    </code>
                                                  </pre>



                                     
                                    第五步:在resouces文件夹下创建lua文件 rateLimiter.lua

                                     
                                          
                                                              
                                                          --获取KEY local key = KEYS[1] local limit = tonumber(ARGV[1]) local curentLimit = tonumber(redis.call('get', key) or "0") if curentLimit + 1 > limit then return 0 else -- 自增长 1 redis.call('INCRBY', key, 1) -- 设置过期时间 redis.call('EXPIRE', key, ARGV[2]) return curentLimit + 1 end                  

                                            <svg>
                                                              <use xlink:href="#vditor-icon-copy" />
                                                            

                                            </svg>
                                                          </span>



                                          
                                                              
                                                        
                                                          --获取KEY
                                        local key = KEYS[1]

                                        local limit = tonumber(ARGV\[1\])


                                        local curentLimit = tonumber(redis.call('get', key) or "0")


                                        if curentLimit + 1 \> limit
                                        then return 0
                                        else
                                        -- 自增长 1
                                        redis.call('INCRBY', key, 1)
                                        -- 设置过期时间
                                        redis.call('EXPIRE', key, ARGV\[2\])
                                        return curentLimit + 1
                                        end


                                                        </code>
                                                      </pre>



                                         
                                        第六步:使用AOP切面拦截限流注解

                                         
                                              
                                                                    
                                                              @Slf4j @Aspect @Component public class RedisLimitAop { @Autowired private StringRedisTemplate stringRedisTemplate; private DefaultRedisScript<Long> redisScript; @PostConstruct public void init(){ redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Long.class); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua"))); } @Pointcut("@annotation(com.zxh.limit.redis.RedisLimit)") private void check() { } @Before("check()") public void before(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿到RedisLimit注解,如果存在则说明需要限流 RedisLimit redisLimit = method.getAnnotation(RedisLimit.class); if(redisLimit != null){ //获取redis的key String key = redisLimit.key(); String className = method.getDeclaringClass().getName(); String name = method.getName(); String limitKey = key + className + method.getName(); log.info(limitKey); if(StringUtils.isEmpty(key)){ throw new RedisLimitException( "key cannot be null" ); } long limit = redisLimit.permitsPerSecond(); long expire = redisLimit.expire(); List<String> keys = new ArrayList<>(); keys.add( key ); String luaScript = buildLuaScript(); RedisScript<Long> redisScript = new DefaultRedisScript<>( luaScript, Long.class ); Long count = stringRedisTemplate.execute( redisScript, keys, String.valueOf(limit), String.valueOf(expire) ); log.info( "Access try count is {} for key={}", count, key ); if (count != null && count == 0) { log.debug("令牌桶={},获取令牌失败",key); throw new RedisLimitException(redisLimit.msg()); } } } }                  

                                                <svg>
                                                                  <use xlink:href="#vditor-icon-copy" />
                                                                

                                                </svg>
                                                              </span>



                                              
                                                                    
                                                            
                                                              @Slf4j
                                            @Aspect
                                            @Component
                                            public class RedisLimitAop {

                                                @Autowired
                                                private StringRedisTemplate stringRedisTemplate;

                                                private DefaultRedisScript&lt;Long&gt; redisScript; 

                                                @PostConstruct
                                                public void init(){
                                                    redisScript = new DefaultRedisScript&lt;&gt;();
                                                    redisScript.setResultType(Long.class);
                                                    redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
                                                }
                                                @Pointcut("@annotation(com.zxh.limit.redis.RedisLimit)")
                                                private void check() {

                                                }

                                                @Before("check()")
                                                public void before(JoinPoint joinPoint) {
                                                    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                                                    Method method = signature.getMethod();

                                                    //拿到RedisLimit注解,如果存在则说明需要限流
                                                    RedisLimit redisLimit = method.getAnnotation(RedisLimit.class);

                                                    if(redisLimit != null){
                                                        //获取redis的key
                                                        String key  = redisLimit.key();
                                                        String className = method.getDeclaringClass().getName();
                                                        String name = method.getName();

                                                        String limitKey = key + className + method.getName();

                                                        log.info(limitKey);

                                                        if(StringUtils.isEmpty(key)){
                                                            throw new RedisLimitException( "key cannot be null" );
                                                        }

                                                        long limit = redisLimit.permitsPerSecond();

                                                        long expire = redisLimit.expire();

                                                        List&lt;String&gt; keys = new ArrayList&lt;&gt;();
                                                        keys.add( key );
                                                        String luaScript = buildLuaScript();
                                                        RedisScript&lt;Long&gt; redisScript = new DefaultRedisScript&lt;&gt;( luaScript, Long.class );

                                                        Long count = stringRedisTemplate.execute( redisScript, keys, String.valueOf(limit), String.valueOf(expire) );

                                                        log.info( "Access try count is {} for key={}", count, key );

                                                        if (count != null &amp;&amp; count == 0) {
                                                            log.debug("令牌桶={},获取令牌失败",key);
                                                            throw new RedisLimitException(redisLimit.msg());
                                                        }
                                                    }

                                                }




                                            }


                                                            </code>
                                                          </pre>



                                             
                                            第七步:给需要限流的接口加上注解

                                             
                                                  
                                                                          
                                                                  @Slf4j @RestController @RequestMapping("/limit/redis") public class LimitRedisController { /** * 基于Redis AOP限流 */ @GetMapping("/test") @RedisLimit(key = "redis-limit:test", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!") public String test() { log.info("限流成功。。。"); return "ok"; } }                  

                                                    <svg>
                                                                      <use xlink:href="#vditor-icon-copy" />
                                                                    

                                                    </svg>
                                                                  </span>



                                                  
                                                                          
                                                                
                                                                  @Slf4j
                                                @RestController
                                                @RequestMapping("/limit/redis")
                                                public class LimitRedisController {

                                                    /**
                                                     * 基于Redis AOP限流
                                                     */
                                                    @GetMapping("/test")
                                                    @RedisLimit(key = "redis-limit:test", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!")
                                                    public String test() {
                                                        log.info("限流成功。。。");
                                                        return "ok";
                                                    }




                                                }


                                                                </code>
                                                              </pre>



                                                 
                                                第八步:体验效果

                                                 
                                                通过访问测试地址: http://127.0.0.1:8080/limit/redis/test,反复刷新并观察输出结果

                                                 
                                                正常响应是返回"ok",触发限流时显示"当前排队人数较多,请稍后再试!"(上述代码未体现异常拦截处理,这里会出现500的错误)

                                                 
                                                4、整合版 {#toc_h2_7}
                                                -----------------


                                                 
                                                当然,每个项目都加入这个代码也是有些繁杂的,为了可以方便的进行切换两种场景,可以将其封装为starter,然后按需引入即可。

                                                 
                                                ### 第1步、创建通用模块cloud-limiter-starter {#toc_h3_8}


                                                 
                                                首先在父项目下创建一个模块

                                                 
                                                ![](http://static.51tbox.com/static/2025-01-06/col/c96f1de8ea934d2b567a2db004a4b6af/1dfd7d31bda54389bf399942b3aeba1d.jpg)

                                                 
                                                然后在pom文件中引入相关依赖

                                                 
                                                      
                                                                                
                                                                      <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <!--SpringFramework--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <scope>provided</scope> </dependency> </dependencies>                  

                                                        <svg>
                                                                          <use xlink:href="#vditor-icon-copy" />
                                                                        

                                                        </svg>
                                                                      </span>



                                                      
                                                                                
                                                                    
                                                                      <dependencies>
                                                      <dependency>
                                                        <groupId>com.google.guava</groupId>
                                                        <artifactId>guava</artifactId>
                                                      </dependency>
                                                      <!--SpringFramework-->
                                                      <dependency>
                                                        <groupId>org.springframework.boot</groupId>
                                                        <artifactId>spring-boot-starter-web</artifactId>
                                                        <scope>provided</scope>
                                                      </dependency>
                                                      <dependency>
                                                        <groupId>org.springframework.boot</groupId>
                                                        <artifactId>spring-boot-starter-data-redis</artifactId>
                                                        <scope>provided</scope>
                                                      </dependency>
                                                      <dependency>
                                                        <groupId>org.projectlombok</groupId>
                                                        <artifactId>lombok</artifactId>
                                                        <scope>provided</scope>
                                                      </dependency>
                                                      <dependency>
                                                        <groupId>org.springframework</groupId>
                                                        <artifactId>spring-aspects</artifactId>
                                                        <scope>provided</scope>
                                                      </dependency>

                                                    \</dependencies\>


                                                                    </code>
                                                                  </pre>



                                                     
                                                    ### 第2步、实现限流功能 {#toc_h3_9}


                                                     
                                                    ①创建限流接口

                                                     
                                                    既然有两种限流机制,按照套路肯定得先创建一个限流接口,就叫 ` LimiterManager ` 吧。

                                                     
                                                          
                                                                                      
                                                                          public interface LimiterManager { boolean tryAccess(Limiter limiter); }                  

                                                            <svg>
                                                                              <use xlink:href="#vditor-icon-copy" />
                                                                            

                                                            </svg>
                                                                          </span>



                                                          
                                                                                      
                                                                        
                                                                          public interface LimiterManager {
                                                            boolean tryAccess(Limiter limiter);
                                                        }

                                                                        </code>
                                                                      </pre>



                                                         
                                                        ②分别实现Redis的限流功能和Guava的限流功能,这里只给出核心代码。

                                                         
                                                        Guava限流的核心实现GuavaLimiter

                                                         
                                                              
                                                                                          
                                                                              @Slf4j public class GuavaLimiter implements LimiterManager{ private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap(); @Override public boolean tryAccess(Limiter limiter) { RateLimiter rateLimiter = getRateLimiter(limiter); if (rateLimiter == null) { return false; } boolean access = rateLimiter.tryAcquire(1,100, TimeUnit.MILLISECONDS); log.info("{} access :{}",limiter.getKey() , access); return access; } }                  

                                                                <svg>
                                                                                  <use xlink:href="#vditor-icon-copy" />
                                                                                

                                                                </svg>
                                                                              </span>



                                                              
                                                                                          
                                                                            
                                                                              @Slf4j
                                                            public class GuavaLimiter implements LimiterManager{
                                                                private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap();

                                                                @Override
                                                                public boolean tryAccess(Limiter limiter) {
                                                                    RateLimiter rateLimiter = getRateLimiter(limiter);
                                                                    if (rateLimiter == null) {
                                                                        return false;
                                                                    }

                                                                    boolean access = rateLimiter.tryAcquire(1,100, TimeUnit.MILLISECONDS);

                                                                    log.info("{} access :{}",limiter.getKey() , access);

                                                                    return access;
                                                                }




                                                            }


                                                                            </code>
                                                                          </pre>



                                                             
                                                            Redis限流的核心实现RedisLimiter

                                                             
                                                                  
                                                                                              
                                                                                  @Slf4j public class RedisLimiter implements LimiterManager{ private final StringRedisTemplate stringRedisTemplate; public RedisLimiter(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryAccess(Limiter limiter) { String key = limiter.getKey(); if (StringUtils.isEmpty(key)) { throw new LimiterException( "redis limiter key cannot be null" ); } List<String> keys = new ArrayList<>(); keys.add( key ); int seconds = limiter.getSeconds(); int limitCount = limiter.getLimitNum(); String luaScript = buildLuaScript(); RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); Long count = stringRedisTemplate.execute( redisScript, keys, "" + limitCount, "" + seconds ); log.info( "Access try count is {} for key={}", count, key ); return count != null && count != 0; } }                  

                                                                    <svg>
                                                                                      <use xlink:href="#vditor-icon-copy" />
                                                                                    

                                                                    </svg>
                                                                                  </span>



                                                                  
                                                                                              
                                                                                
                                                                                  @Slf4j
                                                                public class RedisLimiter implements LimiterManager{

                                                                    private final StringRedisTemplate stringRedisTemplate;

                                                                    public RedisLimiter(StringRedisTemplate stringRedisTemplate) {
                                                                        this.stringRedisTemplate = stringRedisTemplate;
                                                                    }

                                                                    @Override
                                                                    public boolean tryAccess(Limiter limiter) {

                                                                        String key = limiter.getKey();
                                                                        if (StringUtils.isEmpty(key)) {
                                                                            throw new LimiterException( "redis limiter key cannot be null" );
                                                                        }

                                                                        List&lt;String&gt; keys = new ArrayList&lt;&gt;();
                                                                        keys.add( key );

                                                                        int seconds = limiter.getSeconds();
                                                                        int limitCount = limiter.getLimitNum();

                                                                        String luaScript = buildLuaScript();

                                                                        RedisScript&lt;Long&gt; redisScript = new DefaultRedisScript&lt;&gt;(luaScript, Long.class);

                                                                        Long count = stringRedisTemplate.execute( redisScript, keys, "" + limitCount, "" + seconds );

                                                                        log.info( "Access try count is {} for key={}", count, key );

                                                                        return count != null &amp;&amp; count != 0;
                                                                    }




                                                                }


                                                                                </code>
                                                                              </pre>



                                                                 
                                                                ### 第3步、创建配置类 {#toc_h3_10}


                                                                 
                                                                编写配置类根据配置文件注入限流实现类,当配置文件中属性 ` limit.type=local ` 时启用Guava限流机制,当 ` limit.type=redis ` 时启用Redis限流机制。

                                                                 
                                                                      
                                                                                                  
                                                                                      @Configuration public class LimiterConfigure { @Bean @ConditionalOnProperty(name = "limit.type",havingValue = "local") public LimiterManager guavaLimiter(){ return new GuavaLimiter(); } @Bean @ConditionalOnProperty(name = "limit.type",havingValue = "redis") public LimiterManager redisLimiter(StringRedisTemplate stringRedisTemplate){ return new RedisLimiter(stringRedisTemplate); } }                  

                                                                        <svg>
                                                                                          <use xlink:href="#vditor-icon-copy" />
                                                                                        

                                                                        </svg>
                                                                                      </span>



                                                                      
                                                                                                  
                                                                                    
                                                                                      @Configuration
                                                                    public class LimiterConfigure {

                                                                        @Bean
                                                                        @ConditionalOnProperty(name = "limit.type",havingValue = "local")
                                                                        public LimiterManager guavaLimiter(){
                                                                            return new GuavaLimiter();
                                                                        }


                                                                        @Bean
                                                                        @ConditionalOnProperty(name = "limit.type",havingValue = "redis")
                                                                        public LimiterManager redisLimiter(StringRedisTemplate stringRedisTemplate){
                                                                            return new RedisLimiter(stringRedisTemplate);
                                                                        }




                                                                    }


                                                                                    </code>
                                                                                  </pre>



                                                                     
                                                                    ### 第4步、创建AOP {#toc_h3_11}


                                                                     
                                                                    创建一个AOP进行切面拦截

                                                                     
                                                                          
                                                                                                      
                                                                                          @Aspect @EnableAspectJAutoProxy(proxyTargetClass = true) //使用CGLIB代理 @Conditional(LimitAspectCondition.class) public class LimitAspect { @Setter(onMethod_ = @Autowired) private LimiterManager limiterManager; @Pointcut("@annotation(com.zxh.limit.aop.Limit)") private void check() { } @Before("check()") public void before(JoinPoint joinPoint){ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Limit limit = method.getAnnotation(Limit.class); if(limit != null){ Limiter limiter = Limiter.builder().limitNum(limit.limitNum()) .seconds(limit.seconds()) .key(limit.key()).build(); if(!limiterManager.tryAccess(limiter)){ throw new LimiterException( "There are currently many people , please try again later!" ); } } } }                  

                                                                            <svg>
                                                                                              <use xlink:href="#vditor-icon-copy" />
                                                                                            

                                                                            </svg>
                                                                                          </span>



                                                                          
                                                                                                      
                                                                                        
                                                                                          @Aspect
                                                                        @EnableAspectJAutoProxy(proxyTargetClass = true) //使用CGLIB代理
                                                                        @Conditional(LimitAspectCondition.class)
                                                                        public class LimitAspect {

                                                                            @Setter(onMethod_ = @Autowired)
                                                                            private LimiterManager limiterManager;

                                                                            @Pointcut("@annotation(com.zxh.limit.aop.Limit)")
                                                                            private void check() {

                                                                            }

                                                                            @Before("check()")
                                                                            public void before(JoinPoint joinPoint){
                                                                                MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                                                                                Method method = signature.getMethod();

                                                                                Limit limit = method.getAnnotation(Limit.class);
                                                                                if(limit != null){

                                                                                    Limiter limiter = Limiter.builder().limitNum(limit.limitNum())
                                                                                            .seconds(limit.seconds())
                                                                                            .key(limit.key()).build();

                                                                                    if(!limiterManager.tryAccess(limiter)){
                                                                                        throw new LimiterException( "There are currently many people , please try again later!" );
                                                                                    }
                                                                                }
                                                                            }




                                                                        }


                                                                                        </code>
                                                                                      </pre>



                                                                         
                                                                        这里使用自定义条件选择器

                                                                         
                                                                              
                                                                                                          
                                                                                              public class LimitAspectCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { //检查配置文件是否包含limit.type属性 return conditionContext.getEnvironment().containsProperty(ConfigConstant.LIMIT_TYPE); } }                  

                                                                                <svg>
                                                                                                  <use xlink:href="#vditor-icon-copy" />
                                                                                                

                                                                                </svg>
                                                                                              </span>



                                                                              
                                                                                                          
                                                                                            
                                                                                              public class LimitAspectCondition implements Condition {
                                                                                @Override
                                                                                public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
                                                                                    //检查配置文件是否包含limit.type属性
                                                                                    return conditionContext.getEnvironment().containsProperty(ConfigConstant.LIMIT_TYPE);
                                                                                }
                                                                            }

                                                                                            </code>
                                                                                          </pre>



                                                                             
                                                                            ### 第5步、创建spring.factories文件,引导SpringBoot加载配置类 {#toc_h3_12}


                                                                             
                                                                                  
                                                                                                              
                                                                                                  ## AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.zxh.limit.config.LimiterConfigure,\ com.zxh.limit.aop.LimitAspect                  

                                                                                    <svg>
                                                                                                      <use xlink:href="#vditor-icon-copy" />
                                                                                                    

                                                                                    </svg>
                                                                                                  </span>



                                                                                  
                                                                                                              
                                                                                                
                                                                                                  ## AutoConfiguration
                                                                                org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
                                                                                  com.zxh.limit.config.LimiterConfigure,\
                                                                                  com.zxh.limit.aop.LimitAspect

                                                                                                </code>
                                                                                              </pre>



                                                                                 
                                                                                完整目录结构如下:

                                                                                 
                                                                                ![](http://static.51tbox.com/static/2025-01-06/col/c96f1de8ea934d2b567a2db004a4b6af/3baba077bc4a4faaa82baad2ea88e3f2.jpg)

                                                                                 
                                                                                ### 第6步、在项目中引用限流组件 {#toc_h3_13}


                                                                                 
                                                                                ①引入依赖

                                                                                 
                                                                                      
                                                                                                                  
                                                                                                      <dependency> <groupId>com.zxh</groupId> <artifactId>cloud-limit-starter</artifactId> </dependency>                  

                                                                                        <svg>
                                                                                                          <use xlink:href="#vditor-icon-copy" />
                                                                                                        

                                                                                        </svg>
                                                                                                      </span>



                                                                                      
                                                                                                                  
                                                                                                    
                                                                                                      <dependency>
                                                                                        <groupId>com.zxh</groupId>
                                                                                        <artifactId>cloud-limit-starter</artifactId>
                                                                                    </dependency>

                                                                                                    </code>
                                                                                                  </pre>



                                                                                     
                                                                                    ②在 ` application.properties ` 中设置加载的限流组件

                                                                                     
                                                                                          
                                                                                                                      
                                                                                                          limit.type = redis                  

                                                                                            <svg>
                                                                                                              <use xlink:href="#vditor-icon-copy" />
                                                                                                            

                                                                                            </svg>
                                                                                                          </span>



                                                                                          
                                                                                                                      
                                                                                                        
                                                                                                          limit.type = redis

                                                                                                        </code>
                                                                                                      </pre>



                                                                                         
                                                                                        如果不配置此属性则不加载对应限流功能。

                                                                                         
                                                                                        ③在需要限流的接口上加上注解

                                                                                         
                                                                                              
                                                                                                                          
                                                                                                              @Limit(key = "Limiter:test",limitNum = 3,seconds = 1)                  

                                                                                                <svg>
                                                                                                                  <use xlink:href="#vditor-icon-copy" />
                                                                                                                

                                                                                                </svg>
                                                                                                              </span>



                                                                                              
                                                                                                                          
                                                                                                            
                                                                                                              @Limit(key = "Limiter:test",limitNum = 3,seconds = 1)

                                                                                                            </code>
                                                                                                          </pre>



                                                                                             
                                                                                            对于SpringBoot自定义的starter是非常有用的,毕竟写代码的思想就是减少冗余,提高效率,便于维护。

                                                                                             
                                                                                            参考: {#toc_h2_14}
                                                                                            ----------------


                                                                                             
                                                                                            https://www.cnblogs.com/jianzh5/p/17291540.html及其系列文章

                                                                                             
                                                                                                                          

                                                                                              
                                                                                                                          
                                                                                            *** ** * ** ***

                                                                                             标题:SpringBoot 如何进行接口限流? 
                                                                                             作者: https://51tbox.com/ 
                                                                                             地址: / 

                                                                                              
                                                                                                                          
                                                                                            如未加特殊说明,文章均为原创,转载必须注明出处。均采用 https://creativecommons.org/licenses/by-sa/4.0/deed.zh-Hans !
                                                                                              本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!文章观点不代表本网站立场,如需处理请联系首页客服。  
                                                                                              • 网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。  
                                                                                              • 公众号转载请联系网站首页的微信号申请白名单!  

                                                                                              
                                                                                                                          
                                                                                            *** ** * ** ***

                                                                                              
                                                                                             ![]() 

赞(0)
未经允许不得转载:工具盒子 » SpringBoot 如何进行接口限流?