【限流02】限流算法实战篇 - 手撸一个单机版Http接口通用限流框架
本文将从需求的背景、需求分析、框架设计、框架实现几个层面一步一步去实现一个单机版的Http接口通用限流框架。
一、限流框架分析
1、需求背景
微服务系统中,我们开发的接口可能会提供给很多不同的系统去调用,如果调用方处理不当(如:秒杀场景下的流量突增) 导致接口的请求数突增,这些请求与正常请求会去竞争系统的线程资源,最终可能导致正常请求因分配不到线程资源而出现大量接口超时的现象。
对于这种问题的解决思路是,作为接口提供方,我们需要限制每个接口调用方的调用频率,避免出现某个接口调用次数频率过快导致线程资源耗尽的问题。
2、需求分析
(1)功能性需求
为了完成一个通用的限流框架,大概的执行步骤如下:
- 限流框架启动,读取并加载限流规则;
- 收到调用方请求,根据接口读取配置的限流规则,判断是否会被限流;
(2)非功能性需求
作为一个通用的限流框架,非功能性需求通常需要考虑到框架的易用性、容错性、性能、扩展性、灵活性。
- 易用性:对于使用者来说,第三方框架一般都希望能够尽量简单、易接入。因此,在限流规则配置方面,我们希望能够支持xml/yaml/properties等文件配置、也可以支持分布式配置源;对于限流接口,我们希望能够拿来即用;对于限流算法,我们希望能够提供多种限流算法,如固定时间窗口、滑动时间窗口、分布式限流算法等;因为现在大部分都是基于Spring开发的框架,所以我们希望限流框架能够很方便地集成使用到Spring框架中去;
- 扩展性、灵活性:我们需要考虑到框架的扩展性,能够灵活支持各种限流算法、以及自定义的限流算法;对于限流规则,我们希望支持不同格式(Json、Yaml、Xml等)、不同数据源(本地配置或ZK等配置)的限流规则配置方式;
- 性能:每个接口在调用之前都要被检查是否限流,这会增加接口请求的响应时间,因此我们需要尽可能减少限流框架本身对接口的响应时间的影响;
- 容错性:接入限流框架的目的是为了提高系统的可用性和稳定性,所以不能因为限流框架的异常导致影响到服务本身的可用性。
二、限流框架设计
框架设计这块我们主要工作是划分模块、对模块进行设计。通用限流框架我们主要分成限流规则、限流算法、限流模式、集成使用4个模块来设计。
1、限流规则
框架需要定义好限流规则的语法格式,包括调用方标识、需要限流的接口、限流的阈值、时间粒度、限流算法、限流模式等元素。
为了实现简单点,本期我们不考虑限流算法配置、限流模式配置等,需要限制调用方app1在一分钟内,调用接口 /v1/user 的次数不能超过100次的限流配置示例如下:
configs:
- appId: app1limits:- api: /v1/userlimit: 100unit: 60
对于文件格式,我们支持YAML/XML/JSON等格式;
对于数据源,我们支持本地配置,也支持其他的配置中心数据源,如Nacos。
2、限流算法
常见的限流算法有:固定时间窗口限流算法、滑动时间窗口限流算法、令牌桶限流算法、漏桶限流算法等。
基本思路就是:在一定时间内统计某个应用调用某个接口的次数,当调用次数超过阈值,则限流。
默认情况下,我们使用固定时间窗口限流算法。但是为了方便扩展,我们需要预先做好设计预留好扩展点,方便今后开发其他限流算法。
3、限流模式
限流模式分为单机限流和集群限流。
单机限流指的是针对某个服务的单个实例的访问次数进行限制;集群限流是对某个服务的多个实例调用总次数进行限流。
单机限流和集群限流的区别在于接口访问计数器的实现。单机限流只需要在单个实例中维护自己的接口请求计数器,而集群限流需要管理所有的实例计数器,这就需要一个第三方存储(如:Redis)来存储每个实例的接口访问次数。
4、集成使用
因为大部分接口调用方、提供方都是基于Spring框架实现的,所以我们可以开发一个类似于Mybatis-Spring类库,方便在使用Spring框架的项目中集成使用限流框架。
除了以上3个模块,我们还需要考虑到框架的容错性、性能等方面。对于集群限流,为了避免因为限流框架自身响应时间过长影响接口的响应时间,我们可以基于Redis去开发实现集群限流框架;对于限流框架自身可能抛出的异常,我们需要区别对待,如框架代码异常,我们直接抛出,同时不能影响接口使用。
三、限流框架实现
1、最小原型(MVP)代码
在开发框架的时候,我们没必要想着完成框架所需要的所有功能,也没必要使用设计模式、原则去实现一个优秀的框架。第一版本的代码可以在不考虑代码设计和质量的情况下,完成所需要的功能。一个基本的限流框架所需要的功能如下:
- 对于接口类型,只支持HTTP接口的限流,暂不支持RPC等其他类型的接口限流;
- 对于限流规则,只支持本地文件配置,配置文件只支持YAML;
- 对于限流算法,只支持固定时间窗口算法;
- 对于限流模式,只支持单机限流。
整体类的实现如下:
框架入口: RateLimiter,读取、加载、解析限流配置,并提供限流接口;
限流算法: RateLimitAlg, 默认采用固定时间窗口限流算法;
限流规则: ApiLimit、AppRuleConfig、RateLimitRule、RuleConfig
具体代码如下:
RateLimitAlg.java:
public class RateLimiter {private RateLimitRule rule;// 每个API内存中存储限流计数器, key为 api:urlprivate ConcurrentHashMap<String, RateLimitAlg> counters = new ConcurrentHashMap<>();public RateLimiter() {RuleConfig ruleConfig = loadFromYmlAsRuleConfig();Assert.isTrue(ruleConfig != null, "Load from yaml file, RuleConfig is null");this.rule = new RateLimitRule(ruleConfig);}/*** 判断接口是否限流** @param appId* @param url* @return true: 不限流; false: 限流* @throws InterruptedException*/public boolean limit(String appId, String url) throws InterruptedException {// 接口未配置限流, 直接返回ApiLimit apiLimit = rule.getApiLimit(appId, url);if (apiLimit == null) {return true;}String counterKey = appId + ":" + url;RateLimitAlg rateLimitAlg = counters.get(counterKey);if (rateLimitAlg == null) {// 没有计数器, 就构造一个RateLimitAlg rateLimitCounterNew = new RateLimitAlg(apiLimit.getLimit());RateLimitAlg rateLimitCounterOld = counters.putIfAbsent(counterKey, rateLimitCounterNew);if (rateLimitCounterOld == null) {rateLimitAlg = rateLimitCounterNew;}}// 固定窗口统计, 判断是否超过限流阈值return rateLimitAlg.tryAcquire();}private RuleConfig loadFromYmlAsRuleConfig() {InputStream in = null;RuleConfig ruleConfig = null;try {in = this.getClass().getResourceAsStream("/sentinel-rule.yml");if (in != null) {Yaml yaml = new Yaml();ruleConfig = yaml.loadAs(in, RuleConfig.class);return ruleConfig;}} catch (Exception e) {e.printStackTrace();} finally {if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}}return null;}
}
RateLimitAlg.java:
public class RateLimitAlg {// msprivate static final long LOCK_EXPIRE_TIME = 200L;private Stopwatch stopWatch;// 限流计数器private AtomicInteger counter = new AtomicInteger(0);private final int limit;private Lock lock = new ReentrantLock();public RateLimitAlg(int limit) {this(limit, Stopwatch.createStarted());}public RateLimitAlg(int limit, Stopwatch stopWatch) {this.limit = limit;this.stopWatch = stopWatch;}public boolean tryAcquire() throws InterruptedException {int currentCount = counter.incrementAndGet();// 未达到限流if (currentCount < limit) {return true;}// 使用固定时间窗口统计当前窗口请求数// 请求到来时,加锁进行计数器统计工作try {if (lock.tryLock(LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS)) {// 如果超过这个时间窗口, 则计数器counter归零, stopWatch, 窗口进入下一个窗口if (stopWatch.elapsed(TimeUnit.MILLISECONDS) > TimeUnit.SECONDS.toMillis(1)) {counter.set(0);stopWatch.reset();}// 不超过, 则当前时间窗口内的计数器counter+1currentCount = counter.incrementAndGet();return currentCount < limit;}} catch (InterruptedException e) {System.out.println("tryAcquire() wait lock too long:" + LOCK_EXPIRE_TIME + " ms");throw new InterruptedException("tryAcquire() wait lock too long:" + LOCK_EXPIRE_TIME + " ms");} finally {lock.unlock();}// 出现异常 不能影响接口正常请求return true;}
}
限流规则:ApiLimit、AppRuleConfig、RuleConfig
public class ApiLimit {private static final int DEFAULT_UNIT_SECONDS = 1;private String api;private int limit;private int unit = DEFAULT_UNIT_SECONDS;// ...
}public class AppRuleConfig {private String appId;private List<ApiLimit> limits;// ...
}public class RuleConfig {private List<AppRuleConfig> configs;// ...
}
RateLimitRule 提供了快速查询限流规则的方法:
/*** @author: wanggenshen* @date: 2020/6/23 00:12.* @description: 支持快速查询 ApiLimit** TODO:* (1) 精准匹配优化: 二分查找算法优化* (2) 支持前缀匹配: 使用Trie树实现* (3) 支持模糊匹配: 实现难度较高*/
public class RateLimitRule {/*** key : appId + api, value: limit*/private HashMap<String, ApiLimit> map = new HashMap();public RateLimitRule(RuleConfig ruleConfig) {List<AppRuleConfig> configs = ruleConfig.getConfigs();configs.stream().forEach(appRuleConfig -> {String appId = appRuleConfig.getAppId();List<ApiLimit> apiLimitList = appRuleConfig.getLimits();apiLimitList.stream().forEach(apiLimit -> {String key = appId + ":" + apiLimit.getApi();map.put(key, apiLimit);});});}public ApiLimit getApiLimit(String appId, String api) {String key = appId + ":" + api;return map.get(key);}
}
测试:
(1) 首先配置限流规则:
configs:
- appId: app1limits:- api: /v1/userlimit: 5unit: 60- api: /v1/orderlimit: 4unit: 60
- appId: app2limits:- api: /v1/loginlimit: 7unit: 60
(2) 测试:
public static void main(String[] args) {RateLimiter rateLimiter = new RateLimiter();try {for (int i = 0; i < 10; i++) {boolean b = rateLimiter.limit("app1", "/v1/user");System.out.println("/v1/user接口限流结果: " + b);}System.out.println("=====");for (int i = 0; i < 10; i++) {boolean b = rateLimiter.limit("app1", "/v1/order");System.out.println("/v1/order接口限流结果: " + b);}System.out.println("=====");for (int i = 0; i < 10; i++) {boolean b = rateLimiter.limit("app2", "/v1/login");System.out.println("/v1/login接口限流结果:" + b);}} catch (Exception e) {}}
测试结果如下,跟配置的规则相同:
2、优化与重构V2版本
在实现了框架的功能之后,我们需要从Code Reviewer的角度,结合SOLID、DRY、KISS、基于接口而非实现编程、高内聚松耦合、编码规范等去分析代码的设计和实现在可读性、扩展性等方面有没有优化的点。
(1)代码可读性
代码可读性这块我们需要重点关注 目录设计是否合理、模块划分是否清晰、代码结构是否高内聚低耦合、是否符合编码规范这几点。
由于代码较少,所以以上几点都相对较为满足,即可读性较好。
(2)代码扩展性
扩展性主要是要遵循基于接口而非实现的编程思想,具有接口抽象意识。
RateLimitAlg 类只实现了固定时间窗口限流算法,如果我们需要使用其他的限流算法,就需要重写原代码,所以需要提供更加抽象的算法接口;
RateLimitRule 类只实现了简单的查询配置规则的接口,需要提供更加抽象的接口来支持二分查找等优化后的查找算法;
除此之外,入口类RateLimiter只提供规则的加载与接口限流,需要将文件的读取抽离开来。
优化后的代码见链接: 优化后的代码链接
总结
本文从 限流框架的分析、设计、实现3个层面,一步一步实现一个单机版Http接口限流框架。当然,实现出来的框架很粗糙,离生产环境使用还是有一定距离的,但是提供了一种通用框架实现的思路,帮助我们更好地去理解限流算法的思路、通用框架的实现过程。
【限流02】限流算法实战篇 - 手撸一个单机版Http接口通用限流框架相关推荐
- dueros模拟测试没有请求后台_实战 | 用手写一个骚气的请求合并,演绎底层的真实...
来源:公众号[ java进阶架构师] 好文推荐: 字节跳动Java岗4面面经分享:索弓|+rabbitmq+spring+Redis 拼多多面经Java开发3面面经:准备好久没想到面试题超级简单 网易 ...
- 呆呆带你手撸一个思维导图-基础篇
希沃ENOW大前端 公司官网:CVTE(广州视源股份) 团队:CVTE旗下未来教育希沃软件平台中心enow团队 「本文作者:」 前言 你盼世界,我盼望你无bug.Hello 大家好,我是霖呆呆! 哈哈 ...
- 第二篇-用Flutter手撸一个抖音国内版,看看有多炫
前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽, 先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...
- svm手写数字识别_KNN 算法实战篇如何识别手写数字
上篇文章介绍了KNN 算法的原理,今天来介绍如何使用KNN 算法识别手写数字? 1,手写数字数据集 手写数字数据集是一个用于图像处理的数据集,这些数据描绘了 [0, 9] 的数字,我们可以用KNN 算 ...
- 限流之滑动窗口算法实战
一 算法 滑动窗口算法弥补了计数器算法的不足.滑动窗口算法把间隔时间划分成更小的粒度,当更小粒度的时间间隔过去后,把过去的间隔请求数减掉,再补充一个空的时间间隔. 如下图所示,把1分钟划分为10个更小 ...
- 【算法实战篇】时序多分类赛题-2020数字中国创新大赛-智慧海洋建设top5方案(含源码)
Hi,大家好!这里是AILIGHT!AI light the world!这次给大家带来的是2020数字中国创新大赛-数字政府赛道-智能算法赛:智慧海洋建设的算法赛复赛赛道B top5的方案以 ...
- 实战:如何打造一个好的标题进行引流?
很多人认为,做标题只要把行业热搜的词符合自己产品的词放进去组合就好了,或者说,只要把卖的好的跟我同款的标题复制一下,改动一下就行了,他能卖爆,我也能.这个是大错特错的.别人的标题通过搜索引擎下单成交, ...
- 用c语言实现knn算法要有训练集和测试集,KNN算法实战:手写字体识别
我们已经知道手写字体数据集是一个8×8的矩阵,共有64个特征.让我们看一下K最近邻算法对手写字体数据集处理的效果. 1) 导入相关包 这里我们将用到 datasets 中的手写字体数据,使用 trai ...
- 数据结构与算法学习篇给你一个文件里面包含全国人民(14亿)的年龄数据(0~200),现在要你统计每一个年龄有多少人?
给你一个文件里面包含全国人民(14亿)的年龄数据(0~180),现在要你统计每一个年龄有多少人? 限制: 给定机器为 单台+1CPU+1G内存.不得使用现成的容器,比如map等. 假设每个年龄数据为2 ...
最新文章
- 吴恩达老师深度学习视频课笔记:构建机器学习项目(机器学习策略)(2)
- 2013应届毕业生“京北方”校招应聘总结
- 云炬Android开发笔记 使用新版本Android studio快速Build低版本项目的仓库代码(标红部分)
- context:annotation-config vs context:component-scan
- 角落的开发工具集之Vs(Visual Studio)2017插件推荐
- STAT 7008 - Assignment Question 1 (hashtag analysis)
- 百倍加速!Python量化策略的算法性能提升指南
- ny 2 括号配对问题
- 《Spring 5 官方文档》5. 验证、数据绑定和类型转换(二)
- c#多线程thread实例详解
- [Oracle] SQL*Loader 详细使用教程(5)- 典型例子
- 获得当月时间chuo_擅用GTD时间管理法,远离焦虑情绪,让你的工作、学习井然有序...
- 十一、垃圾回收策略配置
- 卡巴斯基授权key导入方式方法及其导入key基本原理
- dalong(大龙燚火锅)
- CentOS6.7 i686上安装JDK7
- 谷歌中国进入后李开复时代:向总部架构靠拢
- java 图片缩放 失真_Java图片缩小后不失真的代码(缩略图)
- Linux 中设置计划任务(定时任务)
- hive日期函数,求日期差等,datediff,date_add,date_sub,add_months