使用Redis实现限流

使用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. 进行请求,快速刷新浏览器,结果如图
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容