介绍源码前,文档上让我们了解下利特尔法则,利特尔法则-维基百科:
在一个稳定的系统中,长期的平均顾客人数(L),等于长期的有效抵达率(λ),乘以顾客在这个系统中平均的等待时间(W);或者,我们可以用一个代数式来表达:
L=λW
这里说的平均时间每个平台的定义不一样的,有的平台以小时为单位,有的平台以分钟为单位,有的平台以秒为单位。
举一个例子:如果每秒生成的请求许可是0.0167个,这里的每秒表示以秒为单位,0.0167是生成的速率(λ)。用等式表达就是:
0.0167=0.0167 * 1S
换句话说,这里的λ是以1秒为单位的。那么问题来了,如果问你,按照这种速率,生成一个许可需要多长时间?
将数值代入,这里的1个许可就是L=1,λ=0.0167,求W为多少秒?答案是:
1/0.0167=59.88023952095808S
将近一分钟才生成一个许可,可见其速度之慢,所以要确认下是不是自己的速率单位搞错了🤔.
知道了这个法则,我们来看下这个工具类的使用demo。
@Test
public void testRateLimiter() {
RateLimiter rateLimiter = RateLimiter.create(0.0167);
while (true) {
if (rateLimiter.tryAcquire()) {
//获取到许可,执行相应的逻辑
//System.out.println(String.format("获取成功,时间:%s", new Date(System.currentTimeMillis()).toString()));
} else {
//获取失败,执行相应的逻辑
//System.out.println(String.format("获取失败,时间:%s", new Date(System.currentTimeMillis()).toString()));
}
}
}
其实测试用例很简单:
- 设置许可生成的速率
- 进行获取许可操作
我们来看下设置生成许可的速率逻辑。进入到RateLimiter.creater()
方法中:
//由入参名称我们可以知道,这个速率是以秒为单位的
public static RateLimiter create(double permitsPerSecond) {
// 内部调用了另一个create重载方法
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
上代码表示速率是以秒为单位,调用了另一个重载方法,这个create方法有两个参数,一个是速率,另一个为
SleepingStopwatch.createFromSystemTimer()
,这个入参是啥意思?忍不住点进去看了下:
// 这是一个RateLimiter抽象内部类
abstract static class SleepingStopwatch {
//构造函数
protected SleepingStopwatch() {}
//抽象方法:读取时间单位为微妙。
protected abstract long readMicros();
//抽象方法:可中断睡眠
protected abstract void sleepMicrosUninterruptibly(long micros);
public static SleepingStopwatch createFromSystemTimer() {
return new SleepingStopwatch() {
//创建一个stopwatch,内部记录了一个起始时间。
final Stopwatch stopwatch = Stopwatch.createStarted();
//重写读取时间方法
@Override
protected long readMicros() {
//实际是获取从stopwatch创建后已经运行的时间。
return stopwatch.elapsed(MICROSECONDS);
}
@Override
protected void sleepMicrosUninterruptibly(long micros) {
if (micros > 0) {
//该方法内部使用的是Thread.sleep方法,封装这个方法是给RateLimiter内部使用。
Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS);
}
}
};
}
}
另外提一嘴,这里面使用了工具StopWatch工具类,它的优势是:对于秒、毫秒为单位方便计时的程序,尤其是单线程、顺序执行程序的时间特性的统计输出支持比较好。
上面的代码大致可以看出,这是一个可以进行线程休眠的stopwatch。
ok,继续回到create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer())
方法,
看下方法代码:
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
//SmoothBursty是RateLimiter的一个子类,继承关系是:SmoothBursty 继承 SmoothRateLimiter 继承 RateLimiter。
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
SmoothBursty ,SmoothRateLimiter ,RateLimiter。三者的关系如图:
继续跟踪SmoothBursty代码。
// 继承SmoothRateLimiter是SmoothRateLimiter的内部类
static final class SmoothBursty extends SmoothRateLimiter {
//这个相当于设置利特尔法则中的W的单位,一般我们都是指定每秒来计算,即值为1,当然你也可以指定2S,5S等。
final double maxBurstSeconds;
SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
//最终调用的是RateLimiter构造方法
super(stopwatch);
//设置W的值(利特尔法则公式中的W)
this.maxBurstSeconds = maxBurstSeconds;
}
//重写doSetRate方法,permitsPerSecond λ生成许可的速率
//stableIntervalMicros
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
//暂存老的最大许可数,L
double oldMaxPermits = this.maxPermits;
// L=λW,获取新的L,因为传值传来了新的生成许可的速率。
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// 如果老的最大许可数(L)是正无穷,则默认以最新速率获得的最大许可数替代。
storedPermits = maxPermits;
} else {
//如果已经存在已生成的部分许可,由于之前生成的许可是老速率生成的,现在换成新速率了,已生成的部分许可,需要等比例的换成新速率下的生成的许可。
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
@Override
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
return 0L;
}
@Override
double coolDownIntervalMicros() {
return stableIntervalMicros;
}
}
由代码我们可以知道maxBurstSeconds是利特尔法则中W的含义,换句话说RateLimiter rateLimiter = RateLimiter.create();
方法内部默认将maxBurstSeconds设为了1S为单位。所以如果想自定义λ和W的值,可以使用这个类来创建RateLimiter。
这里需要注意的是它替换旧值的策略,即代码里说到的按比例替换。使用L1和L2表示老的和新的最大许可数,使用S1和S2表示旧的和新的已生成的部分许可数。它直接使用的是L1/L2=S1/S2,所以S2=L2*S1/L1。
我们继续来观察下create方法:
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
//SmoothBursty是RateLimiter的一个子类,继承关系是:SmoothBursty 继承 SmoothRateLimiter 继承 RateLimiter。
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
创建完rateLimiter需要setRate看下setRate代码,首先是RateLimiter里的setRate方法:
public final void setRate(double permitsPerSecond) {
// 验证速率是否大于整
Preconditions.checkArgument(permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized(this.mutex()) {
//上锁设置速率, this.stopwatch.readMicros()方法表示获取已经运行的时间,
this.doSetRate(permitsPerSecond, this.stopwatch.readMicros());
}
}
doSetRate是抽象方法,我们来看一下它的实现,位于SmoothRateLimiter类中,代码如下:
final void doSetRate(double permitsPerSecond, long nowMicros) {
//刷新生成的许可数、时间标记等。
resync(nowMicros);
//stableIntervalMicros表示产生一个令牌所需要的时间,所以由公式:L=λW,L为1,得到W=1/λ
//得到的时间是以秒为单位,换算成微妙需要乘上1000.所以才有下面的计算方法。
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
//赋值生成一个许可所需要的时间
this.stableIntervalMicros = stableIntervalMicros;
//该方法是个抽象方法,有两个实现,SmoothBursty和SmoothWarmingUp,我们目前只解析SmoothBursty中的。
doSetRate(permitsPerSecond, stableIntervalMicros);
}
void resync(long nowMicros) {
// 如果当前持续时间大于上次记录时间
if (nowMicros > nextFreeTicketMicros) {
//计算出时间间隔除以获取一个许可的单位时间计算出获得的许可数。
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
//计算出已生成的许可数。为什么每次要计算更新一次,因为时间一直再更新。放在这里触发。
storedPermits = min(maxPermits, storedPermits + newPermits);
//将这次获取到的时间做一个标记,留作下次计算使用
nextFreeTicketMicros = nowMicros;
}
}
上述代码中的coolDownIntervalMicros
方法,我们只解析SmoothBursty
中的,SmoothWarmingUp
下次再说。
//SmoothBursty类中的两个方法:
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
//该方法上面已介绍
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = maxPermits;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
double coolDownIntervalMicros() {
//直接返回生成一个许可需要多少微妙
return stableIntervalMicros;
}
看完这些后,我们再回到最初的测试代码:
@Test
public void testRateLimiter() {
RateLimiter rateLimiter = RateLimiter.create(0.0167);
while (true) {
if (rateLimiter.tryAcquire()) {
//获取到许可,执行相应的逻辑
//System.out.println(String.format("获取成功,时间:%s", new Date(System.currentTimeMillis()).toString()));
} else {
//获取失败,执行相应的逻辑
//System.out.println(String.format("获取失败,时间:%s", new Date(System.currentTimeMillis()).toString()));
}
}
}
我们已经得到了实际类是SmoothBursty的RateLimiter,接下来我们要看下rateLimiter.tryAcquire()的代码:
//内部调用了tryAcquire重载方法
public boolean tryAcquire() {
return tryAcquire(1, 0, MICROSECONDS);
}
//tryAcquire重载方法
//permits:需要获取的许可数
//timeout:获取许可允许的超时时间
//unit:超时时间单位
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
//获取超时时间单位为微秒
long timeoutMicros = max(unit.toMicros(timeout), 0);
//检查permits是否大于0
checkPermits(permits);
long microsToWait;
//上锁获取许可开始
synchronized (mutex()) {
//获取stopwatch已经持续的时间
long nowMicros = stopwatch.readMicros();
//canAcquire方法判断当前持续时间是否大于上一次标记时间
if (!canAcquire(nowMicros, timeoutMicros)) {
//不行返回false
return false;
} else {
//如果符合当前持续时间大于上次标记时间,则可以尝试获取许可。
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
//sleep一会
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
// 验证许可数是否符合需求,需要是正整数
private static void checkPermits(int permits) {
checkArgument(permits > 0, "Requested permits (%s) must be positive", permits);
}
看完上述的代码,需要继续跟踪canAcquire
和reserveAndGetWaitLength
的代码,先看下canAcquire
的代码:
private boolean canAcquire(long nowMicros, long timeoutMicros) {
//这行代码可以理解成当前持续时间+超时时间应该大于等于上次标记时间,换句话说,当前持续时间(包含超时时间)一定大于上次标记的时间,不能小于后者。
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
继续看下queryEarliestAvailable
方法代码,这个方法实现是在SmoothRateLimiter
里实现的(目前不分析SmoothWarmingUp
)。其中nextFreeTicketMicros
代表的是时间标记,上述resync
方法中已经介绍过,我们可以把它理解成已经更新了的最新许可生成数的时间标记。
final long queryEarliestAvailable(long nowMicros) {
//返回上次标记时间
return nextFreeTicketMicros;
}
再继续观察下reserveAndGetWaitLength
代码,
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
我们看下SmoothRateLimiter
里的reserveEarliestAvailable
方法,看下代码:
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
//刷新当前持续时间的已生成许可数,并刷新标记时间。SmoothRateLimiter类中的resync方法上面已做解析
resync(nowMicros);
//最新的标记时间
long returnValue = nextFreeTicketMicros;
//能获取到的最小许可数
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
//算取需要的许可数和已经生成的许可数差值
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros =
//SmoothRateLimiter中的storedPermitsToWaitTime方法直接返回0L,所以直接计算的是后面的乘式
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
//将获取指定许可需要等待的时间加上上次标记时间来重置标记时间,当下次获取许可时,如果当前时间小于标记时间说明没有获得许可,也就是说能进到当前方法里的都是能获得许可的。
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
//由于进入到该方法都是获得许可的,所以需要减去获得的许可。
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
看完上述代码,我们来总结下涉及的一些流程。
图片里的流程是我画的自己理解的大致流程,里面的关键点有:
- 判断有没有获取到许可判断条件是当前持续时间是否大于标记时间,因为标记时间的生成是根据下一个许可生成的时间点来的赋值的。如果不是在指定时间点之后,那么许可也就没有生成。
- 代码里涉及到一些属性,如:最大存储许可数、当前已存储许可数、标记时间等,在Limiter初始化时会触发更新,在进行获取许可操作时也会进行更新。
本文解读的源码是根据SmoothBursty
来的,它还有个同级的类叫SmoothWarmingUp
,两者的区别是:
SmoothBursty
就是普通的限流器,第一个流量进来就要遵守速率限制SmoothWarmingUp
带预热的限流。即在指定预热期,允许放过的流量逐渐增加。预热期结束后,允许放过的流量就等于设定的限流值。
看完了SmoothBursty
的代码相关的流程,你可以独自看下SmoothWarmingUp
的代码,上述的博文是需要结合源码看的,光看文章介绍是没有深刻印象的,需要自己本地跑下测试代码,一步步debug去理解。
虽然它是一个单节点的限流工具,但是可以根据当前集群里的节点数以及目标速率来计算单个节点所需要控制的速率,这就需要借助统一配置中心来动态配置实现。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类