1. 常见的限流算法
1.1. 控制最大并发数限流
以秒杀业务为例,秒杀的商品数量少,但进行秒杀的人很多。如秒杀的商品数量是10,但是却有100万人同时发起请求,最终能够抢到的人也就是前面几个人,后面的基本上都没有希望了,那么可以通过控制并发数来实现,比如并发数控制在10个,其他超过并发数的请求全部拒绝,提示:秒杀失败,请稍后重试。
1.2. 漏桶算法限流
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会则直接溢出(即请求被拒绝),可以看出漏桶算法能强行限制数据的传输速率(QPS)。
1.3. 令牌桶算法限流
令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。
令牌桶算法示意图:
1.4. 应用场景
- 令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。
- 漏桶算法用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,调用速度不能超过他的限制,由于不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证别人的系统不被打垮,用漏桶算法。一些简单场景中,直接控制最大并发数即可。
2. 代码示例
2.1 控制最大并发数
使用Semaphore控制同时访问资源的线程个数,如共有3个秒杀的商品,则同时访问资源的个数是3。如果有人下单失败,则释放一个许可,将其返回给信号量,让商品继续参与秒杀。
static Semaphore semaphore = new Semaphore(3);
static Random rand = new Random(47);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
boolean flag = false;
try {
flag = semaphore.tryAcquire(100, TimeUnit.MICROSECONDS);
if (flag) {
boolean isOrderSucc = rand.nextBoolean();
if(!isOrderSucc){
System.out.println(Thread.currentThread() + "下单失败。。。。。");
semaphore.release();
}else{
System.out.println(Thread.currentThread() + "秒杀成功。。。。。");
}
} else {
System.out.println(Thread.currentThread() + ",秒杀失败,请稍微重试!");
}
} catch (InterruptedException e) {
}
}).start();
}
TimeUnit.SECONDS.sleep(5);
}
程序运行结果如下:
2.2. 漏桶算法限流
简单实现的代码示例如下:创建了一个容量为10,流水为60/分钟的漏桶,如果请求太多,会导致桶溢出,则请求直接不处理
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
public class LeakBucketLimitTest {
public static class LeakBucketLimit {
AtomicInteger threadNum = new AtomicInteger(1);
//容量
private int capcity;
//流速
private int flowRate;
//流速时间单位
private TimeUnit flowRateUnit;
private BlockingQueue<Request> queue;
//漏桶流出的任务时间间隔(纳秒)
private long flowRateNanosTime;
public LeakBucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) {
this.capcity = capcity;
this.flowRate = flowRate;
this.flowRateUnit = flowRateUnit;
this.start();
}
//漏桶线程
public void start() {
this.queue = new ArrayBlockingQueue<Request>(capcity);
//漏桶流出的任务时间间隔(纳秒)
this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate;
Thread thread = new Thread(this::bucketWork);
thread.setName("漏桶线程-" + threadNum.getAndIncrement());
thread.start();
}
//漏桶线程开始工作
public void bucketWork() {
while (true) {
Request req = this.queue.poll();
if (Objects.nonNull(req)) {
//唤醒任务线程
LockSupport.unpark(req.thread);
}
//休眠flowRateNanosTime
LockSupport.parkNanos(this.flowRateNanosTime);
}
}
//返回一个漏桶
public static LeakBucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) {
if (capcity < 0 || flowRate < 0) {
throw new IllegalArgumentException("capcity、flowRate必须大于0!");
}
return new LeakBucketLimit(capcity, flowRate, flowRateUnit);
}
//当前线程加入漏桶,返回false,表示漏桶已满;true:表示加入漏桶,并按指定速率处理请求
public boolean flowInfo(int num) {
Thread thread = Thread.currentThread();
Request req = new Request(thread);
if (this.queue.offer(req)) {
LockSupport.park();
return true;
}
return false;
}
}
//漏桶中存放的元素
static class Request {
private Thread thread;
public Request(Thread thread) {
this.thread = thread;
}
}
public static void main(String[] args) throws InterruptedException {
LeakBucketLimit bucketLimit = LeakBucketLimit.build(10, 60, TimeUnit.MINUTES);
for (int i = 0; i < 15; i++) {
final int num = i;
new Thread(() -> {
boolean acquire = bucketLimit.flowInfo(num);
//注意: 如果桶满,则下面的代码立即运行,否则按指定的速率执行
System.out.println(Thread.currentThread().getName() + " " + acquire);
}).start();
}
TimeUnit.SECONDS.sleep(8);
System.out.println("finished");
}
}
2.3. 令牌桶算法限流
令牌桶算法图例
a. 按特定的速率向令牌桶投放令牌
b. 根据预设的匹配规则先对报文进行分类,不符合匹配规则的报文不需要经过令牌桶的处理,直接发送;
c. 符合匹配规则的报文,则需要令牌桶进行处理。当桶中有足够的令牌则报文可以被继续发送下去,同时令牌桶中的令牌 量按报文的长度做相应的减少;
d. 当令牌桶中的令牌不足时,报文将不能被发送,只有等到桶中生成了新的令牌,报文才可以发送。这就可以限制报文的流量只能是小于等于令牌生成的速度,达到限制流量的目的。
示例代码如下:
import com.google.common.base.Preconditions;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
*
*/
class TokenBucketTest {
static class TokenBucket {
// 默认桶大小个数 即最大瞬间流量是64M
private static final int DEFAULT_BUCKET_SIZE = 1024 * 1024 * 64;
// 一个桶的单位是1字节
private int everyTokenSize = 1;
// 瞬间最大流量
private int maxFlowRate;
// 平均流量
private int avgFlowRate;
// 使用队列缓存桶数量:最大的流量峰值就是 = everyTokenSize*DEFAULT_BUCKET_SIZE 64M = 1 * 1024 * 1024 * 64
private ArrayBlockingQueue<Byte> tokenQueue = new ArrayBlockingQueue<Byte>(DEFAULT_BUCKET_SIZE);
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private volatile boolean isStart = false;
private ReentrantLock lock = new ReentrantLock(true);
private static final byte A_CHAR = 'a';
public TokenBucket() {
}
public TokenBucket(int maxFlowRate, int avgFlowRate) {
this.maxFlowRate = maxFlowRate;
this.avgFlowRate = avgFlowRate;
}
public TokenBucket(int everyTokenSize, int maxFlowRate, int avgFlowRate) {
this.everyTokenSize = everyTokenSize;
this.maxFlowRate = maxFlowRate;
this.avgFlowRate = avgFlowRate;
}
public void addTokens(Integer tokenNum) {
// 若是桶已经满了,就不再家如新的令牌
for (int i = 0; i < tokenNum; i++) {
tokenQueue.offer(Byte.valueOf(A_CHAR));
}
}
public TokenBucket build() {
start();
return this;
}
/**
* 获取足够的令牌个数
*
* @return
*/
public boolean getTokens(byte[] dataSize) {
Preconditions.checkNotNull(dataSize);
Preconditions.checkArgument(isStart, "please invoke start method first !");
int needTokenNum = dataSize.length / everyTokenSize + 1;// 传输内容大小对应的桶个数
final ReentrantLock lock = this.lock;
lock.lock();
try {
boolean result = needTokenNum <= tokenQueue.size(); // 是否存在足够的桶数量
if (!result) {
return false;
}
int tokenCount = 0;
for (int i = 0; i < needTokenNum; i++) {
Byte poll = tokenQueue.poll();
if (poll != null) {
tokenCount++;
}
}
return tokenCount == needTokenNum;
} finally {
lock.unlock();
}
}
public void start() {
// 初始化桶队列大小
if (maxFlowRate != 0) {
tokenQueue = new ArrayBlockingQueue<Byte>(maxFlowRate);
}
// 初始化令牌生产者
TokenProducer tokenProducer = new TokenProducer(avgFlowRate, this);
scheduledExecutorService.scheduleAtFixedRate(tokenProducer, 0, 1,
TimeUnit.SECONDS);
isStart = true;
}
public void stop() {
isStart = false;
scheduledExecutorService.shutdown();
}
public boolean isStarted() {
return isStart;
}
//令牌桶生产者,按指定速率在桶中放令牌
class TokenProducer implements Runnable {
private int avgFlowRate;
private TokenBucket tokenBucket;
public TokenProducer(int avgFlowRate, TokenBucket tokenBucket) {
this.avgFlowRate = avgFlowRate;
this.tokenBucket = tokenBucket;
}
@Override
public void run() {
tokenBucket.addTokens(avgFlowRate);
}
}
public static TokenBucket newBuilder() {
return new TokenBucket();
}
public TokenBucket everyTokenSize(int everyTokenSize) {
this.everyTokenSize = everyTokenSize;
return this;
}
public TokenBucket maxFlowRate(int maxFlowRate) {
this.maxFlowRate = maxFlowRate;
return this;
}
public TokenBucket avgFlowRate(int avgFlowRate) {
this.avgFlowRate = avgFlowRate;
return this;
}
private String stringCopy(String data, int copyNum) {
StringBuilder sbuilder = new StringBuilder(data.length() * copyNum);
for (int i = 0; i < copyNum; i++) {
sbuilder.append(data);
}
return sbuilder.toString();
}
}
public static void main(String[] args) throws IOException,
InterruptedException {
tokenTest();
}
private static void tokenTest() throws InterruptedException, IOException {
TokenBucket tokenBucket = TokenBucket.newBuilder().avgFlowRate(512).maxFlowRate(1024).build();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("D:/ds_test")));
String data = "xxxx";// 四个字节
for (int i = 1; i <= 1000; i++) {
Random random = new Random();
int i1 = random.nextInt(100);
boolean tokens = tokenBucket.getTokens(tokenBucket.stringCopy(data, i1).getBytes());
TimeUnit.MILLISECONDS.sleep(100);
if (tokens) {
bufferedWriter.write("token pass --- index:" + i1);
System.out.println("token pass --- index:" + i1);
} else {
bufferedWriter.write("token rejuect --- index" + i1);
System.out.println("token rejuect --- index" + i1);
}
bufferedWriter.newLine();
bufferedWriter.flush();
}
bufferedWriter.close();
}
}
2.4. 使用工具类RateLimiter限流
Google开源工具包Guava提供了限流工具类RateLimiter,可以非常方便的控制系统每秒吞吐量,示例代码如下:从运行日志看,当QPS从5调整到10,阻塞睡眠的时间明显变短
public static void main(String[] args) throws InterruptedException {
/**
* permitsPerSecond为每秒生成的令牌
*
* 平衡稳定
* * 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* * 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
* * 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
* 平衡预热
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
* 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
* 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
* 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
*/
RateLimiter rateLimiter = RateLimiter.create(5,2L,TimeUnit.SECONDS);//设置QPS为5
for (int i = 0; i < 10; i++) {
double sleep = rateLimiter.acquire();
System.out.println(String.format("请求%s被处理,sleep了%s秒",i,sleep));
}
System.out.println("----------");
//可以随时调整速率,我们将qps调整为10
rateLimiter.setRate(10);
for (int i = 0; i < 10; i++) {
double sleep = rateLimiter.acquire();
System.out.println(String.format("请求%s被处理,sleep了%s秒",i,sleep));
}
}