1 Semaphore和RateLimiter
限流是保护高并发系统的三把利器之一,另外两个是缓存和降级。限流在很多场景中用来限制并发和请求量,比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等。
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务或进行流量整形。
常用的限流方式和场景有:
- 限制总并发数(比如数据库连接池、线程池)
- 限制瞬时并发数(如nginx的limitconn模块,用来限制瞬时并发连接数,Java的Semaphore也可以实现)
- 限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率)
- 其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流
比如说,我们需要限制方法被调用的并发数不能超过100(同一时间并发数),则我们可以用信号量 Semaphore实现。
可如果我们要限制方法在一段时间内平均被调用次数不超过100,则需要使用 RateLimiter。
RateLimiter 与 Semaphore区别
- RateLimiter限制的是速率
- Semaphore限制的是并发数
2 Semaphore(信号量,需要多线程使用)
Semaphore:是一种计数器,用来保护一个或者多个共享资源的访问。
通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
常用于控制某一controller最大并发数,设置new Semaphore(5)
,意思是当前请求最大并发为5:
* 1、请求可以进来,但是没有资源时需要等待。
* 2、当前controller有资源时,开始处理当前请求。
2.1 导入依赖
位于package java.util.concurrent;
,无需导入
2.2 方法解释
构造方法
- permits:初始许可数,也就是最大访问线程数
- fair:当设置为false时,创建的信号量为非公平模式;当设置为true时,信号量是公平模式
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}
主要方法
-
void acquire() :从信号量获取一个许可,如果无可用许可前将一直阻塞等待
-
void acquire(int permits) :获取指定数目的许可,如果无可用许可前也将会一直阻塞等待
-
void acquireUninterruptibly() :获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
-
boolean tryAcquire():从信号量尝试获取一个许可,如果无可用许可,直接返回false,不会阻塞
-
boolean tryAcquire(int permits): 尝试获取指定数目的许可,如果无可用许可直接返回false
-
boolean tryAcquire(int permits, long timeout, TimeUnit unit): 在指定的时间内尝试从信号量中获取许可,如果在指定的时间内获取成功,返回true,否则返回false
-
void release(): 释放一个许可,别忘了在finally中使用,注意:多次调用该方法,会使信号量的许可数增加,达到动态扩展的效果,如:初始permits为1, 调用了两次release,最大许可会改变为2
-
int availablePermits(): 获取当前信号量可用的许可数量
2.3 Semaphore使用流程解析
1、Semaphore初始化
Semaphore semaphore=new Semaphore(2);
- 当调用new Semaphore(2) 方法时,默认会创建一个非公平的锁的同步阻塞队列。
- 把初始令牌数量赋值给同步队列的state状态,state的值就代表当前所剩余的令牌数量。
2、获取令牌
semaphore.acquire();
-
当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-1。
-
当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。
-
当计算出来的state>=0,则代表获取令牌成功。
源码:
/**
* 获取1个令牌
*/
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* 共享模式下获取令牌,获取成功则返回,失败则加入阻塞队列,挂起线程
* @param arg
* @throws InterruptedException
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取令牌,arg为获取令牌个数,当可用令牌数减当前令牌数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
/**
* 1、创建节点,加入阻塞队列,
* 2、重双向链表的head,tail节点关系,清空无效节点
* 3、挂起当前节点线程
* @param arg
* @throws InterruptedException
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//创建节点加入阻塞队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获得当前节点pre节点
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);//返回锁的state
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//重组双向链表,清空无效节点,挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
3、释放令牌
semaphore.release();
当调用semaphore.release() 方法时
-
线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程
-
释放令牌成功之后,同时会唤醒同步队列中的一个线程。
-
被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程。
源码:
/**
* 释放令牌
*/
public void release() {
sync.releaseShared(1);
}
/**
*释放共享锁,同时会唤醒同步队列中的一个线程。
* @param arg
* @return
*/
public final boolean releaseShared(int arg) {
//释放共享锁
if (tryReleaseShared(arg)) {
//唤醒所有共享节点线程
doReleaseShared();
return true;
}
return false;
}
/**
* 唤醒同步队列中的一个线程
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//是否需要唤醒后继节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//修改状态为初始0
continue;
unparkSuccessor(h);//唤醒h.nex节点线程
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
}
if (h == head) // loop if head changed
break;
}
}
2.4 使用示例
@Slf4j
public class MySemaphore {
public static void main(String[] args) {
Eat();
}
/**
* 常用于控制某一controller最大并发数,
* 设置new Semaphore(5),意思是当前请求最大并发为5
* 1、请求可以进来,但是没有资源时需要等待。
* 2、当前controller有资源时,开始处理当前请求。
* <p>
* 模拟食堂窗口打饭
*/
public static void Eat() {
// 一共有三个窗口
Semaphore semaphore = new Semaphore(3);
// 10个同学来打饭
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
log.info("====" + Thread.currentThread().getName() + "来到食堂窗口====");
if (semaphore.availablePermits() == 0) {
log.info("等待前面同学打饭ing...");
}
// 如果获取不到令牌则阻塞线程
semaphore.acquire();
log.info(Thread.currentThread().getName() + "成功打饭");
// 模拟打饭时间
Thread.sleep(1000 * 2);
log.info(Thread.currentThread().getName() + "离开食堂");
//释放令牌,让其他线程进入
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, i + "同学");
thread.start();
}
}
}
2.5 运行结果
3 RateLimiter(限流器,适用于单、多线程)
RateLimiter是一个基于令牌桶算法实现的限流器,常用于控制网站的QPS。
与Semaphore不同,Semaphore控制的是某一时刻的访问量,RateLimiter控制的是某一时间间隔的访问量。
通常可应用于
- 抢购限流防止冲垮系统;
- 限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;
- 限制网速,单位时间内只允许上传下载多少字节等。
3.1 导入依赖
使用Guava RateLimiter
Guava
是Java领域优秀的开源项目,它包含了Google在Java项目中使用一些核心库,包含集合(Collections),缓存(Caching),并发编程库(Concurrency),常用注解(Common annotations),String操作,I/O操作方面的众多非常实用的函数。
Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
3.2 方法解释
构造方法
-
根据指定的稳定吞吐率创建限流器,这里的吞吐率是指每秒多少许可书(通常是QPS、每秒多少个查询)
public static RateLimiter create(double permitsPerSecond)
-
根据指定的稳定吞吐率和预热器来创建限流器,这里的吞吐率是指每秒多少许可书(通常是QPS、每秒多少个请求量),在这段预热时间内,RateLimiter 每秒分配的许可书会平稳的增长直到预热期结束时达到其最大速率。(前提是请求足够,能够使其饱和)。
通俗来讲就是warmupPeriod
秒内创建permitsPerSecond
个许可。public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
主要方法
-
从RateLimiter 获取一个许可,该方法会阻塞直到获取到请求
public double acquire()
-
从RateLimiter 获取指定许可数,该方法会阻塞直到获取到请求
public double acquire(int permits)
-
从RateLimiter获取许可,如果该许可可以在无延迟情况下立即获取到的话
public boolean tryAcquire()
-
从RateLimiter获取permits个许可,如果该许可可以在无延迟情况下立即获取到的话
public boolean tryAcquire(int permits)
-
在timeout时间内从RateLimiter获取permits个许可,如果无法在指定时间内获取到则会返回false(无需等待)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
-
更新RateLimiter的稳定速率,参数permitsPerSecond由构造RateLimiter的工厂方法提供
public final void setRate(double permitsPerSecond)
-
返回RateLimiter配置中的稳定速率,该速率单位是每秒多少个许可数
public final double getRate()
3.3 平滑突发限流和平滑预热限流
// RateLimiter提供了两个工厂方法,最终会调用下面两个函数,生成RateLimiter的两个子类。
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
static RateLimiter create(
SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit,
double coldFactor) {
RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
平滑突发限流
使用 RateLimiter的静态方法创建一个限流器,设置每秒放置的令牌数为5个。返回的RateLimiter对象可以保证1秒内不会给超过5个令牌,并且以固定速率进行放置,达到平滑输出的效果。
public void testSmoothBursty() {
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.182014s
* get 1 tokens: 0.188464s
* get 1 tokens: 0.198072s
* get 1 tokens: 0.196048s
* get 1 tokens: 0.197538s
* get 1 tokens: 0.196049s
*/
RateLimiter使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
public void testSmoothBursty2() {
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("end");
/**
* output:
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* end
* get 1 tokens: 0.499796s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
*/
}
}
RateLimiter由于会累积令牌,所以可以应对突发流量。在下面代码中,有一个请求会直接请求5个令牌,但是由于此时令牌桶中有累积的令牌,足以快速响应。 RateLimiter在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。
public void testSmoothBursty3() {
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.996766s 滞后效应,需要替前一个请求进行等待
* get 1 tokens: 0.194007s
* get 1 tokens: 0.196267s
* end
* get 5 tokens: 0.195756s
* get 1 tokens: 0.995625s 滞后效应,需要替前一个请求进行等待
* get 1 tokens: 0.194603s
* get 1 tokens: 0.196866s
*/
}
}
平滑预热限流
每三秒生成两个令牌
RateLimiter
的 SmoothWarmingUp
是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。 比如下面代码中的例子,创建一个平均分发令牌速率为2,预热期为3秒。由于设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。这种功能适合系统刚启动需要一点时间来“热身”的场景。
public void testSmoothwarmingUp() {
// 每三秒生成两个令牌
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
while (true)
{
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: 1.329289s
* get 1 tokens: 0.994375s
* get 1 tokens: 0.662888s 上边三次获取的时间相加正好为3秒
* end
* get 1 tokens: 0.49764s 正常速率0.5秒一个令牌
* get 1 tokens: 0.497828s
* get 1 tokens: 0.49449s
* get 1 tokens: 0.497522s
*/
}
}
3.4 使用示例
@Slf4j
public class MyRateLimiter {
public static void main(String[] args) {
test1();
}
public static void test1() {
// 新建一个每秒限制3个的令牌桶
RateLimiter rateLimiter = RateLimiter.create(3);
// 每秒只能执行三次(qps=3)
for (int i = 0; i < 20; i++) {
//阻塞直到可以授予请求,acquire是距离上次执行的时间
double acquire = rateLimiter.acquire();
log.info("发送了短信:" + i);
}
}
public static void test2() {
//新建一个每秒限制3个的令牌桶
RateLimiter rateLimiter = RateLimiter.create(3.0);
ExecutorService executor = Executors.newFixedThreadPool(16);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
//获取令牌桶中一个令牌,最多等待10秒
boolean res = rateLimiter.tryAcquire(1, 10, TimeUnit.SECONDS);
if (res) {
log.info("得到了许可");
}
});
}
executor.shutdown();
}
}
参考文章
超详细的Guava RateLimiter限流原理解析
https://www.cnblogs.com/wxgblogs/p/5422508.html
https://zhuanlan.zhihu.com/p/98593407
https://zhuanlan.zhihu.com/p/69986837
https://zhuanlan.zhihu.com/p/60979444