背景说明
限流在微服务架构的系统中是十分常见的需求,同样限流也是各个服务接入阶段也是不得不考虑的问题。在面对流量激增的情况下,我们要考虑给程序做限流控制,避免短时间过多的流量直接击垮系统。
所谓的限流指的是什么呢?可以这么理解,在一个时间周期内请求的次数,不得超过我们给定的一个预期值。如果超过了必须做出拒绝的策略,拒绝策略可以是丢弃,或者阻塞。那么这种需求的常见的实现方案有哪些呢?
常见的实现方案
限流功能常见的实现方案包括:
- 固定窗口法。
- 滑动窗口法
- 滑动日志法
- 漏桶法
- 令牌法
对于固定窗口和滑动窗口的理解
假定我们的限流要求是4秒内只能通过20个请求,那么固定窗口是将4秒的时间跨度视作一个整体窗口。要求在这一个窗口内,请求的次数不得超过20次。并且当时间经过4秒后,整个窗口会重置,窗口的计数器置为0。
这种做法在实现上很简单,但是会有一些问题无法很好的处理,比如窗口边界发生请求突增导致限流的结果超过预期设置的QPS。怎么理解这个现象呢?比如周期的最后一秒交易数量到了20次,在下一周期的第1秒交易数量也达到了20次。那么将两部分合起来,它的QPS其实是超过我们的预期。
为了解决边界请求突增的问题,我们可以使用滑动窗口法。滑动窗口法的一个核心思想是,它基于固定窗口法做了改进,固定窗口的问题是将整个时间跨度视为一个窗口,划分的粒度太粗了。那么滑动窗口呢,它的做法是划分出多个窗口,每个窗口内各自持有一个计数器。还是在刚刚的场景,划分出4个窗口,4个窗口各自都持有计数器。这种做法就解决了刚刚固定窗口的问题,在第4秒虽然来了20个请求,那么属于第4秒的窗口的计数器达到了20。现在时间来到4.5秒,这个时候第4秒的窗口是不会被回收的。那么4.5秒的时候进来的交易同样会被限流住,故此问题解决。滑动窗口就是通过这种方式让限流效果更加的平滑。
结合漏桶和滑动日志的特点实现限流器
漏桶中只能以恒定的速率剔除过期元素,而滑动日志的做法中让每一个元素都记录了请求的时间,那么我可以将这两者结合起来。在剔除的阶段,我可以拿到每个元素的请求时间并计算出过期时间,与当前时间进行实时比较,这样就能准确剔除真正到期了的元素。
通过这种方式,能够应对流量突增的情况。
下面附上我实现的限流算法的代码。
package com.hhdd.demos.rateLimitter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalTime;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 简单的限流器实现
* 实现思想和漏桶思路有点像,但略有变形
* 漏桶是按照恒定速率清除桶内的元素
* 本实现因桶内记录了时间戳故可更灵活的剔除过期成员
*
* @Author huanghedidi
* @Date 2025/2/7 21:12
*/
public class QueueTimeLogRateLimiter<T extends RateLimiterRequest> {
/**
* 限流器的名称,如针对每个接口设置一个限流器
* 则限流器名称可赋值为接口名称
*/
private String name;
/**
* 漏桶思想中桶的容积
*/
private int capacity;
/**
* 时间跨度,单位为ms
*/
private long timeSpan;
/**
* 漏桶内的元素用queue记录
* queue数据结构是有序的,方便剔除过期元素
*/
private Queue<T> queue;
/**
* 限流器是共享资源,存在并发问题,操作需要用锁
*/
private Lock lock;
private Atom