限流实现之窗口计数

    技术2025-12-18  11

    该种实现方案采用注解的方式,可根据需要灵活配置,但是缺点也很明显,不能均匀限流,配置不当的话,短时间可能会有大量请求涌入后端,具体效果也在摸索中,发现不足请多交流,好了,废话少说,放码过来!

    环境:springboot redis

    <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

    redis工具类

    package com.huobao.c.buyer.util; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.concurrent.TimeUnit; /** * Created by lemon on 2019-01-16. */ @Slf4j @Component public class RedisUtils { @Autowired private RedisTemplate redisTemplate; /** * 向 redis里面 放入值 * @param key * @param value * @return */ public Boolean setStrData(String key, Object value) { return setStrData(key, value, null); } /** * 向 redis里面设置 数据(可以指定 过期 时间) * @param key * @param value * @param seconds * @return */ public Boolean setStrData(String key, Object value, Long seconds) { try { ValueOperations<Serializable,Object> valueOperations = redisTemplate.opsForValue(); if (null != seconds) { valueOperations.set(key, value,seconds, TimeUnit.MILLISECONDS); }else{ valueOperations.set(key,value); } } catch (Exception e) { e.printStackTrace(); log.error("向redis添加数据异常",e); return false; } return true; } /** * 获取 数据 * @param key * @return */ public Object getData(String key) { try { ValueOperations<Serializable,Object> valueOperations = redisTemplate.opsForValue(); return valueOperations.get(key); } catch (Exception e) { e.printStackTrace(); log.error("redis获取数据异常",e); return null; } } /** * 替换指定偏移量的值 * @param key * @param value * @param offset 偏移量 * @return */ public void offset(String key,Object value,Long offset){ try { ValueOperations<Serializable,Object> valueOperations = redisTemplate.opsForValue(); valueOperations.set(key,value,offset); } catch (Exception e) { e.printStackTrace(); log.error("redis替换指定偏移异常",e); } } }

     

    自定义限流注解

    package com.huobao.c.buyer.config; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * @author lemon * @Description: 自定义访问限流注解 * @date 2020/7/1 10:35 */ @Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { /** * 时间间隔 单位秒 * @return */ int seconds()default 2; /** * 访问次数 * @return */ int maxCount()default 2; /** * 是否需要登录 * @return */ boolean needLogin()default true; }

    定义拦截器

    拦截器里需要登陆并且限流的接口,放的key是uri+用户标识,我这边放的是userId(过滤器已经把token转换为userId放入请求参数里),也可放入ip等,不建议直接放token,因为key长度太长的话,影响效率

    package com.huobao.c.buyer.interceptors; import com.huobao.c.buyer.config.AccessLimit; import com.huobao.c.buyer.util.RedisUtils; import com.huobao.common.exception.ValidationException; import com.huobao.common.util.StringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Created by lemon on 2019/6/3. */ @Slf4j @Configuration public class AccessLimitInterceptor extends HandlerInterceptorAdapter { @Autowired private RedisUtils redisUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; //获取方法中的注解,看是否有限流注解 AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class); if (accessLimit != null) { int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean login = accessLimit.needLogin(); String key = request.getRequestURI(); //如果需要登录 if (login) { String userId = request.getParameter("userId"); if (StringUtils.isNotEmpty(userId)) { key += "_" + userId; } } //从redis中获取用户访问的次数 Integer count = (Integer) redisUtils.getData(key); if (count == null) { //第一次访问 redisUtils.setStrData(key, 1, seconds * 1000L); } else if (count < maxCount) { //加1 count++; redisUtils.offset(key, count, 0L); } else { log.info("访问频繁拦截,访问地址:{}", key); throw new ValidationException("您的访问太频繁了,请稍后再试"); } } } } catch (ValidationException e) { throw e; } catch (Exception e) { log.info("访问限流异常:{}", e.getMessage()); } return true; } }

     上面代码中进行计数+1操作,我使用的是替换指定偏移量操作,不少博客说key放入数值,然后自增操作key,但是实际使用中,自增时候会报目标类型不是integer类型错误,这是因为spring  boot  帮我们注入的  redisTemplate  类,泛型里面只能写<String, String>、<Object, Object>,要想Integer类型需要手动配置redis conifg,我这里也算偷个懒,用了上面的方法。

    redisTemplate.opsForValue().set(key,1,3000l); redisTemplate.opsForValue().increment(key);

    配置拦截器

    package com.huobao.c.buyer.config; import com.huobao.c.buyer.interceptors.AccessLimitInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * Created by lemon on 2019/6/3. */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Autowired private AccessLimitInterceptor accessLimitInterceptor ; /** * 注册自定义的拦截器类 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 加入自定义拦截器,这里可以根据实际需要对各个url添加不同的拦截器 registry.addInterceptor(accessLimitInterceptor ).addPathPatterns("/**/**"); } }

     

    使用演示

    使用很简单,只需在需要限流的api加上注解和参数即可,也可使用默认参数

    @AccessLimit(seconds = 3,maxCount = 1,needLogin = true) @ApiOperation(value = "用户提现",notes = "用户提现",produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(value = "save_accountwithdraw",method = RequestMethod.POST) public Result saveAccountWithdraw(@ModelAttribute AccountWithDrawForm accountWithDrawForm){ Result.generateSuccess("提现成功") }

     

    Processed: 0.034, SQL: 9