一.总体介绍
很多做服务接口的人或多或少的遇到这样的场景,由于业务应用系统的负载能力有限,为了防止非预期的请求对系统压力过大而拖垮业务应用系统。也就是面对大流量时,如何进行流量控制?
服务接口的流量控制策略:分流、降级、限流等。本文讨论 限流策略,虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。
实际场景中常用的限流策略:
1.Nginx前端限流
按照一定的规则如帐号、IP、系统调用逻辑等在Nginx层面做限流
2.业务应用系统限流
1、客户端限流
2、服务端限流
3.数据库限流
红线区,力保数据库
二.常用的限流算法
常见的限流算法有:令牌桶、漏桶。 计数器也可以进行粗暴限流实现。
2.1 令牌桶(单机)
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小
流程:
1.所有的流量在放行之前需要获取一定量的 token;
2.所有的 token 存放在一个 bucket(桶)当中,每 1/r 秒,都会往这个 bucket 当中加入一个 token;
3.bucket 有最大容量(capacity or limit),在 bucket 中的 token 数量等于最大容量,而且没有 token 消耗时,新的额外的 token 会被抛弃。
这种实现方法有几个优势:
1.避免了给每一个 Bucket 设置一个定时器这种笨办法,
2.数据结构需要的内存量很小,只需要储存 Bucket 中剩余的 Token 量以及上次补充 Token 的时间戳就可以了;
3.只有在用户访问的时候,才会计算 Token 补充量,对于系统的计算资源占用量也较小。
Guava 库当中也有一个 RateLimiter,其作用也是 用来进行限流,于是阅读了 RateLimiter 的源代码,查看一些 Google 的人是如何实现 Token Bucket 算法的。
private void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
storedPermits = min(maxPermits,
storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
nextFreeTicketMicros = nowMicros;
}
}
在 resync 方法中的这句代码 storedPermits = min(maxPermits, storedPermits+ (nowMicros - nextFreeTicketMicros)/stableIntervalMicros); 就是 RateLimiter 中计算 Token 数量的方法。没有使用计时器,而是使用时间戳的方式计算。这个做法给足了 信息。我们可以在 Bucket 中存放现在的 Token 数量,然后存储上一次补充 Token 的时间戳,当用户下一次请求获取一个 Token 的时候, 根据此时的时间戳,计算从上一个时间戳开始,到现在的这个时间点所补充的所有 Token 数量,加入到 Bucket 当中。
通过使用RateLimiter简单模拟一个实现:
package com.niepeng.goldcode.common.ratelimit;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.google.common.util.concurrent.RateLimiter;
import com.niepeng.goldcode.util.DateUtil;
/**
* 介绍文档:google的ratelimiter文档翻译
* http://ifeve.com/guava-ratelimiter/
*
* @author niepeng
*
*/
public class ApiCallDemo {
private int permitsPerSecond = 10; // 每秒10个许可
private int threadNum = 3;
public static void main(String[] args) {
new ApiCallDemo().call();
}
private void call() {
ExecutorService executor = Executors.newFixedThreadPool(threadNum);
final RateLimiter rateLimiter = RateLimi