开发高并发系统时有三把利器用来保护系统: 缓存、降级和限流
-
缓存
: 缓存的目的是提升系统访问速度和增大系统处理容量。 -
降级
: 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开。 -
限流
: 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
一、限流算法
常见的限流算法: 令牌桶算法,漏桶算法,与计数器算法。
令牌桶算法
令牌桶算法的原理就是以一个恒定的速度往桶里放入令牌,每一个请求的处理都需要从桶里先获取一个令牌,当桶里没有令牌时,则请求不会被处理,要么排队等待,要么降级处理,要么直接拒绝服务。当桶里令牌满时,新添加的令牌会被丢弃或拒绝。
令牌桶算法主要是: 可以控制请求的平均处理速率,它允许预消费,即可以提前消费令牌,以应对突发请求,但是后面的请求需要为预消费买单(等待更长的时间),以满足请求处理的平均速率是一定的。
漏桶算法
漏桶算法的原理是水(请求)先进入漏桶中,漏桶以一定的速度出水(处理请求),当水流入速度大于流出速度导致水在桶内逐渐堆积直到桶满时,水会溢出(请求被拒绝)。
漏桶算法主要是: 控制请求的处理速率,平滑网络上的突发流量,请求可以以任意速度进入漏桶中,但请求的处理则以恒定的速度进行。
计数器算法
计数器算法是限流算法中最简单的一种算法,限制在一个时间窗口内,至多处理多少个请求。比如每分钟最多处理10个请求,则从第一个请求进来的时间为起点,60s的时间窗口内只允许最多处理10个请求。下一个时间窗口又以前一时间窗口过后第一个请求进来的时间为起点。
常见的比如一分钟内只能获取一次短信验证码的功能可以通过计数器算法来实现。
二、RateLimiter
Guava 的 RateLimiter 是令牌桶算法的一种实现,但 RateLimiter 只适用于单机应用限流,如果想要集群限流,则需要引入redis
或者阿里开源的sentinel
中间件。
Guava的RateLimiter
有两个实现类:平滑突发限流-SmoothBursty
和平滑预热限流SmoothWarmingUp
,其都是令牌桶算法的变种实现,区别在于SmoothBursty加令牌的速度是恒定的,而SmoothWarmingUp会有个预热期,在预热期内加令牌的速度是慢慢增加的,直到达到固定速度为止。
平滑预热限流SmoothWarmingUp适用场景是: 对于有的系统而言刚启动时能承受的QPS较小,需要预热一段时间后才能达到最佳状态。
// 平滑突发限流
public static RateLimiter create(double permitsPerSecond) {
// ...
}
// 平滑预热限流
public static RateLimiter create(
double permitsPerSecond,long warmupPeriod,TimeUnit unit
) {
// ...
}
- permitsPerSecond: RateLimiter的速率,每秒生成的许可证的数量。
- warmupPeriod: 在这段时间内RateLimiter会增加它的速率,在抵达它的稳定速率(最大速率)之前。
- unit: warmupPeriod参数的时间单位。
- 如果permitsPerSecond为负数或者为0, 抛出IllegalArgumentException异常。
请求方法说明
acquire()-阻塞请求
- 从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求。如果存在等待的情况的话,告诉调用者获取到该请求所需要的睡眠时间。 该方法等同于acquire(1)。
acquire(int permits)-阻塞请求
- 从RateLimiter获取指定许可数,该方法会被阻塞直到获取到请求数。如果存在等待的情况的话,告诉调用者获取到这些请求数所需要的睡眠时间。
tryAcquire(long timeout,TimeUnit unit)-非阻塞请求
- 从RateLimiter获取许可如果该许可可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可的话,那么立即返回false(无需等待)。 该方法等同于tryAcquire(1, timeout, unit)。
tryAcquire(int permits)-非阻塞请求
- 从RateLimiter 获取许可数,如果该许可数可以在无延迟下的情况下立即获取得到的话。 该方法等同于tryAcquire(permits, 0, anyUnit)。
tryAcquire()-非阻塞请求
- 从RateLimiter 获取许可,如果该许可可以在无延迟下的情况下立即获取得到的话。 该方法等同于tryAcquire(1)。
tryAcquire(int permits,long timeout,TimeUnit unit)-非阻塞请求
- 从RateLimiter 获取指定许可数如果该许可数可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可数的话,那么立即返回false (无需等待)。
平滑突发限流
使用RateLimiter
的静态方法创建一个限流器,设置每秒放置的令牌数为5个。返回的RateLimiter对象可以保证1秒内不会给超过5个令牌,并且以固定速率进行放置,达到平滑输出的效果。
public class RateLimiterApp {
public static void main(String[] args) throws Exception {
RateLimiter r = RateLimiter.create(5);
while (true) {
System.out.println("get 1 tokens: " + r.acquire() + "s");
}
}
}
/**
output: 基本上都是0.2s执行一次,符合一秒发放5个令牌的设定。
get 1 tokens: 0.0s
get 1 tokens: 0.197932s
get 1 tokens: 0.198233s
get 1 tokens: 0.19996s
get 1 tokens: 0.199421s
*/
RateLimiter
使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
public class RateLimiterApp {
public static void main(String[] args) throws Exception {
RateLimiter r = RateLimiter.create(2);
while (true)
{
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
try {
Thread.sleep(2000);
} catch (Exception e) {}
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
}
}
}
/**
output:
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.49981s
end
get 1 tokens: 0.497264s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.499861s
end
*/
RateLimiter
由于会累积令牌,所以可以应对突发流量。在下面代码中,有一个请求会直接请求5个令牌,但是由于此时令牌桶中有累积的令牌,足以快速响应。
RateLimiter
在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。
public class RateLimiterApp {
public static void main(String[] args) throws Exception {
RateLimiter r = RateLimiter.create(5);
while (true) {
System.out.println("get 5 tokens: " + r.acquire(5) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
}
}
}
/**
output:
get 5 tokens: 0.0s
get 1 tokens: 0.997142s 滞后效应,需要替前一个请求进行等待
get 1 tokens: 0.197697s
get 1 tokens: 0.200034s
end
get 5 tokens: 0.199453s
get 1 tokens: 1.000037s 滞后效应,需要替前一个请求进行等待
get 1 tokens: 0.199008s
get 1 tokens: 0.19974s
end
get 5 tokens: 0.199607s
get 1 tokens: 0.999362s 滞后效应,需要替前一个请求进行等待
get 1 tokens: 0.200178s
get 1 tokens: 0.199999s
end
*/
平滑预热限流
RateLimiter
的SmoothWarmingUp
是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。
比如下面代码中的例子,创建一个平均分发令牌速率为2,预热期为3秒钟。由于设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。这种功能适合系统刚启动需要一点时间来“热身”的场景。
public class RateLimiterApp {
public static void main(String[] args) throws Exception {
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
for (int i = 0; i < 5; i++) {
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
if (i == 2) {
System.out.println("冷却中。。。");
TimeUnit.SECONDS.sleep(5);
}
}
}
}
/** output:
get 1 tokens: 0.0s
get 1 tokens: 1.326011s
get 1 tokens: 0.997863s
get 1 tokens: 0.665804s 上边三次获取的时间相加正好为3秒
end
get 1 tokens: 0.498552s 正常速率0.5秒一个令牌
get 1 tokens: 0.499138s
get 1 tokens: 0.499813s
get 1 tokens: 0.499168s
end
get 1 tokens: 0.499752s
get 1 tokens: 0.499633s
get 1 tokens: 0.499589s
get 1 tokens: 0.499271s
end
冷却中。。。
get 1 tokens: 0.0s
get 1 tokens: 1.333139s
get 1 tokens: 1.000213s
get 1 tokens: 0.666137s 经过冷却后, 又开始预热
end
get 1 tokens: 0.499337s
get 1 tokens: 0.500058s
get 1 tokens: 0.499509s
get 1 tokens: 0.499485s
end
*/
三、模拟限流
public class RateLimiterApp {
public static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws Exception {
// 限制每秒只能接收3个请求
final RateLimiter rateLimiter = RateLimiter.create(3);
ExecutorService executor = Executors.newCachedThreadPool();
Random random = new Random();
for (int i = 0; i < 5; i++) {
int requestCount = random.nextInt(10);
log("id=%s当前时间共有%s个请求", i, requestCount);
for (int m = 0; m < requestCount; m++) {
final String index = i + "-" + m;
executor.execute(() -> {
long start = System.currentTimeMillis();
// 非阻塞,如果没有拿到令牌直接返回false
boolean acquire = rateLimiter.tryAcquire();
if (!acquire) {
log("第%s次请求, 系统繁忙", index);
return;
}
// 模拟业务执行
try {
TimeUnit.SECONDS.sleep(random.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log("id=%s, 准备执行完成, 耗时=%s ms.",
index, (end - start));
});
}
// 等待15秒,给执行中的任务缓存期
executor.awaitTermination(15, TimeUnit.SECONDS);
System.out.println("\n\n");
}
executor.shutdown();
}
private static void log(String format, Object... args) {
System.out.println(
String.format("%s " + String.format(format, args), LocalDateTime.now().format(DATETIME_FORMAT)));
}
}
四、Reference