基于Spring的簡單分布式限流Filter

使用redis做滑動窗口

talk is cheap show me the code

代碼

限流配置RateLimitProperties

@Configuration
@ConfigurationProperties(prefix = "rateLimit")
@Data
public class RateLimitProperties {

    private boolean enabled;

    private List<RateLimitRule> rules;

    /**
     * 每 {timeOfSecond} 秒允許 {key} 命中 {url}正則 {capacity} 次珍语,超過直接返回 {responseBody}
     */
    @Data
    public static class RateLimitRule {
        /**
         * url支持正則, 會以該url作為限速基準(zhǔn)洲鸠。
         * 必填
         */
        private String url;
        /**
         * url的http method参歹,GET POST PUT DELETE 等
         * 為空攔截所有
         */
        private String method;
        /**
         * 被攔截后的響應(yīng)體
         */
        private String responseBody;
        /**
         * 從http header 或者h(yuǎn)ttp parameter中取相應(yīng)值做攔截基準(zhǔn)
         * 必填
         */
        private List<String> keys;
        /**
         * 必填
         */
        private Integer timeOfSecond;
        /**
         * 必填
         */
        private Integer capacity;
    }
}

限流Filter RateLimitFilter

@Component
@WebFilter(urlPatterns = "/*", filterName = "rateLimitFilter")
@Slf4j
public class RateLimitFilter implements Filter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RateLimitProperties rateLimitProperties;
    /**
     * 頻率控制腳本
     * 參數(shù):key,窗口長度(毫秒)损合,窗口容量,發(fā)送時(shí)間(時(shí)間戳:秒)
     * 返回:0:未超出頻率;1:超出頻率
     * 注意:緩存一周數(shù)據(jù)威彰,會出現(xiàn)某個(gè)時(shí)間段斷檔肝谭;允許最大次數(shù)需要>=1
     */
    private static final String RATE_LIMIT_LUA = "local nowSize = redis.call('LLEN', KEYS[1])\n" +
            "local window = tonumber(ARGV[1])\n" +
            "local maxSize = tonumber(ARGV[2])\n" +
            "local nowTime = tonumber(ARGV[3])\n" +
            "if nowSize < maxSize then\n" +
            "    redis.call('LPUSH', KEYS[1], nowTime)\n" +
            "    if nowSize == 0 then\n" +
            "        redis.call(\"EXPIRE\", KEYS[1], 86400)\n" +
            "    end\n" +
            "else\n" +
            "    local earliestTime = redis.call('LINDEX', KEYS[1], -1)\n" +
            "    if nowTime - earliestTime <= window then\n" +
            "        return 1\n" +
            "    else\n" +
            "        redis.call('LPUSH', KEYS[1], nowTime)\n" +
            "        redis.call('LTRIM', KEYS[1], 0, maxSize-1)\n" +
            "    end\n" +
            "end\n" +
            "return 0";
    /**
     * rateLimit:URL:KEY:VALUE
     */
    private static final String KEY_PREFIX = "rateLimit:%s:%s:%s";
    private DefaultRedisScript<Long> rateLimitLuaScript;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        rateLimitLuaScript = new DefaultRedisScript<>();
        rateLimitLuaScript.setResultType(Long.class);
        rateLimitLuaScript.setScriptText(RATE_LIMIT_LUA);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)
                || !rateLimitProperties.isEnabled()) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        try {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            RateLimitRule rule = findRule(rateLimitProperties, request.getServletPath(), request.getMethod());
            if (rule != null && CollectionUtils.isNotEmpty(rule.getKeys())) {
                for (String key : rule.getKeys()) {
                    String value = getValueFromHeaderOrParam(request, key);
                    if (StringUtils.isBlank(value)) {
                        continue;
                    }
                    boolean block = this.checkRateLimit(key, value, rule);
                    if (block) {
                        log.warn("{}:{}, path:{} is blocked.", key, value, request.getServletPath());
                        response.setStatus(429);
                        response.setContentType("application/json; charset=utf-8");
                        response.setCharacterEncoding("UTF-8");
                        if (StringUtils.isNotBlank(rule.getResponseBody())) {
                            response.getOutputStream().write(rule.getResponseBody().getBytes(StandardCharsets.UTF_8));
                        }
                        return;
                    }
                }
            }
        } catch (Exception e) {
            log.error("rate limit error.", e);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    private boolean checkRateLimit(String param, String value, RateLimitRule rule) {
        String key = String.format(KEY_PREFIX, rule.getUrl(), param, value);
        if (rule.getCapacity() == null || rule.getTimeOfSecond() == null) {
            return false;
        }
        Long count = stringRedisTemplate.execute(rateLimitLuaScript, Lists.newArrayList(key),
                String.valueOf(rule.getTimeOfSecond() * 1000),
                String.valueOf(rule.getCapacity()),
                String.valueOf(System.currentTimeMillis())
        );
        return count != null && count == 1;
    }

    @Override
    public void destroy() {

    }

    public static String getValueFromHeaderOrParam(HttpServletRequest request, String key) {
        String value = request.getHeader(key);
        if (StringUtils.isBlank(value)) {
            value = request.getParameter(key);
        }
        return value;
    }

    public static RateLimitRule findRule(RateLimitProperties rateLimitProperties, String path, String method) {
        if (rateLimitProperties == null || CollectionUtils.isEmpty(rateLimitProperties.getRules())) {
            return null;
        }
        for (RateLimitRule rule : rateLimitProperties.getRules()) {
            if ((StringUtils.isBlank(rule.getMethod()) || method.equalsIgnoreCase(rule.getMethod()))
                    && path.matches(rule.getUrl())) {
                return rule;
            }
        }
        return null;
    }
}

配置文件

rateLimit.enabled=true
rateLimit.rules[0].url=/.**
rateLimit.rules[0].method=GET
rateLimit.rules[0].keys=user_id
rateLimit.rules[0].responseBody={"msg":"您太快了","code":429}
rateLimit.rules[0].timeOfSecond=1
rateLimit.rules[0].capacity=3
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掘宪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子攘烛,更是在濱河造成了極大的恐慌魏滚,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坟漱,死亡現(xiàn)場離奇詭異栏赴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)靖秩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門须眷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沟突,你說我怎么就攤上這事花颗。” “怎么了惠拭?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵扩劝,是天一觀的道長庸论。 經(jīng)常有香客問我,道長棒呛,這世上最難降的妖魔是什么聂示? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮簇秒,結(jié)果婚禮上鱼喉,老公的妹妹穿的比我還像新娘。我一直安慰自己趋观,他們只是感情好扛禽,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著皱坛,像睡著了一般编曼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上剩辟,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天掐场,我揣著相機(jī)與錄音,去河邊找鬼贩猎。 笑死刻肄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的融欧。 我是一名探鬼主播敏弃,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼噪馏!你這毒婦竟也來了麦到?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤欠肾,失蹤者是張志新(化名)和其女友劉穎瓶颠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刺桃,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粹淋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瑟慈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桃移。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖葛碧,靈堂內(nèi)的尸體忽然破棺而出借杰,到底是詐尸還是另有隱情,我是刑警寧澤进泼,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布蔗衡,位于F島的核電站纤虽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绞惦。R本人自食惡果不足惜逼纸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望济蝉。 院中可真熱鬧杰刽,春花似錦、人聲如沸堆生。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淑仆。三九已至,卻和暖如春哥力,著一層夾襖步出監(jiān)牢的瞬間蔗怠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工吩跋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寞射,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓锌钮,卻偏偏與公主長得像桥温,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子梁丘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容