美文网首页
使用Redis实现限流

使用Redis实现限流

作者: felixfeijs | 来源:发表于2021-04-25 15:59 被阅读0次

使用Redis实现限流

  • 原理:使用Redis的Hash数据结构,把对应的key、接口url、限流规则存放进redis,在拦截器中对接口进行拦截,获取到有配置的url,进行规则获取,获取到规则之后,对redis对应接口对应key进行increment自增的操作(increment 指令是线程安全的,不用担心并发的问题),如果是第一次的话,设置该key的过期时间,过期时间为配置时间,单位为配置单位,下次调用的时候,如果当前接口对应key的自增数大于配置的limit数则进行请求超出限制的提示。

具体步骤

  1. 引入Redis依赖包,和其他工具包
        <!-- 阿里json -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.41</version>
        </dependency>
                <!-- 整合Redis start -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 整合Redis end -->
  1. 在yml文件中编写限流接口和限流规则
request_limit:
  # 限流的接口
  url: /utils/redis,/demo/user/account
  rules:
    # 限流规则,每秒3次调用
    limit: 3
    time: 1
    timeUnit: SECONDS
  1. 编写限流配置类
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 + "]";
    }
}
  1. 继承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);
    }
}
  1. 实现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();
    }
}
  1. 编写服务启动时注入限流接口和规则
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("限流规则配置错误,请使用逗号分割");
        }
    }
}
  1. Controller接口写入配置的路径即可
  2. 进行请求,快速刷新浏览器,结果如图

相关文章

网友评论

      本文标题:使用Redis实现限流

      本文链接:https://www.haomeiwen.com/subject/fteerltx.html