这是几年前写的旧文,此前发布Wordpress小站上,现在又重新整理。算是温故知新,后续会继续整理。如有错误望及时指出,在此感谢。
遇到什么问题?
1.接口服务被无序调用,导致服务响应慢,出现各种异常;
2.业务资源如数据库,避免被大量请求导致服务被击穿;
3.硬件资源如cpu等面对高并发情况下无法及时响应;
解决方法有哪些?
1.增加缓存机制;
2.对业务进行限流;
这次以限流为目标,使用Google开源的Guava-RateLimiter
3.对业务进行降级;
为什么要用RateLimiter?有没有替代方案?
当然有的,JDK自带的Concurrent包中的Semaphore也可以当作限流器来使用。
那为什么要用RateLimiter呢?
因为RateLimiter使用起来更方便,更简洁。
Semaphore只能控制并发总量,而RateLimiter不光可以控制并发总量,还能控制并发的速率。
RateLimiter常用的方法:
1.acquire
返回一个令牌,会有等待的过程,返回值是等待的时长,单位为秒;
可以一次调用获取多个令牌;
2.tryAcquire
尝试立即获取令牌,可以尝试获取多个;
返回结果表示是否成功获取到令牌;
3.实例化
工厂方法 RateLimiter.create(double permitsPerSecond)
默认返回的是SmoothBursty子类,一种每秒按照固定(permitsPerSecond)数量生成令牌;
场景演示
JDK:1.8
Maven
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
方法被无序调用(code_01)
import com.google.common.util.concurrent.RateLimiter;
public class GuavaRateLimiterTest1 {
static RateLimiter rateLimiter = RateLimiter.create(2);
public static void main(String[] args) {
//RateLimiter常用的方法有:
//1.acquire,返回一个令牌,会有等待的过程,返回值是等待的时长,单位为秒;可以一次调用获取多个令牌;
//2.tryAcquire,立即获取令牌,可以尝试获取多个;返回结果表示是否成功获取到令牌;
//RateLimiter的默认构造器返回的是SmoothBursty,是一种每秒按照固定速率生成令牌
//为了测试出效果,这里使用多个线程重复模拟请求同一个方法
for (int i = 0; i < 10; i++) {
print(i);
}
// 第[0]次获取,当前时间:1649777733792,waitTime:0.0
// 第[1]次获取,当前时间:1649777734292,waitTime:0.498631
// 第[2]次获取,当前时间:1649777734791,waitTime:0.498059
// 第[3]次获取,当前时间:1649777735292,waitTime:0.499828
// 第[4]次获取,当前时间:1649777735791,waitTime:0.499297
// 第[5]次获取,当前时间:1649777736291,waitTime:0.499883
// 第[6]次获取,当前时间:1649777736791,waitTime:0.499408
// 第[7]次获取,当前时间:1649777737292,waitTime:0.499867
// 第[8]次获取,当前时间:1649777737791,waitTime:0.499272
// 第[9]次获取,当前时间:1649777738291,waitTime:0.499843
}
public static void print(int ct) {
//acquire的返回值是拿到令牌所等待的时长,单位为秒
double waitTime = rateLimiter.acquire();
System.out.println("第[" + ct + "]次获取,当前时间:" + System.currentTimeMillis() + ",waitTime:" + waitTime);
}
}
多线程并发抢占资源(code_02)
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.CountDownLatch;
public class GuavaRateLimiterTest2 {
//限速器
static RateLimiter rateLimiter = RateLimiter.create(8);
//并发测试控制器
static CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
//RateLimiter的默认构造器返回的是SmoothBursty,是一种每秒按照固定速率生成令牌
//RateLimiter常用的方法有:
//1.acquire,返回一个令牌,会有等待的过程,返回值是等待的时长,单位为秒;可以一次调用获取多个令牌;
//2.tryAcquire,立即获取令牌,可以尝试获取多个;返回结果表示是否成功获取到令牌;
System.out.println("--------------------------");
System.out.println("let's...");
System.out.println("--------------------------");
//为了测试出效果,这里使用多个线程重复模拟请求同一个方法
//设置每秒生成8个令牌,当获取令牌失败时,会抛出异常,代表请求被拒绝
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println("thread:" + Thread.currentThread().getName() + ",ready...");
latch.await();
//Guava自带的测试工具进行翻车检查
Preconditions.checkState(rateLimiter.tryAcquire(), "thread:" + Thread.currentThread().getName() + "令牌不足访问被拒绝...");
//获取令牌成功,执行具体业务
System.out.println("thread:" + Thread.currentThread().getName() + ",doing...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t" + i).start();
}
Thread.sleep(3000L);
latch.countDown();
System.out.println("--------------------------");
System.out.println("go!");
System.out.println("--------------------------");
// --------------------------
// let's...
// --------------------------
// thread:t0,ready...
// thread:t2,ready...
// thread:t1,ready...
// thread:t3,ready...
// thread:t5,ready...
// thread:t4,ready...
// thread:t6,ready...
// thread:t8,ready...
// thread:t9,ready...
// thread:t7,ready...
// --------------------------
// go!
// --------------------------
// thread:t2,doing...
// thread:t7,doing...
// thread:t8,doing...
// thread:t5,doing...
// thread:t9,doing...
// thread:t1,doing...
// thread:t3,doing...
// thread:t6,doing...
// thread:t4,doing...
// Exception in thread "t0" java.lang.IllegalStateException: thread:t0令牌不足访问被拒绝...
// at com.google.common.base.Preconditions.checkState(Preconditions.java:507)
// at com.xxx.guava.GuavaRateLimiterTest2.lambda$main$0(GuavaRateLimiterTest2.java:41)
// at java.lang.Thread.run(Thread.java:748)
}
}
模拟限速下载器(code_03)
import com.google.common.util.concurrent.RateLimiter;
public class GuavaRateLimiterTest3 {
static int limit = 3;
static RateLimiter limiter = RateLimiter.create(limit);
public static void main(String[] args) {
//利用RateLimiter定时生成令牌的特性,做一个速率控制器(限速器),模拟某盘的下载控制功能
//当然实际的限速器要比这个复杂的多,这里只是开阔下思维,同时与JDK自带的Semaphore进行下比较
//Semaphore用来控制并发总量,而RateLimiter用来控制并发速率
String inputStr = "阿富汗战争,即2001年阿富汗战争,是以 美国 为首的联军在2001年10月7日起对 “基地”组织 和塔利班的一场战争,该战争是美国对 9·11事件 的报复。";
System.out.println("文件内容:" + inputStr);
System.out.println("文件长度:" + inputStr.length());
System.out.println("--------------------------");
long startTime = System.currentTimeMillis();
String downloadStr = download(inputStr);
long stopTime = System.currentTimeMillis();
System.out.println("下载开始时间:" + startTime);
System.out.println("下载结束时间:" + stopTime);
System.out.println("下载耗时(毫秒):" + (stopTime - startTime));
System.out.println("下载的文件内容:" + downloadStr);
System.out.println("下载的文件长度:" + downloadStr.length());
System.out.println("文件内容是否相等:" + downloadStr.equals(inputStr));
// 文件内容:阿富汗战争,即2001年阿富汗战争,是以 美国 为首的联军在2001年10月7日起对 “基地”组织 和塔利班的一场战争,该战争是美国对 9·11事件 的报复。
// 文件长度:79
// --------------------------
// 下载开始时间:1649750529917
// 下载结束时间:1649750555917
// 下载耗时(毫秒):26000
// 下载的文件内容:阿富汗战争,即2001年阿富汗战争,是以 美国 为首的联军在2001年10月7日起对 “基地”组织 和塔利班的一场战争,该战争是美国对 9·11事件 的报复。
// 下载的文件长度:79
// 文件内容是否相等:true
}
public static String download(String inputStr) {
StringBuffer sb = new StringBuffer();
char[] inputChars = inputStr.toCharArray();
final int str_length = inputStr.length();
int start_index = 0;
int len = limit;
while (start_index < str_length) {
limiter.acquire(limit);
if ((start_index + limit) >= str_length) {
len = str_length - start_index;
}
String s = new String(inputChars, start_index, len);
//System.out.println("s:" + s + ",start_index:" + start_index + ",len:" + len);
sb.append(inputChars, start_index, len);
start_index += limit;
}
return sb.toString();
}
}
总结
RateLimiter没有Release方法,不需要手动进行令牌的回收释放;
RateLimiter默认按照设定的参数,每秒固定生成令牌数量,不光可以简单控制并发总量,更重要能控制访问的速率,粒度更细;
RateLimiter的Acquire可以一次返回多个令牌,对业务的弹性来讲丰富了很多场景;