在分布式系统中,应对高并发访问时,缓存、限流、降级是保护系统正常运行的常用方法。当请求量突发暴涨时,如果不加以限制访问,则可能导致整个系统崩溃,服务不可用。同时有一些业务场景,比如短信验证码,或者其它第三方API调用,也需要提供必要的访问限制支持。还有一些资源消耗过大的请求,比如数据导出等(参考 记一次线上Java服务CPU 100%处理过程 ),也有限制访问频率的需求。

常见的限流算法有令牌桶算法,漏桶算法,与计数器算法。本文主要对三个算法的基本原理及Google Guava包中令牌桶算法的实现RateLimiter进行介绍,下一篇文章介绍最近写的一个以RateLimiter为参考的分布式限流实现及计数器限流实现。

令牌桶算法

令牌桶算法的原理就是以一个恒定的速度往桶里放入令牌,每一个请求的处理都需要从桶里先获取一个令牌,当桶里没有令牌时,则请求不会被处理,要么排队等待,要么降级处理,要么直接拒绝服务。当桶里令牌满时,新添加的令牌会被丢弃或拒绝。

令牌桶算法的处理示意图如下(图片来自网络)

令牌桶算法主要是可以控制请求的平均处理速率,它允许预消费,即可以提前消费令牌,以应对突发请求,但是后面的请求需要为预消费买单(等待更长的时间),以满足请求处理的平均速率是一定的。

漏桶算法

漏桶算法的原理是水(请求)先进入漏桶中,漏桶以一定的速度出水(处理请求),当水流入速度大于流出速度导致水在桶内逐渐堆积直到桶满时,水会溢出(请求被拒绝)。

漏桶算法的处理示意图如下(图片来自网络)

漏桶算法主要是控制请求的处理速率,平滑网络上的突发流量,请求可以以任意速度进入漏桶中,但请求的处理则以恒定的速度进行。

计数器算法

计数器算法是限流算法中最简单的一种算法,限制在一个时间窗口内,至多处理多少个请求。比如每分钟最多处理10个请求,则从第一个请求进来的时间为起点,60s的时间窗口内只允许最多处理10个请求。下一个时间窗口又以前一时间窗口过后第一个请求进来的时间为起点。常见的比如一分钟内只能获取一次短信验证码的功能可以通过计数器算法来实现。

Guava RateLimiter解析

Guava是Google开源的一个工具包,其中的RateLimiter是实现了令牌桶算法的一个限流工具类。在pom.xml中添加guava依赖,即可使用RateLimiter

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>

如下测试代码示例了RateLimiter的用法,

public static void main(String[] args) {
RateLimiter rateLimiter = RateLimiter.create(1); //创建一个每秒产生一个令牌的令牌桶
for(int i=1;i<=5;i++) {
double waitTime = rateLimiter.acquire(i); //一次获取i个令牌
System.out.println("acquire:" + i + " waitTime:" + waitTime);
}
}

运行后,输出如下,

acquire:1 waitTime:0.0
acquire:2 waitTime:0.997729
acquire:3 waitTime:1.998076
acquire:4 waitTime:3.000303
acquire:5 waitTime:4.000223

第一次获取一个令牌时,等待0s立即可获取到(这里之所以不需要等待是因为令牌桶的预消费特性),第二次获取两个令牌,等待时间1s,这个1s就是前面获取一个令牌时因为预消费没有等待延到这次来等待的时间,这次获取两个又是预消费,所以下一次获取(取3个时)就要等待这次预消费需要的2s了,依此类推。可见预消费不需要等待的时间都由下一次来买单,以保障一定的平均处理速率(上例为1s一次)。

RateLimiter有两种实现:

  1. SmoothBursty: 令牌的生成速度恒定。使用 RateLimiter.create(double permitsPerSecond) 创建的是 SmoothBursty 实例。
  2. SmoothWarmingUp:令牌的生成速度持续提升,直到达到一个稳定的值。WarmingUp,顾名思义就是有一个热身的过程。使用 RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 时创建就是 SmoothWarmingUp 实例,其中 warmupPeriod 就是热身达到稳定速度的时间。

类结构如下

关键属性及方法解析(以 SmoothBursty 为例)

1.关键属性

/** 桶中当前拥有的令牌数. */
double storedPermits; /** 桶中最多可以保存多少秒存入的令牌数 */
double maxBurstSeconds; /** 桶中能存储的最大令牌数,等于storedPermits*maxBurstSeconds. */
double maxPermits; /** 放入令牌的时间间隔*/
double stableIntervalMicros; /** 下次可获取令牌的时间点,可以是过去也可以是将来的时间点*/
private long nextFreeTicketMicros = 0L;

2.关键方法

调用 RateLimiter.create(double permitsPerSecond) 方法时,创建的是 SmoothBursty 实例,默认设置 maxBurstSeconds 为1s。SleepingStopwatch 是guava中的一个时钟类实现。

@VisibleForTesting
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
} SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
super(stopwatch);
this.maxBurstSeconds = maxBurstSeconds;
}

并通过调用 SmoothBursty.doSetRate(double, long) 方法进行初始化,该方法中:

  1. 调用 resync(nowMicros) 对 storedPermits 与 nextFreeTicketMicros 进行了调整——如果当前时间晚于 nextFreeTicketMicros,则计算这段时间内产生的令牌数,累加到 storedPermits 上,并更新下次可获取令牌时间 nextFreeTicketMicros 为当前时间。
  2. 计算 stableIntervalMicros 的值,1/permitsPerSecond。
  3. 调用 doSetRate(double, double) 方法计算 maxPermits 值(maxBurstSeconds*permitsPerSecond),并根据旧的 maxPermits 值对 storedPermits 进行调整。

源码如下所示

@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
} /** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
} @Override
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;
}
}

调用 acquire(int) 方法获取指定数量的令牌时,

  1. 调用 reserve(int) 方法,该方法最终调用 reserveEarliestAvailable(int, long) 来更新下次可取令牌时间点与当前存储的令牌数,并返回本次可取令牌的时间点,根据该时间点计算需要等待的时间
  2. 阻塞等待1中返回的等待时间
  3. 返回等待的时间(秒)

源码如下所示

/** 获取指定数量(permits)的令牌,阻塞直到获取到令牌,返回等待的时间*/
@CanIgnoreReturnValue
public double acquire(int permits) {
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
} final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
} /** 返回需要等待的时间*/
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
} /** 针对此次需要获取的令牌数更新下次可取令牌时间点与存储的令牌数,返回本次可取令牌的时间点*/
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros); // 更新当前数据
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // 本次可消费的令牌数
double freshPermits = requiredPermits - storedPermitsToSpend; // 需要新增的令牌数
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros); // 需要等待的时间 this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); // 更新下次可取令牌的时间点
this.storedPermits -= storedPermitsToSpend; // 更新当前存储的令牌数
return returnValue;
}

acquire(int) 方法是获取不到令牌时一直阻塞,直到获取到令牌,tryAcquire(int,long,TimeUnit) 方法则是在指定超时时间内尝试获取令牌,如果获取到或超时时间到则返回是否获取成功

  1. 先判断是否能在指定超时时间内获取到令牌,通过 nextFreeTicketMicros <= timeoutMicros + nowMicros 是否为true来判断,即可取令牌时间早于当前时间加超时时间则可取(预消费的特性),否则不可获取。
  2. 如果不可获取,立即返回false。
  3. 如果可获取,则调用 reserveAndGetWaitLength(permits, nowMicros) 来更新下次可取令牌时间点与当前存储的令牌数,返回等待时间(逻辑与前面相同),并阻塞等待相应的时间,返回true。

源码如下所示

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
long microsToWait;
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) { //判断是否能在超时时间内获取指定数量的令牌
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
} private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros; //只要可取时间小于当前时间+超时时间,则可获取(可预消费的特性!)
} @Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}

以上就是 SmoothBursty 实现的基本处理流程。注意两点:

  1. RateLimiter 通过限制后面请求的等待时间,来支持一定程度的突发请求——预消费的特性。
  2. RateLimiter 令牌桶的实现并不是起一个线程不断往桶里放令牌,而是以一种延迟计算的方式(参考resync函数),在每次获取令牌之前计算该段时间内可以产生多少令牌,将产生的令牌加入令牌桶中并更新数据来实现,比起一个线程来不断往桶里放令牌高效得多。(想想如果需要针对每个用户限制某个接口的访问,则针对每个用户都得创建一个RateLimiter,并起一个线程来控制令牌存放的话,如果在线用户数有几十上百万,起线程来控制是一件多么恐怖的事情)

总结

本文介绍了限流的三种基本算法,其中令牌桶算法与漏桶算法主要用来限制请求处理的速度,可将其归为限速,计数器算法则是用来限制一个时间窗口内请求处理的数量,可将其归为限量(对速度不限制)。Guava 的 RateLimiter 是令牌桶算法的一种实现,但 RateLimiter 只适用于单机应用,在分布式环境下就不适用了。虽然已有一些开源项目可用于分布式环境下的限流管理,如阿里的Sentinel,但对于小型项目来说,引入Sentinel可能显得有点过重,但限流的需求在小型项目中也是存在的,下一篇文章就介绍下基于 RateLimiter 的分布式下的限流实现。


[转载请注明出处]

作者:雨歌

欢迎关注作者公众号:半路雨歌,查看更多技术干货文章

常用限流算法与Guava RateLimiter源码解析的更多相关文章

  1. 限流神器之-Guava RateLimiter 实战

    前段时间,项目中需要对某些访问量较高的路径进行访问并发数控制,以及有些功能,比如Excel导出下载功能,数据量很大的情况下,用户不断的点击下载按钮,重复请求数据库,导致线上数据库挂掉.于是在这样的情况 ...

  2. Spring Cloud Zuul 限流详解(附源码)(转)

    在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选择,只需要编写一个过滤器就可以了,关键在于如何实现限流的算法. ...

  3. Spring Cloud(十二):Spring Cloud Zuul 限流详解(附源码)(转)

    前面已经介绍了很多zuul的功能,本篇继续介绍它的另一大功能.在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选 ...

  4. Google guava cache源码解析1--构建缓存器(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.guava cache 当下最常用最简单的本地缓存 线程安全的本地缓存 类似于ConcurrentHas ...

  5. 第二章 Google guava cache源码解析1--构建缓存器

    1.guava cache 当下最常用最简单的本地缓存 线程安全的本地缓存 类似于ConcurrentHashMap(或者说成就是一个ConcurrentHashMap,只是在其上多添加了一些功能) ...

  6. RateLimiter源码解析

    RateLimiter是Guava包提供的限流器,采用了令牌桶算法,特定是均匀地向桶中添加令牌,每次消费时也必须持有令牌,否则就需要等待.应用场景之一是限制消息消费的速度,避免消息消费过快而对下游的数 ...

  7. Guava Cache源码解析

    概述: 本次主要是分析cache的源码,基本概念官方简介即可. 基本类图: 在官方的文档说明中,Guava Cache实现了三种加载缓存的方式: LoadingCache在构建缓存的时候,使用buil ...

  8. Google guava cache源码解析1--构建缓存器(2)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. CacheBuilder-->maximumSize(long size)     /**       ...

  9. Google guava cache源码解析1--构建缓存器(3)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 下面介绍在LocalCache(CacheBuilder, CacheLoader)中调用的一些方法: Ca ...

随机推荐

  1. 入门大数据---基于Zookeeper搭建Spark高可用集群

    一.集群规划 这里搭建一个 3 节点的 Spark 集群,其中三台主机上均部署 Worker 服务.同时为了保证高可用,除了在 hadoop001 上部署主 Master 服务外,还在 hadoop0 ...

  2. Flask项目实战:创建电影网站(2)

    flask网站制作后台时候常见流程总结 安利一个神神器: 百度脑图PC版 创建数据库 下面是创建User数据库,需要导入db库 #coding:utf8 from flask import Flask ...

  3. vue基础入门(2.3)

    2.3.样式绑定 2.3.1.绑定class样式 1.绑定单个class <!DOCTYPE html> <html lang="en"> <head ...

  4. Security 10:权限管理

    SQL Server 用于管理权限的TSQL命令有:GRANT用于授予权限,REVOKE 用于移除授予的权限,而DENY用于防止安全主体通过GRANT获得权限.但是,SQL Server的权限管理不是 ...

  5. vim中设置tab的长度

  6. 每日一题 - 剑指 Offer 53 - I. 在排序数组中查找数字 I

    题目信息 时间: 2019-07-04 题目链接:Leetcode tag:二分查找 哈希表 难易程度:简单 题目描述: 统计一个数字在排序数组中出现的次数. 示例1: 输入: nums = [5,7 ...

  7. API测试之Postman使用全指南(原来使用 Postman测试API如此简单)

    Postman Postman是一个可扩展的API开发和测试协同平台工具,可以快速集成到CI/CD管道中.旨在简化测试和开发中的API工作流. Postman 工具有 Chrome 扩展和独立客户端, ...

  8. Codeforces 1215D Ticket Game 题解

    Codeforces 1215D Ticket Game 原题 题目 Monocarp and Bicarp live in Berland, where every bus ticket consi ...

  9. DLL注入之windows消息钩取

    DLL注入之windows消息钩取 0x00 通过Windows消息的钩取 通过Windows消息钩取可以使用SetWindowsHookEx.该函数的原型如下: SetWindowsHookEx( ...

  10. js 整理 前端知识点 前端面试题 (2020)(vue)

    数据类型 字符串(String).数字(Number).布尔(Boolean).对空(Null).未定义(Undefined).Symbol. 引用数据类型:对象(Object).数组(Array). ...