# 使用Redis实现限流 **Repository Path**: fpfgitmy_admin/redis-interface-limit ## Basic Information - **Project Name**: 使用Redis实现限流 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-04-28 - **Last Updated**: 2021-04-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 使用Redis实现限流 + 原理:使用Redis的Hash数据结构,把对应的key、接口url、限流规则存放进redis,在拦截器中对接口进行拦截,获取到有配置的url,进行规则获取,获取到规则之后,对redis对应接口对应key进行increment自增的操作(increment 指令是线程安全的,不用担心并发的问题),如果是第一次的话,设置该key的过期时间,过期时间为配置时间,单位为配置单位,下次调用的时候,如果当前接口对应key的自增数大于配置的limit数则进行请求超出限制的提示。 + #### 具体步骤 1. 引入Redis依赖包,和其他工具包 ``` com.alibaba fastjson 1.2.41 org.springframework.boot spring-boot-starter-data-redis ``` 2. 在yml文件中编写限流接口和限流规则 ``` request_limit: # 限流的接口 url: /utils/redis,/demo/user/account rules: # 限流规则,每秒3次调用 limit: 3 time: 1 timeUnit: SECONDS ``` 3. 编写限流配置类 ``` public class RequestLimitConfig implements Serializable { private static final long serialVersionUID = 1101875328323558092L; // 最大请求次数 private long limit; // 时间 private long time; // 时间单位 private TimeUnit timeUnit; public RequestLimitConfig() { super(); } public RequestLimitConfig(long limit, long time, TimeUnit timeUnit) { super(); this.limit = limit; this.time = time; this.timeUnit = timeUnit; } public long getLimit() { return limit; } public void setLimit(long limit) { this.limit = limit; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } public TimeUnit getTimeUnit() { return timeUnit; } public void setTimeUnit(TimeUnit timeUnit) { this.timeUnit = timeUnit; } @Override public String toString() { return "RequestLimitConfig [limit=" + limit + ", time=" + time + ", timeUnit=" + timeUnit + "]"; } } ``` 4. 继承`HandlerInterceptorAdapter`类,实现其方法编写限流拦截器 ``` import com.alibaba.fastjson.JSONObject; import com.example.demo.config.RequestLimitConfig; import com.example.demo.constants.GlobalConstants; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; import java.nio.charset.StandardCharsets; /** * 接口限流拦截器 */ public class RequestLimitInterceptor extends HandlerInterceptorAdapter { private static final Logger log = LoggerFactory.getLogger(RequestLimitInterceptor.class); @Autowired private RedisTemplate redisTemplate; /** * 在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /** * 获取到请求的URI */ String contentPath = request.getContextPath(); String uri = request.getRequestURI(); if (!StringUtils.isEmpty(contentPath) && !contentPath.equals("/")) { uri = uri.substring(uri.indexOf(contentPath) + contentPath.length()); } log.info("uri={}", uri); /** * 尝试从hash中读取得到当前接口的限流配置 */ String str = this.redisTemplate.opsForHash().get(GlobalConstants.REQUEST_LIMIT_CONFIG, uri).toString(); RequestLimitConfig requestLimitConfig = JSONObject.parseObject(str, RequestLimitConfig.class); if (requestLimitConfig == null) { log.info("该uri={}没有限流配置", uri); return true; } String limitKey = GlobalConstants.REQUEST_LIMIT + ":" + uri; /** * 当前接口的访问次数 +1 increment 指令是线程安全的,不用担心并发的问题 */ long count = this.redisTemplate.opsForValue().increment(limitKey); if (count == 1) { /** * 第一次请求,设置key的过期时间 */ this.redisTemplate.expire(limitKey, requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit()); log.info("设置过期时间:time={}, timeUnit={}", requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit()); } log.info("请求限制。limit={}, count={}", requestLimitConfig.getLimit(), count); if (count > requestLimitConfig.getLimit()) { /** * 限定时间内,请求超出限制,响应客户端错误信息。 */ response.setContentType(MediaType.TEXT_PLAIN_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.getWriter().write("服务器繁忙,稍后再试"); return false; } return true; } /** * 在方法执行后调用(暂未使用) * * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { super.postHandle(request, response, handler, modelAndView); } } ``` 5. 实现`WebMvcConfigurer`接口,编写资源配置类 ``` /** * 资源配置器 */ @Configuration public class WebMvcConfiguration implements WebMvcConfigurer { @Value("${request_limit.url}") private String url; // 添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // 添加限流的接口 registry.addInterceptor(this.requestLimitInterceptor()) .addPathPatterns(url.split(",")); } @Bean public RequestLimitInterceptor requestLimitInterceptor() { return new RequestLimitInterceptor(); } } ``` 6. 编写服务启动时注入限流接口和规则 ``` import com.alibaba.fastjson.JSONObject; import com.example.demo.constants.GlobalConstants; import com.example.demo.exception.BusinessException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; /** * 初始化运行方法 */ @Component public class ApplicationRunnerImpl implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(ApplicationRunnerImpl.class); // 引入redis @Autowired private RedisTemplate redisTemplate; @Value("${request_limit.url}") private String url; @Value("${request_limit.rules.limit}") private String limit; @Value("${request_limit.rules.time}") private String time; @Value("${request_limit.rules.timeUnit}") private String timeUnit; @Override public void run(ApplicationArguments args) throws Exception { // 已经写好了方法 //TestCron.init(); JSONObject rulesJson = new JSONObject(); rulesJson.put("limit", limit); rulesJson.put("time", time); rulesJson.put("timeUnit", timeUnit); try { String[] urlArr = url.split(","); // 初始化在Redis中存入接口限流规则 for (String item : urlArr) { redisTemplate.opsForHash().put(GlobalConstants.REQUEST_LIMIT_CONFIG, item, rulesJson); log.info("Redis存放成功,接口地址:" + item + " 限流规则:" + " 时间:" + time + " 单位:" + timeUnit + " 次数:" + limit); } } catch (Exception e) { e.printStackTrace(); throw new BusinessException("限流规则配置错误,请使用逗号分割"); } } } ``` 7. Controller接口写入配置的路径即可 8. 进行请求,快速刷新浏览器,结果如图 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0428/152143_3a95288c_1942182.png "屏幕截图.png")