1.作用
跟JDK的信号量Semaphore作用比较像,Semaphore用于控制,如下代码允许被多个线程同时访问的线程数量:
try {
semaphore.acquire();
//被控制代码....
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
而RateLimiter用于控制1秒钟允许操作代码块的次数(也就是控制操作速率(匀速))的
package com.mzj.guava.concurrent.ratelimiter;
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class RateLimiterExample {
/**
* 1秒钟只允许有0.5次操作,也就是只允许2秒执行一次操作(testLimiter方法)
*/
private final static RateLimiter limiter = RateLimiter.create(0.5);
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
IntStream.range(0,10).forEach(i -> service.submit(RateLimiterExample::testLimiter));
}
private static void testLimiter(){
System.out.println(Thread.currentThread().getName() + " waiting " + limiter.acquire());
//被控制的代码。。。
}
}
代码示例
2、典型应用场景
2.1 使用RateLimiter实现漏桶算法
背景:虽然可以通过水平扩展的方式(增加业务系统服务器节点数量)扩容业务系统,使得业务系统可以承载更多的并发量,但是系统中的一些稀缺资源,如数据库连接、CPU、内存、硬盘等,面对扩容前后的请求是固定不变的,所以要进行限流,也就是根据实际的承载能力(并发的数据库连接/系统计算能力等等指标:比如200个/秒 速率进行业务受理),限制并发处理的请求量,限流常使用漏桶算法,而漏桶算法常用RateLimiter进行实现。
行业场景:①秒杀系统中的:限流(漏桶算法),②电信行业月初+月末业务量高峰期
漏桶算法原理
桶的上游请求数量,不进行任何限制(顶多给个桶的上沿),而桶的下游始终是固定的,比如1秒中固定处理200个请求,超出(上沿)的请求,一种处理方式为降级,即告诉客户端请求超限,稍后重试,另一种方式可以写入数据库, 然后告诉客户端请求会稍后处理。这样,并发处理的业务量最多是200个请求。
漏桶算法实现
桶:Bucket.java
package com.mzj.guava.concurrent.ratelimiter.bucket;
import com.google.common.util.concurrent.Monitor;
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
public class Bucket {
//桶
private final ConcurrentLinkedQueue<Integer> container = new ConcurrentLinkedQueue<>();
//桶的上沿
private final static int BUCKET_LIMIT = 1000;//最多能向桶里放入1000个请求
//桶的下沿
private final RateLimiter limiter = RateLimiter.create(10);//做多1秒只能处理10个请求
//是否允许放入桶的Monitor
private final Monitor offerMonitor = new Monitor();
//是否允许从桶中获取元素
private final Monitor pollMonitor = new Monitor();
//桶的上游向桶中放数据
public void submit(Integer data) {
if (offerMonitor.enterIf(offerMonitor.newGuard(() -> container.size() < BUCKET_LIMIT))) {
try {
container.offer(data);
System.out.println(Thread.currentThread() + " submit data " + data + ",current size : " + container.size());
} finally {
offerMonitor.leave();
}
} else {
//超过桶的上沿,则降级
throw new IllegalStateException("The bucket is full.");
}
}
public void takeTheConsumer(Consumer<Integer> consumer) {
if (pollMonitor.enterIf(pollMonitor.newGuard(() -> !container.isEmpty()))) {
try {
System.out.println(Thread.currentThread() + " waiting " + limiter.acquire());
consumer.accept(container.poll());
} finally {
pollMonitor.leave();
}
}
}
}
测试代码:BucketTest.java
package com.mzj.guava.concurrent.ratelimiter.bucket;
import org.omg.CORBA.DATA_CONVERSION;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public class BucketTest {
public static void main(String[] args) {
final Bucket bucket = new Bucket();
final AtomicInteger DATA_CREATOR = new AtomicInteger(0);
//生产者线程:5个线程向桶中提交数据(每个线程1秒提交5次请求,总计25个请求)
IntStream.range(0,5).forEach(i -> {
new Thread(() ->{
for (;;){
int data = DATA_CREATOR.getAndIncrement();
bucket.submit(data);
try{
TimeUnit.MILLISECONDS.sleep(200L);
}catch (Exception e){
if(e instanceof IllegalStateException){
System.out.println(e.getMessage());
}
}
}
}).start();
});
//消费者线程
//生产者线程:也是5个线程从桶中拿请求进行处理,但是多少个线程不重要,因为限制为每秒只能处理10个
IntStream.range(0,5).forEach(i -> {
new Thread(() ->{
for (;;){
bucket.takeTheConsumer(x-> System.out.println(Thread.currentThread()+ " W " + x));
}
}).start();
});
}
}
实际开发中关键点
请求溢出时的降级方法,目的是用户体验要好,比如:及时通知客户端,稍后处理,或者提示目前系统繁忙,请稍后再试
代码示例
2.2 使用RateLimiter实现令牌桶算法
引入:漏桶算法不关心往桶里放入的请求数量是多少,而关注的是处理请求时控制在一种匀速的速率进行处理,比如100个/s
而令牌桶与漏桶是相反的,令牌桶关注(限制)的是放入桶的速率。
但是需要注意:rateLimiter获取令牌,仅仅控制了调用rateLimiter.tryAcquire后进入下面方法体的速率(不管几个线程),而并未同步进入方法体的线程,所以如果设计多线程访问共享资源,要另外进行同步处理!!!
代码示例