限流算法
- 计数器限流固定窗口滑动窗口
- 桶限流令牌桶漏桶
计数器限流可以分为:
- 固定窗口
- 滑动窗口
固定窗口计数器限流简单明了 , 就是限制单位之间内的请求数 , 比如设置QPS为10 , 那么从一开始的请求进入就计数 , 每次计数前判断是否到10 , 到达就拒绝请求 , 并保证这个计数周期是1秒 , 1秒后计数器清零 。
以下是利用redis实现计数器分布式限流的实现 , 曾经在线上实践过的lua脚本:
local key = KEYS[1] local limit = tonumber(ARGV[1]) local refreshInterval = tonumber(ARGV[2]) local currentLimit = tonumber(redis.call('get', key) or '0') if currentLimit + 1 > limit thenreturn -1; elseredis.call('INCRBY', key, 1)redis.call('EXPIRE', key, refreshInterval)return limit - currentLimit - 1 end 一个明显的弊端就是固定窗口计数器算法无法处理突刺流量 , 比如10QPS , 1ms中来了10个请求 , 后续的999ms的所有请求都会被拒绝 。
滑动窗口
为了解决固定窗口的问题 , 滑动窗口将窗口细化 , 用更小的窗口来限制流量 。比如 1 分钟的固定窗口切分为 60 个 1 秒的滑动窗口 。然后统计的时间范围随着时间的推移同步后移 。
即便滑动时间窗口限流算法可以保证任意时间窗口内接口请求次数都不会超过最大限流值 , 但是仍然不能防止在细时间粒度上面访问过于集中的问题 。
为了应对上面的问题 , 对于时间窗口限流算法 , 还有很多改进版本 , 比如:
多层次限流 , 我们可以对同一个接口设置多条限流规则 , 除了 1 秒不超过 100 次之外 , 我们还可以设置 100ms 不超过 20 次 (代码中实现写了平均的两倍) , 两条规则同时限制 , 流量会更加平滑 。
简单实现的代码如下:
public class SlidingWindowRateLimiter { // 小窗口链表 LinkedList<Room> linkedList = null; long stepInterval = 0; long subWindowCount = 10; long stepLimitCount = 0; int countLimit = 0; int count = 0; public SlidingWindowRateLimiter(int countLimit, int interval){ // 每个小窗口的时间间距 this.stepInterval = interval * 1000/ subWindowCount; // 请求总数限制 this.countLimit = countLimit; // 每个小窗口的请求量限制数 设置为平均的2倍 this.stepLimitCount = countLimit / subWindowCount * 2; // 时间窗口开始时间 long start = System.currentTimeMillis(); // 初始化连续的小窗口链表 initWindow(start); } Room getAndRefreshwindows(long requestTime){ Room firstRoom = linkedList.getFirst(); Room lastRoom = linkedList.getLast(); // 发起请求时间在主窗口内 if(firstRoom.getStartTime() < requestTime && requestTime < lastRoom.getEndTime()){ long distanceFromFirst = requestTime - firstRoom.getStartTime(); int num = (int) (distanceFromFirst/stepInterval); return linkedList.get(num); }else{ long distanceFromLast = requestTime - lastRoom.getEndTime(); int num = (int)(distanceFromLast/stepInterval); // 请求时间超出主窗口一个窗口以上的身位 if(num >= subWindowCount){ initWindow(requestTime); return linkedList.getFirst(); }else{ moveWindow(num+1); return linkedList.getLast(); } } } public boolean acquire(){ synchronized (mutex()) { Room room = getAndRefreshWindows(System.currentTimeMillis()); int subCount = room.getCount(); if(subCount + 1 <= stepLimitCount && count + 1 <= countLimit){ room.increase(); count ++; return true; } return false; } } /** * 初始化窗口 * @param start */ private void initWindow(long start){ linkedList = new LinkedList<Room>(); for (int i = 0; i < subWindowCount; i++) { linkedList.add(new Room(start, start += stepInterval)); } // 总记数清零 count = 0; } /** * 移动窗口 * @param stepNum */ private void moveWindow(int stepNum){ for (int i = 0; i < stepNum; i++) { Room removeRoom = linkedList.removeFirst(); count = count - removeRoom.count; } Room lastRoom = linkedList.getLast(); long start = lastRoom.endTime; for (int i = 0; i < stepNum; i++) { linkedList.add(new Room(start, start += stepInterval)); } } public static void main(String[] args) throws InterruptedException { SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(20, 5); for (int i = 0; i < 26; i++) { System.out.println(slidingWindowRateLimiter.acquire()); Thread.sleep(300); } } class Room{ Room(long startTime, long endTime) { this.startTime = startTime; this.endTime = endTime; this.count = 0; } private long startTime; private long endTime; private int count; long getStartTime() { return startTime; } long getEndTime() { return endTime; } int getCount() { return count; } int increase(){ this.count++; return this.count; } } private volatile Object mutexDoNotUseDirectly; private Object mutex() { Object mutex = mutexDoNotUseDirectly; if (mutex == null) { synchronized (this) { mutex = mutexDoNotUseDirectly; if (mutex == null) { mutexDoNotUseDirectly = mutex = new Object(); } } } return mutex; }}
推荐阅读
- Java8新特性之空指针异常的克星Optional类
- 跟我一起了解Java到底好在哪?
- 交通事故现场拍照技巧,全是干货!
- 防火墙主备切换案例分析,干货值得收藏
- java服务 tomcat安装,不要太简单
- Java开发必会的Linux命令
- 常用排序算法之JavaScript实现
- Java核心技术-macOS下配置Java11环境
- 分享cmd 窗口中运行 Java 程序小技巧
- 如何理解JAVA类装载器ClassLoader?高级开发才懂的技术点